Azure OpenAI Realtime API (GA) は「OpenAI互換」をどこまで信じていいか

— 404から401まで層を剥がして実測した互換性の境界


TL;DR(結論の表を最初に置く)

互換性 根拠
リクエスト/レスポンスのスキーマ ✅ 同一 client_secrets のボディ形式、イベントプロトコルとも本家と同型
session.model の意味 ⚠️ 名前空間が異なる 本家=グローバルモデル名 / Azure=デプロイメント名
ブラウザ直WSの認証(サブプロトコル方式) ❌ 401(実測) Azure公式のWS認証3方式(後述)に含まれず、記載もなし
エフェメラルキーのTTL ⚠️ 大幅に異なる 本家公称: デフォルト10分・上限2時間 / Azure実測: 約3時間(本家の上限すら超える)
  • 「互換」はAPIスキーマの層では成立するが、接続トランスポートと設定値の層では成立しない
  • ブラウザからAzureに直接続する経路として安全に選べるのはWebRTCが実質唯一。api-keyクエリパラメータも公式には存在するが、失効しない標準キーをブラウザに配ることになるため実運用では選べない。WS化するならサーバー中継構成が前提
  • デフォルトTTLに依存せず expires_after を明示指定すべき

1. 前提: 検証環境と構成

  • 検証の動機: OpenAI Realtime API(WebRTC/WS)で動いている既存実装を、Azure OpenAIのキーとエンドポイントに差し替えたら動くのか?という素朴な疑問
  • 環境:
  • Next.js + TypeScript
  • openai npmパッケージ(本家SDK)+ @openai/agents-realtime
  • Azure OpenAI リソース(GA v1エンドポイント)、gpt-realtime系モデルのデプロイメント
  • 用語の前提(1段落で):
  • Azureには「モデル」の上に「デプロイメント」という抽象があり、自分で任意の名前を付ける
  • GA (v1) APIとPreview APIでURL構造がまったく違う(本文で後述)

2. 実録: エラーコードで層を降りる

2.1 404 Resource not found — URLの世代が違う

  • 症状: SDKからの接続で404
  • 原因: AzureのRealtime APIはURL形式が2世代ある
    # Preview(旧)
    wss://{resource}.openai.azure.com/openai/realtime?api-version=2025-04-01-preview&deployment={name}
    # GA(現行)
    wss://{resource}.openai.azure.com/openai/v1/realtime?model={name}

    (Previewのapi-versionは検証当時 2024-10-01-preview でも同様。現行ドキュメントの例は 2025-04-01-preview)
  • 世代の混在(GAパスにPreviewパラメータ等)も404/400になる。これは公式ドキュメントにも明記がある:

    Using the wrong path or mixing GA/preview formats results in a 404 error.(Microsoft Learn、参考リンク[1])

  • 確定: パスをGA形式に統一 → 404は解消し400へ(=パス解決は通った)

2.2 400 Bad Request — model パラメータの欠落

  • GAパスでは ?model= が必須。deployment=api-version= はGAでは使わない
  • 確定: model を渡す → エラーが OperationNotSupported に変化(=スキーマ検証は通った)

2.3 OperationNotSupported — 「model」の意味が本家と違う

  • エラー: The realtime operation does not work with the specified model
  • 原因: session.model本家のモデル名 を渡していた
  • Azureでは model = Azureポータルの「モデル デプロイ」一覧の「名前」列(=デプロイメント名)
  • デフォルトのデプロイ名はモデル名と一致するため、偶然動く環境では罠が隠蔽される
  • 確定: デプロイメント名を渡す → client_secrets の発行に成功

2.4 寄り道: .env の引用符問題

  • MODEL="A"MODEL=A は、ローダーによって解釈が違う
  • dotenv / Next.js組み込み → 引用符を剥がす。docker –env-file / ECS environmentFiles → 引用符ごと値になる
  • 教訓: 環境変数起因を疑うときは console.log(JSON.stringify(process.env.X)) で可視化する

2.5 WS接続が無応答 → wscatで層を降りる

  • ブラウザ(agents-realtime経由)ではハンドシェイクに応答が返らない
  • SDKを剥がして生のWSクライアントで検証:
    bash
    npx wscat -c "wss://{resource}.openai.azure.com/openai/v1/realtime?model={deployment}" \
    -H "api-key: $AZURE_OPENAI_API_KEY"
  • 確定: api-keyヘッダー(サーバー間相当)では接続成功 → サーバー・パス・デプロイは正常。問題はブラウザ経路の認証に絞られた

2.6 401 Unauthorized — サブプロトコル方式の拒否

  • 本家のブラウザWS認証: サブプロトコル欄にトークンを載せる規約
    js
    new WebSocket(url, ["realtime", "openai-insecure-api-key." + key]);
  • これは標準の認証方式ではなく、Sec-WebSocket-Protocol欄への相乗り(OpenAIローカルの規約)である点を解説
  • 同方式をAzureに向けると401(実測)
  • Azure公式ドキュメント(参考リンク[1])がWS認証として挙げるのは次の3方式のみで、サブプロトコル方式の記載はない:
  • Microsoft Entra の Bearer トークン(Authorization ヘッダー)
  • api-key 接続ヘッダー — ただし「ブラウザ環境では利用不可」と明記
  • api-key クエリパラメータ — ブラウザからも技術的には使えるが、失効しない標準キーをクライアントに配ることになる
  • つまり「ブラウザから安全に直結する経路」としてはWebRTCしか残らない、が現時点の結論
  • 正直な限界を一つ: トークンを渡すトランスポート(ヘッダー/サブプロトコル/クエリ)ごとの総当たり対照実験まではしていない。本記事の根拠は「サブプロトコル方式での401(実測)」と「公式ドキュメントに同方式の記載がないこと」の2点である

3. おまけの実測: エフェメラルキーのTTLが本家公称と違う

  • 本家公称(現行GAの client_secrets リファレンス、参考リンク[3]): expires_after.secondsデフォルト600秒(10分)、指定可能範囲10〜7200秒(2時間)
  • 注意: ネット上に残る「エフェメラルトークンは1分で失効」という記述はbeta時代の /v1/realtime/sessions のもの。現行GAの client_secrets とは別物なので、古い記事を読むときは世代に注意
  • なお本家でも「公称と実際の expires_at が食い違う」というコミュニティ報告が過去にあり(参考リンク[4])、TTL周りの記述は本家も揺れてきた経緯がある
  • Azure実測(検証時): expires_at - 発行時刻約3時間
  • つまり本家仕様の上限7200秒すら超えるデフォルトであり、スキーマ互換でも「値の意味論」が違う例がもう一つ増えることになる
  • 検証方法: 素のcurlで expires_after 指定なしで発行し、jq でTTL秒を算出
    bash
    curl -s -X POST ".../openai/v1/realtime/client_secrets" \
    -H "api-key: $KEY" -H "Content-Type: application/json" \
    -d '{"session":{"type":"realtime","model":"{deployment}"}}' \
    | jq '{expires_at, ttl_sec: (.expires_at - (now|floor))}'
  • 補足: client secret の失効は「そのキーで新規接続を開始できる期限」であり、開始済みセッションの継続は失効後も切られない(本家リファレンスの定義)。漏洩リスクを見積もるときはこの意味で読む
  • セキュリティ含意: 「エフェメラル」の名で数時間生きるトークンをブラウザに配るのは漏洩窓が広すぎる
  • 対策は1行: "expires_after": {"anchor": "created_at", "seconds": 120} を明示指定(anchorは現状 created_at のみ)
  • 教訓: デフォルト値はベンダーの都合であって、セキュリティ要件ではない

4. ではどう設計するか

4.1 直結構成とリレー構成のトレードオフ

直結(ブラウザ → プロバイダ) リレー(ブラウザ → 自社サーバ → プロバイダ)
経路 WebRTC(WS直結は上記のとおりAzureでは実質不可) ブラウザ⇔自社サーバ間WS + サーバ間WS
レイテンシ 最小(本家推奨) 数十ms増 + サーバで音声を終端する実装コスト
認証 プロバイダ固有の規約(ek_発行フロー等)に密結合 プロバイダ依存区間がサーバー間WSに限定。ヘッダー認証で本家/Azureとも接続可(実測済み)。ek_の仕組み自体が不要
クライアント認証 プロバイダ規約に従う 自社基盤(Cognito等)に統一できる
可搬性 乗り換えが「設定変更」でなく「再設計」になる プロファイル差し替えで済む
運用 企業ネットワークのTLSインスペクション等と衝突しやすい 443のWSSでLB配下に収まり、監査証跡も取れる

4.2 プロバイダ可搬性を残したいなら

  • 接続URL・認証ヘッダー・model値を設定モジュールに集約し、プロバイダごとのプロファイルとして分離する
  • 「本家用のモデル名」をコードにハードコードしない(Azureではデプロイ名になるため)

5. まとめ: 互換レイヤーは主要経路しか保証しない

  • 互換を謳うサービスの互換性には勾配がある: よく使われる経路ほど互換性が高く、周縁の経路ほど検証されていない
  • 今回踏んだ「ブラウザ直WS」は本家ですらWebRTC推奨の周縁経路であり、勾配の一番薄い場所だった
  • チェックリスト(自分が使う経路は主要経路か?):
  • [ ] その構成、移行先の公式ドキュメントに載っているか
  • [ ] model 等の設定値の意味論は同じか(スキーマ互換≠値互換)
  • [ ] 認証トランスポートはSDK任せにできる層か
  • [ ] デフォルト値(TTL等)を実測したか

付録: 層を降りる道具箱

素手のクライアント 今回の用途
HTTP curl(--http1.1でUpgrade検証) エラーボディの回収、ハンドシェイクの生応答
WebSocket wscat / websocat SDKを剥がした接続検証、-H/-sでの認証方式の切り替え検証
値の可視化 JSON.stringify / jq .env引用符、TTL算出

参考リンク

  1. Use the GPT Realtime API via WebSockets — Microsoft Learn(GA/PreviewのURL形式、WS認証の3方式)
  2. Use the GPT Realtime API via WebRTC — Microsoft Learn(client_secrets エンドポイントとエフェメラルトークンのフロー)
  3. Create client secret — OpenAI API Reference(expires_after のデフォルト600秒・範囲10〜7200秒)
  4. Question About Ephemeral Key TTL in Realtime API — OpenAI Developer Community(本家でも公称と実測が食い違った報告)

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール