ブログ一覧

SDK なしで Gemini Live API を WebSocket で直接叩く

更新: Web開発者向け

Gemini Live API の応答遅延を計測しようとして、SDK を挟むと「遅いのは API なのか SDK の処理なのか」の切り分けができないことに気づきました。もうひとつ、検証環境の一部が企業プロキシ+社内 CA という構成で、SDK がそのまま繋がる保証もありません。

そこで SDK を使わず、wshttps-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 の生 PCM
  • serverContent.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_PROXYNODE_EXTRA_CA_CERTS の 2 点セット。コードに CA を埋め込まない
  • 計測用途を超えるなら SDK に戻る。直叩きの保守コストは計測の簡潔さと引き換え

関連

この記事をシェア