SDK なしで Gemini Live API を WebSocket で直接叩く
Gemini Live API の応答遅延を計測しようとして、SDK を挟むと「遅いのは API なのか SDK の処理なのか」の切り分けができないことに気づきました。もうひとつ、検証環境の一部が企業プロキシ+社内 CA という構成で、SDK がそのまま繋がる保証もありません。
そこで SDK を使わず、ws と https-proxy-agent だけで Live API と直接話す計測プローブを書きました。プロトコルの実体は素の WebSocket + JSON なので、約 100 行で音声対話が成立します。この記事はその材料一式です(v1beta、2026-08 動作確認。プロトコル形状はバージョン依存なので時点に注意してください)。
エンドポイント
wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=API_KEY認証はクエリパラメータの API キーです。ブラウザから直接叩く構成ならキーを晒さないために ephemeral token を使いますが、サーバー側で動かす計測プローブなのでキー直渡しにしています。
メッセージシーケンス
接続後のやり取りは 4 種類覚えれば足ります。
手順 1: クライアント → setup(接続後の最初のメッセージ)
{
"setup": {
"model": "models/gemini-2.5-flash-native-audio-preview-12-2025",
"generationConfig": {
"responseModalities": ["AUDIO"],
"thinkingConfig": { "thinkingBudget": 0 }
},
"systemInstruction": { "parts": [{ "text": "..." }] },
"outputAudioTranscription": {},
"contextWindowCompression": { "slidingWindow": {} }
}
}手順 2: サーバー → `{"setupComplete": {}}`。これが来たら送信可能です。
手順 3: クライアント → clientContent(テキストを送って喋らせる)
{
"clientContent": {
"turns": [{ "role": "user", "parts": [{ "text": "..." }] }],
"turnComplete": true
}
}turnComplete: true が必須です。false だと無言で無視されます(実測の詳細は別記事に書きました)。
手順 4: サーバー → serverContent ほか(複数メッセージで流れてくる)
受信側で見るフィールドは次のとおりです。
serverContent.modelTurn.parts[].inlineData— 音声本体。{"mimeType": "audio/pcm;rate=24000", "data": "<base64>"}の形で、中身は 24kHz / 16bit / mono / little-endian の生 PCMserverContent.outputTranscription.text— 発話転写の差分テキストserverContent.turnComplete: true— ターンの終端serverContent.interrupted: true— 生成の中断usageMetadata— トークン消費。promptTokensDetails/responseTokensDetailsにモダリティ別の内訳が入るgoAway.timeLeft— 接続終了の予告(接続は約 10 分で切られます。セッション制限の記事参照)
プロキシ環境対応
このプローブを書いた理由の半分はここです。企業プロキシ配下では素の new WebSocket(url) は外に出られません。https-proxy-agent を挟みます。
import WebSocket from "ws";
import { HttpsProxyAgent } from "https-proxy-agent";
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy;
const ws = new WebSocket(url, proxy ? { agent: new HttpsProxyAgent(proxy) } : {});社内 CA(TLS を復号して検査するタイプのプロキシ)の環境では、Node に CA 証明書を教える必要がありますが、これはコード変更不要です。NODE_EXTRA_CA_CERTS 環境変数が設定されていれば Node の TLS 層が自動で拾います。
NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem node probe.mjs受信した音声をファイルで聴く
音声は生 PCM のチャンクで届くので、そのままでは再生できません。検証では WAV ヘッダーを 44 バイト手組みして聴けるファイルにしました。
function wav(pcmBufs) {
const data = Buffer.concat(pcmBufs);
const h = Buffer.alloc(44);
h.write("RIFF", 0); h.writeUInt32LE(36 + data.length, 4); h.write("WAVE", 8);
h.write("fmt ", 12); h.writeUInt32LE(16, 16); h.writeUInt16LE(1, 20); h.writeUInt16LE(1, 22);
h.writeUInt32LE(24000, 24); h.writeUInt32LE(24000 * 2, 28); h.writeUInt16LE(2, 32); h.writeUInt16LE(16, 34);
h.write("data", 36); h.writeUInt32LE(data.length, 40);
return Buffer.concat([h, data]);
}24,000Hz × 16bit × mono は 48 バイト/ミリ秒です。この換算を覚えておくと、受信バイト数から音声の長さ(=モデルが何秒喋ったか)を計算でき、計測項目がひとつ増えます。
計測プローブとして動かした結果
このプローブで取った数字の一部です(2026-08-18、Node 22、データセンター級回線)。
- WebSocket 接続確立: 340〜480ms
- テキストキュー送信 → 最初の音声チャンク受信: 0.6〜1.0 秒(
thinkingBudget: 0のとき。既定の thinking 有効だと数秒に伸びます。初動遅延の記事参照) - 原価:
usageMetadataのモダリティ別集計で、テキストキュー 7 発話のセッションがおよそ $0.01〜0.02(2.5 系、音声出力 $12/100 万トークンの料金で計算。料金は 2026-08 時点)
SDK を使わないことの利点は、この数字が「API そのものの数字」だと言い切れることです。遅延に SDK のバッファリングや再接続ロジックが混ざりません。逆に言うと、再接続・エラー処理・音声のストリーミング再生などは全部自分で書くことになるので、プロダクションコードを SDK なしで書く理由は薄いです。用途は計測と切り分け、それから「SDK が対応していない環境で動くか」の確認までだと思います。
この構成で気をつけること
- プロトコルは v1beta の形状(2026-08 確認)。バージョンが上がればフィールド名ごと変わり得る
setupは接続後の最初のメッセージでなければならない。setupCompleteを待ってから送信を始める- 音声は 24kHz PCM16LE mono、48 バイト/ミリ秒。この前提が崩れると WAV 化も長さ換算も壊れる
- プロキシ対応は
HTTPS_PROXY+NODE_EXTRA_CA_CERTSの 2 点セット。コードに CA を埋め込まない - 計測用途を超えるなら SDK に戻る。直叩きの保守コストは計測の簡潔さと引き換え