{
  "asyncapi": "2.6.0",
  "info": {
    "title": "Speko Router streaming API",
    "version": "1.0.0",
    "description": "WebSocket contract of the Speko Router: streaming speech-to-text and text-to-speech sessions, plus the five native speech-to-speech routes. On the STT and TTS routes JSON text frames carry the control messages and events documented here and binary frames carry raw audio in the format the session.configure frame declared, with no JSON envelope. On /v1/realtime, /v1/live, /v1/bidi, /v1/realtime/translations and /v1/realtime/translations/qwen the Router speaks the OpenAI/xAI Realtime, GPT-Live, Gemini Live, OpenAI realtime translation and Alibaba LiveTranslate protocols natively: audio travels base64-encoded inside the vendor's own JSON events, the Router forwards a bounded set of vendor commands and every vendor server event verbatim, and only its own error frame is Router-shaped. The Go types in package relayapi are the source of truth for every Router-defined shape in this document, and speccheck_test.go machine-checks the two against each other. The HTTP endpoints are specified separately in openapi.yaml."
  },
  "defaultContentType": "application/json",
  "servers": {
    "router": {
      "url": "router.speko.dev",
      "protocol": "wss",
      "description": "Global entry point. The upgrade request authenticates with a Speko-issued bearer token and must carry an Idempotency-Key header. The idempotency content hash of a WebSocket session is the SHA-256 of the exact session.configure text-frame bytes, so a retry must reproduce the configure frame byte-for-byte; later frames are intentionally outside the hash — they are the stream's continuous content, guarded by budgets and leases, while the configure frame is the session's intent.",
      "security": [
        {
          "bearerAuth": []
        }
      ]
    }
  },
  "channels": {
    "/v1/stt/stream": {
      "description": "Streaming speech-to-text. The first text frame MUST be session.configure; its exact frame bytes are the session's idempotency content hash. Audio input travels in binary frames in the format the configure frame declared, and the client must not send audio before session.ready arrives. Exactly one terminal frame — session.closed or error — ends every session.",
      "bindings": {
        "ws": {
          "method": "GET",
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The Router persists only the key, a content hash, and the session's execution status — never the content itself. The hash covers the exact session.configure text-frame bytes; later frames are intentionally outside the hash. Reusing a key with the same hash while the original admission is still live returns request_in_progress (retryable); after dispatch it returns request_already_started with the original request id; reusing a key with a different hash returns idempotency_conflict. Keys are opaque, and the normative length bound is 256 BYTES — maxLength below counts code points, which JSON Schema cannot avoid, so multi-byte keys hit the byte bound first."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendSTTFrames",
        "summary": "Frames the client sends.",
        "description": "JSON control frames, plus untyped binary frames carrying audio input.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/sttSessionConfigure"
            },
            {
              "$ref": "#/components/messages/sttInputCommit"
            },
            {
              "$ref": "#/components/messages/sttSessionClose"
            },
            {
              "$ref": "#/components/messages/sttAudioInput"
            }
          ]
        }
      },
      "subscribe": {
        "operationId": "receiveSTTFrames",
        "summary": "Frames the Router sends.",
        "description": "session.ready is always the first event frame; exactly one terminal frame — session.closed or error — ends the session.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/sttSessionReady"
            },
            {
              "$ref": "#/components/messages/sttTranscriptDelta"
            },
            {
              "$ref": "#/components/messages/sttTranscriptFinal"
            },
            {
              "$ref": "#/components/messages/sttUsageUpdated"
            },
            {
              "$ref": "#/components/messages/sttSessionClosed"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    },
    "/v1/tts/stream": {
      "description": "Streaming text-to-speech. The first text frame MUST be session.configure; its exact frame bytes are the session's idempotency content hash. The client must not append text before session.ready arrives. Synthesized audio travels in binary frames between the utterance.started and utterance.done events that share its sequence number. utterance.timings, when the route's model measures speech timings, carries time-aligned spans for that same sequence and arrives before its utterance.done. Exactly one terminal frame — session.closed or error — ends every session.",
      "bindings": {
        "ws": {
          "method": "GET",
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The Router persists only the key, a content hash, and the session's execution status — never the content itself. The hash covers the exact session.configure text-frame bytes; later frames are intentionally outside the hash. Reusing a key with the same hash while the original admission is still live returns request_in_progress (retryable); after dispatch it returns request_already_started with the original request id; reusing a key with a different hash returns idempotency_conflict. Keys are opaque, and the normative length bound is 256 BYTES — maxLength below counts code points, which JSON Schema cannot avoid, so multi-byte keys hit the byte bound first."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendTTSFrames",
        "summary": "Frames the client sends.",
        "description": "JSON control frames only; TTS clients never send binary frames.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/ttsSessionConfigure"
            },
            {
              "$ref": "#/components/messages/ttsInputAppend"
            },
            {
              "$ref": "#/components/messages/ttsInputCommit"
            },
            {
              "$ref": "#/components/messages/ttsInputCancel"
            },
            {
              "$ref": "#/components/messages/ttsSessionClose"
            }
          ]
        }
      },
      "subscribe": {
        "operationId": "receiveTTSFrames",
        "summary": "Frames the Router sends.",
        "description": "session.ready is always the first event frame; exactly one terminal frame — session.closed or error — ends the session.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/ttsSessionReady"
            },
            {
              "$ref": "#/components/messages/ttsUtteranceStarted"
            },
            {
              "$ref": "#/components/messages/ttsAudioOutput"
            },
            {
              "$ref": "#/components/messages/ttsUtteranceTimings"
            },
            {
              "$ref": "#/components/messages/ttsUtteranceDone"
            },
            {
              "$ref": "#/components/messages/ttsUsageUpdated"
            },
            {
              "$ref": "#/components/messages/ttsSessionClosed"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    },
    "/v1/realtime": {
      "description": "OpenAI or xAI Realtime speech-to-speech, spoken natively. The model is selected exactly with the `model` query parameter (for example, /v1/realtime?model=gpt-realtime-2.1 or a versioned grok-voice model), and its catalog row selects openai.realtime.v1 or xai.realtime.v1. The idempotency content hash is the SHA-256 of that model value. Once the session is admitted the Router relays that vendor's protocol: the client sends Realtime-shaped client events (session.update, input_audio_buffer.append with base64 PCM, input_audio_buffer.commit, conversation.item.create, response.create, response.cancel, ...) and receives every Realtime server event (session.created, session.updated, input_audio_buffer.speech_started, response.output_audio.delta, response.done, ...) verbatim. The Router pins the admitted model and mono PCM16 at 24 kHz: a session.update that names another model or audio format is refused with invalid_request, and GPT-Live-only commands are refused the same way. Audio is base64-encoded inside JSON text frames; binary frames are not accepted. The Router's own terminal error frame (lease_expired, budget_exhausted, provider_unavailable) uses the shared ErrorEvent shape; vendor `error` events pass through unchanged. xAI session.update keeps voice and turn_detection at the top level of session, as required by xAI's native protocol.",
      "parameters": {
        "model": {
          "description": "Exact Realtime model id from GET /v1/models rows whose endpoint is /v1/realtime.",
          "schema": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          }
        }
      },
      "bindings": {
        "ws": {
          "method": "GET",
          "query": {
            "type": "object",
            "properties": {
              "model": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              }
            },
            "required": [
              "model"
            ]
          },
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The content hash covers the exact `model` query value."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendRealtimeEvents",
        "summary": "OpenAI/xAI Realtime client events, forwarded to the model.",
        "message": {
          "$ref": "#/components/messages/nativeClientEvent"
        }
      },
      "subscribe": {
        "operationId": "receiveRealtimeEvents",
        "summary": "OpenAI/xAI Realtime server events, plus the Router error frame.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/nativeServerEvent"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    },
    "/v1/realtime/translations": {
      "description": "OpenAI realtime speech translation (gpt-realtime-translate), spoken natively on the Router's mirror of the vendor's dedicated /v1/realtime/translations socket. The model is selected exactly with the `model` query parameter (/v1/realtime/translations?model=gpt-realtime-translate) and its catalog row speaks openai.realtime.translation.v1; the idempotency content hash is the SHA-256 of that model value. This is not the conversational Realtime protocol: there is no voice, no instructions, no tools, no conversation items and no response.create. The client names the target language in a native session.update ({\"type\":\"session.update\",\"session\":{\"audio\":{\"output\":{\"language\":\"es\"}}}}); the only other settings accepted are session.audio.input.transcription (model gpt-realtime-whisper, or null) and session.audio.input.noise_reduction (type near_field or far_field, or null). Any other session field, and any target language outside es, pt, fr, ja, ru, zh, de, ko, hi, id, vi, it and en, is refused with invalid_request. Audio is mono PCM16 at 24 kHz in both directions, base64-encoded in session.input_audio_buffer.append events and streamed continuously, silence included. The client receives every vendor server event verbatim: session.created, session.output_audio.delta (translated audio), session.output_transcript.delta (target text), session.input_transcript.delta (source text) and session.closed; vendor `error` events pass through unchanged. Send session.close when the source ends and keep reading until session.closed: the vendor flushes pending input and emits the remaining translation first. The Router's own terminal error frame uses the shared ErrorEvent shape.",
      "parameters": {
        "model": {
          "description": "Exact translation model id from GET /v1/models rows whose endpoint is /v1/realtime/translations.",
          "schema": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          }
        }
      },
      "bindings": {
        "ws": {
          "method": "GET",
          "query": {
            "type": "object",
            "properties": {
              "model": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              }
            },
            "required": [
              "model"
            ]
          },
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The content hash covers the exact `model` query value."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendTranslationEvents",
        "summary": "OpenAI realtime translation client events, forwarded to the model.",
        "message": {
          "$ref": "#/components/messages/nativeClientEvent"
        }
      },
      "subscribe": {
        "operationId": "receiveTranslationEvents",
        "summary": "OpenAI realtime translation server events, plus the Router error frame.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/nativeServerEvent"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    },
    "/v1/realtime/translations/qwen": {
      "description": "Alibaba Qwen LiveTranslate speech-to-speech translation, spoken natively (alibaba.livetranslate.v1). The model is selected exactly with the `model` query parameter (/v1/realtime/translations/qwen?model=qwen3.8-livetranslate-flash-realtime) and the idempotency content hash is the SHA-256 of that model value. The session is not conversational: there is no assistant turn, no response.create and no instructions. After connecting, send the vendor's session.update with session.translation.language (the target language) and optionally output_modalities, voice, input_audio_transcription, translation.corpus and turn_detection. Then stream input_audio_buffer.append events (base64 mono PCM16 at 16 kHz). input_audio_buffer.commit and input_audio_buffer.clear are forwarded. Every other command, including input_image_buffer.append, is refused with invalid_request. The Router pins the audio formats (PCM16 in at 16 kHz, PCM16 out at 24 kHz), so a session.update that names another format or sample rate is refused. The client receives every vendor server event verbatim: session.created, session.updated, conversation.item.input_audio_transcription.text/.completed (the source), response.audio_transcript.delta and response.text.delta (the translation), response.audio.delta (the translated speech), and response.done with the vendor's token usage. Send session.finish to end the session and keep reading until session.finished: the Router forwards the finish upstream so the last segment is translated, then closes. Closing the socket without session.finish drops the final segment.",
      "parameters": {
        "model": {
          "description": "Exact LiveTranslate model id from GET /v1/models rows whose endpoint is /v1/realtime/translations/qwen.",
          "schema": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          }
        }
      },
      "bindings": {
        "ws": {
          "method": "GET",
          "query": {
            "type": "object",
            "properties": {
              "model": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              }
            },
            "required": [
              "model"
            ]
          },
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The content hash covers the exact `model` query value."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendQwenTranslationEvents",
        "summary": "LiveTranslate client events, forwarded to the model.",
        "message": {
          "$ref": "#/components/messages/nativeClientEvent"
        }
      },
      "subscribe": {
        "operationId": "receiveQwenTranslationEvents",
        "summary": "LiveTranslate server events, plus the Router error frame.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/nativeServerEvent"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    },
    "/v1/live": {
      "description": "GPT-Live speech-to-speech, spoken natively. The first text frame MUST be the vendor's session.start command naming the exact model (gpt-live-1); its exact frame bytes are the idempotency content hash and the Router validates it strictly before admission — unknown fields, audio formats other than mono PCM16 at 16 or 24 kHz, store=true, and a responses delegation without a backend model are refused. After session.started the Router relays the vendor protocol: the client streams session.input_audio.append (base64 PCM, continuous — GPT-Live decides when to speak, there is no commit), forwards the bounded command set (session.update, session.instructions.append, session.thinking.append, session.commentary.append, session.input_audio.mute/unmute, response.item.create, response.create) and receives every server event verbatim: session.started, session.output_audio.delta, session.input_transcript.delta and session.output_transcript.delta with their start_ms/end_ms session-timeline intervals preserved (they may overlap and are not turn boundaries), session.delegation.created, response.event envelopes carrying nested Responses events, the append acknowledgments, session.usage.updated cumulative snapshots, and the final session.closed with its usage. Realtime-only commands (input_audio_buffer.commit, response.cancel) are refused with invalid_request. Send session.close and keep reading until session.closed; the Router drains up to 15 seconds and otherwise ends the session with its own error frame, which means final vendor usage was not confirmed.",
      "bindings": {
        "ws": {
          "method": "GET",
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The content hash covers the exact session.start text-frame bytes."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendLiveEvents",
        "summary": "GPT-Live client events, forwarded to the model.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/liveSessionStart"
            },
            {
              "$ref": "#/components/messages/nativeClientEvent"
            }
          ]
        }
      },
      "subscribe": {
        "operationId": "receiveLiveEvents",
        "summary": "GPT-Live server events, plus the Router error frame.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/nativeServerEvent"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    },
    "/v1/bidi": {
      "description": "Gemini Live (BidiGenerateContent) speech-to-speech, spoken natively. The first text frame MUST be the vendor's setup message naming the exact model (gemini-3.8-live); its exact frame bytes are the idempotency content hash and the Router validates it strictly before admission — unknown fields are refused. Frames on this protocol are keyed by their single top-level field (setup, realtimeInput, clientContent, toolResponse) rather than by a type tag, which is the one way it differs from /v1/realtime and /v1/live. After setupComplete the Router relays the vendor protocol: the client streams realtimeInput audio (continuous — Gemini decides when to speak, there is no commit) and may forward clientContent and toolResponse; every other key is refused with invalid_request. The client receives every server message verbatim: setupComplete, serverContent with its modelTurn audio parts and input/output transcriptions, toolCall, usageMetadata, goAway, and the final close. The Router pins the admitted model and mono PCM16 — 16 kHz in, 24 kHz out — and its own terminal error frame (lease_expired, budget_exhausted, provider_unavailable) uses the shared ErrorEvent shape; vendor errors pass through unchanged.",
      "bindings": {
        "ws": {
          "method": "GET",
          "headers": {
            "type": "object",
            "properties": {
              "Idempotency-Key": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256,
                "description": "Required on the upgrade request. The content hash covers the exact setup text-frame bytes."
              }
            },
            "required": [
              "Idempotency-Key"
            ]
          }
        }
      },
      "publish": {
        "operationId": "sendBidiEvents",
        "summary": "Gemini Live client messages, forwarded to the model.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/bidiSetup"
            },
            {
              "$ref": "#/components/messages/bidiRealtimeInput"
            },
            {
              "$ref": "#/components/messages/bidiClientEvent"
            }
          ]
        }
      },
      "subscribe": {
        "operationId": "receiveBidiEvents",
        "summary": "Gemini Live server messages, plus the Router error frame.",
        "message": {
          "oneOf": [
            {
              "$ref": "#/components/messages/bidiServerEvent"
            },
            {
              "$ref": "#/components/messages/wsError"
            }
          ]
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Speko-issued API credential presented as a bearer token on the upgrade request. Failures normalize to the authentication_failed error code."
      }
    },
    "messages": {
      "sttSessionConfigure": {
        "name": "session.configure",
        "title": "STT session.configure",
        "summary": "Opens a streaming transcription; must be the first text frame on the socket.",
        "payload": {
          "$ref": "#/components/schemas/STTSessionConfigure"
        }
      },
      "sttInputCommit": {
        "name": "input.commit",
        "title": "STT input.commit",
        "summary": "Marks the end of the audio the client wants finalized.",
        "payload": {
          "$ref": "#/components/schemas/STTInputCommit"
        }
      },
      "sttSessionClose": {
        "name": "session.close",
        "title": "STT session.close",
        "summary": "Asks the server to finish outstanding transcripts and end the session with a terminal session.closed frame.",
        "payload": {
          "$ref": "#/components/schemas/STTSessionClose"
        }
      },
      "sttAudioInput": {
        "name": "audio",
        "title": "STT binary audio frame",
        "summary": "One binary WebSocket frame of raw audio input in the format the session.configure frame declared. Binary frames are untyped: they carry no JSON envelope.",
        "contentType": "application/octet-stream",
        "payload": {
          "type": "string",
          "format": "binary"
        }
      },
      "sttSessionReady": {
        "name": "session.ready",
        "title": "STT session.ready",
        "summary": "Confirms admission and reports the concrete route; always the first event frame.",
        "payload": {
          "$ref": "#/components/schemas/STTSessionReady"
        }
      },
      "sttTranscriptDelta": {
        "name": "transcript.delta",
        "title": "STT transcript.delta",
        "summary": "An interim hypothesis, superseded by later frames.",
        "payload": {
          "$ref": "#/components/schemas/STTTranscriptDelta"
        }
      },
      "sttTranscriptFinal": {
        "name": "transcript.final",
        "title": "STT transcript.final",
        "summary": "A finalized span of transcript.",
        "payload": {
          "$ref": "#/components/schemas/STTTranscriptFinal"
        }
      },
      "sttUsageUpdated": {
        "name": "usage.updated",
        "title": "STT usage.updated",
        "summary": "Cumulative usage while the stream is live.",
        "payload": {
          "$ref": "#/components/schemas/STTUsageUpdated"
        }
      },
      "sttSessionClosed": {
        "name": "session.closed",
        "title": "STT session.closed",
        "summary": "The clean terminal frame carrying final usage.",
        "payload": {
          "$ref": "#/components/schemas/STTSessionClosed"
        }
      },
      "ttsSessionConfigure": {
        "name": "session.configure",
        "title": "TTS session.configure",
        "summary": "Opens a streaming synthesis session; must be the first text frame on the socket.",
        "payload": {
          "$ref": "#/components/schemas/TTSSessionConfigure"
        }
      },
      "ttsInputAppend": {
        "name": "input.append",
        "title": "TTS input.append",
        "summary": "Adds text to the utterance being assembled.",
        "payload": {
          "$ref": "#/components/schemas/TTSInputAppend"
        }
      },
      "ttsInputCommit": {
        "name": "input.commit",
        "title": "TTS input.commit",
        "summary": "Finalizes the appended text as one utterance and starts synthesis.",
        "payload": {
          "$ref": "#/components/schemas/TTSInputCommit"
        }
      },
      "ttsInputCancel": {
        "name": "input.cancel",
        "title": "TTS input.cancel",
        "summary": "Abandons uncommitted appended text and any in-flight synthesis for the current utterance.",
        "payload": {
          "$ref": "#/components/schemas/TTSInputCancel"
        }
      },
      "ttsSessionClose": {
        "name": "session.close",
        "title": "TTS session.close",
        "summary": "Asks the server to finish outstanding synthesis and end the session with a terminal session.closed frame.",
        "payload": {
          "$ref": "#/components/schemas/TTSSessionClose"
        }
      },
      "ttsSessionReady": {
        "name": "session.ready",
        "title": "TTS session.ready",
        "summary": "Confirms admission and reports the concrete route; always the first event frame.",
        "payload": {
          "$ref": "#/components/schemas/TTSSessionReady"
        }
      },
      "ttsUtteranceStarted": {
        "name": "utterance.started",
        "title": "TTS utterance.started",
        "summary": "Announces the binary audio frames for one committed utterance.",
        "payload": {
          "$ref": "#/components/schemas/TTSUtteranceStarted"
        }
      },
      "ttsAudioOutput": {
        "name": "audio",
        "title": "TTS binary audio frame",
        "summary": "One binary WebSocket frame of synthesized audio in the requested output format, sent between the utterance.started and utterance.done events that share its sequence number. Binary frames are untyped: they carry no JSON envelope.",
        "contentType": "application/octet-stream",
        "payload": {
          "type": "string",
          "format": "binary"
        }
      },
      "ttsUtteranceTimings": {
        "name": "utterance.timings",
        "title": "TTS utterance.timings",
        "summary": "Time-aligned spans for the utterance sharing its sequence number, sent only when the route's model measures them.",
        "payload": {
          "$ref": "#/components/schemas/TTSUtteranceTimings"
        }
      },
      "ttsUtteranceDone": {
        "name": "utterance.done",
        "title": "TTS utterance.done",
        "summary": "Marks the end of one utterance's binary audio frames.",
        "payload": {
          "$ref": "#/components/schemas/TTSUtteranceDone"
        }
      },
      "ttsUsageUpdated": {
        "name": "usage.updated",
        "title": "TTS usage.updated",
        "summary": "Cumulative usage while the stream is live.",
        "payload": {
          "$ref": "#/components/schemas/TTSUsageUpdated"
        }
      },
      "ttsSessionClosed": {
        "name": "session.closed",
        "title": "TTS session.closed",
        "summary": "The clean terminal frame carrying final usage.",
        "payload": {
          "$ref": "#/components/schemas/TTSSessionClosed"
        }
      },
      "liveSessionStart": {
        "name": "session.start",
        "title": "GPT-Live session.start",
        "summary": "Opens a GPT-Live session; must be the first text frame on the socket and names the exact model.",
        "payload": {
          "$ref": "#/components/schemas/LiveSessionStart"
        }
      },
      "bidiSetup": {
        "name": "setup",
        "title": "Gemini Live setup",
        "summary": "Opens a Gemini Live session; must be the first text frame on the socket and names the exact model.",
        "payload": {
          "$ref": "#/components/schemas/BidiSetup"
        }
      },
      "bidiRealtimeInput": {
        "name": "realtimeInput",
        "title": "Gemini Live client audio",
        "summary": "Base64 PCM16 audio at 16 kHz, or the end-of-audio marker. This is the media path, not a forwardable control.",
        "payload": {
          "$ref": "#/components/schemas/BidiAudioChunk"
        }
      },
      "nativeClientEvent": {
        "name": "native client event",
        "title": "Vendor client event",
        "summary": "One vendor client event forwarded verbatim within the route's bounded command set. Its shape is the vendor's; the Router reads only the type tag.",
        "payload": {
          "$ref": "#/components/schemas/NativeEvent"
        }
      },
      "nativeServerEvent": {
        "name": "native server event",
        "title": "Vendor server event",
        "summary": "One vendor server event delivered verbatim, native identifiers included.",
        "payload": {
          "$ref": "#/components/schemas/NativeEvent"
        }
      },
      "bidiClientEvent": {
        "name": "bidi client event",
        "title": "Gemini Live client message",
        "summary": "One Gemini Live client message forwarded verbatim within the route's bounded key set (clientContent, toolResponse). Its shape is the vendor's; the Router reads only the single top-level key, since this protocol carries no type tag.",
        "payload": {
          "$ref": "#/components/schemas/BidiClientEvent"
        }
      },
      "bidiServerEvent": {
        "name": "bidi server event",
        "title": "Gemini Live server message",
        "summary": "One Gemini Live server message delivered verbatim. It is keyed by its union member (setupComplete, serverContent, toolCall, toolCallCancellation, goAway, sessionResumptionUpdate) and may carry a usageMetadata sibling beside it.",
        "payload": {
          "$ref": "#/components/schemas/BidiServerEvent"
        }
      },
      "wsError": {
        "name": "error",
        "title": "Terminal error frame",
        "summary": "The terminal error frame, shared by STT and TTS sessions. The type name matches the SSE error event deliberately: one error vocabulary across every streaming transport.",
        "payload": {
          "$ref": "#/components/schemas/ErrorEvent"
        }
      }
    },
    "schemas": {
      "RoutingMode": {
        "type": "string",
        "description": "Discriminates the routing tagged union. The set is closed: an empty or unknown mode is rejected so a typo can never silently fall back to automatic provider selection.",
        "enum": [
          "auto",
          "explicit"
        ]
      },
      "RoutingObjective": {
        "type": "string",
        "description": "Ranks auto-mode candidates. Unknown objectives are rejected rather than treated as balanced so callers learn about typos before any money is reserved.",
        "enum": [
          "balanced",
          "quality",
          "latency",
          "cost"
        ]
      },
      "Routing": {
        "type": "object",
        "description": "Selects how the Router picks a provider for one session. It is a tagged union over mode: auto carries the objective and optional allow/deny provider filters, explicit carries exactly a provider and model. Fields from the other arm are rejected in both directions; a present-but-empty filter array ([]) counts as present and is rejected in explicit mode, while an empty-string provider or model in auto mode is indistinguishable from omission and tolerated as such. A wholly omitted routing object means {mode: auto, objective: balanced}, and an auto routing without an objective defaults to balanced — the default exists for omission, never for partially specified routing.\nExplicit mode also accepts the combined \"provider/model\" spelling in the model field: {\"model\": \"openai/gpt-5.2\"} with no provider splits at the first slash, and a model redundantly prefixed with the stated provider has the prefix stripped. Upstream model ids that themselves contain a slash must therefore name the provider one way or the other, e.g. \"together/meta-llama/Llama-X\" or provider \"together\" with model \"meta-llama/Llama-X\".",
        "properties": {
          "mode": {
            "$ref": "#/components/schemas/RoutingMode"
          },
          "objective": {
            "$ref": "#/components/schemas/RoutingObjective"
          },
          "allow_providers": {
            "type": "array",
            "description": "Auto mode only. Provider ids must not be blank.",
            "items": {
              "type": "string"
            }
          },
          "deny_providers": {
            "type": "array",
            "description": "Auto mode only. Provider ids must not be blank.",
            "items": {
              "type": "string"
            }
          },
          "provider": {
            "type": "string",
            "description": "Explicit mode only, and required there."
          },
          "model": {
            "type": "string",
            "description": "Explicit mode only, and required there. Accepts either the bare model id with the provider field set, or the combined \"provider/model\" form with the provider field omitted."
          }
        },
        "additionalProperties": false
      },
      "Route": {
        "type": "object",
        "description": "The response-side counterpart of Routing: the concrete decision that served a session. region is the Speko Router location, not a provider-processing residency guarantee, and attempt_id identifies the (possibly post-fallback) attempt that produced the output.",
        "properties": {
          "provider": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "attempt_id": {
            "type": "string"
          }
        },
        "required": [
          "provider",
          "model",
          "region",
          "attempt_id"
        ],
        "additionalProperties": false
      },
      "AudioConfig": {
        "type": "object",
        "description": "Raw audio on a Router stream: the binary input frames of an STT session, or the synthesized output frames of a TTS session. The bounds match the local gateway's portable media contract so a caller can move between provider-direct and Router routes without re-encoding.",
        "properties": {
          "encoding": {
            "type": "string",
            "enum": [
              "pcm_s16le",
              "opus"
            ]
          },
          "sample_rate_hz": {
            "type": "integer",
            "minimum": 8000,
            "maximum": 192000
          },
          "channels": {
            "type": "integer",
            "minimum": 1,
            "maximum": 8
          }
        },
        "required": [
          "encoding",
          "sample_rate_hz",
          "channels"
        ],
        "additionalProperties": false
      },
      "Usage": {
        "type": "object",
        "description": "Normalized, provider-neutral quantities for one session. Only the lines relevant to the session's kind are set; zero lines are omitted on the wire. Split lines are mutually exclusive by contract: a token counted in cached_input_tokens is not repeated in input_tokens, and a reasoning token is not repeated in output_tokens, so the splits always sum to the totals.",
        "properties": {
          "duration_ms": {
            "type": "integer",
            "minimum": 0
          },
          "characters": {
            "type": "integer",
            "minimum": 0
          },
          "input_tokens": {
            "type": "integer",
            "minimum": 0
          },
          "cached_input_tokens": {
            "type": "integer",
            "minimum": 0
          },
          "cache_write_5m_tokens": {
            "type": "integer",
            "minimum": 0,
            "description": "Disjoint cache-write input tokens for 5-minute retention."
          },
          "cache_write_1h_tokens": {
            "type": "integer",
            "minimum": 0,
            "description": "Disjoint cache-write input tokens for 1-hour retention."
          },
          "incomplete": {
            "type": "boolean",
            "description": "Terminal billing quantities are missing or inconsistent; do not interpret missing evidence as zero usage."
          },
          "output_tokens": {
            "type": "integer",
            "minimum": 0
          },
          "reasoning_tokens": {
            "type": "integer",
            "minimum": 0
          },
          "tool_calls": {
            "type": "integer",
            "minimum": 0,
            "description": "Billable hosted tool invocations (web search) performed by a delegated GPT-Live backend; omitted for every other route."
          }
        },
        "additionalProperties": false
      },
      "TranscriptSegment": {
        "type": "object",
        "description": "One time-aligned span of the transcript.",
        "properties": {
          "text": {
            "type": "string"
          },
          "start_ms": {
            "type": "integer",
            "minimum": 0
          },
          "end_ms": {
            "type": "integer",
            "minimum": 0,
            "description": "Must not precede start_ms."
          },
          "speaker": {
            "type": "string",
            "description": "Whoever said the span, present only when the request asked for diarization and the provider attributed it. The VENDOR's own label carried verbatim (\"A\" from one provider, \"1\" from another): renumbering across providers would invent an identity mapping no vendor promised. Absent means unattributed, which is not the same as a new speaker."
          }
        },
        "required": [
          "text",
          "start_ms",
          "end_ms"
        ],
        "additionalProperties": false
      },
      "ErrorCode": {
        "type": "string",
        "description": "Stable, machine-readable failure class. The set is closed: the Router normalizes every failure — including provider failures — to one of these codes and never forwards a raw provider response body. On an established WebSocket session the code arrives in a terminal error frame and no HTTP status applies; when the same code is returned over HTTP — including a rejected upgrade — it maps to one canonical status: 400 capability_unsupported and invalid_request; 401 authentication_failed; 402 insufficient_credit; 404 route_not_found; 405 method_not_allowed; 409 idempotency_conflict, request_in_progress, and request_already_started; 413 payload_too_large; 415 unsupported_media; 429 rate_limited and concurrency_exhausted; 500 relay_error; 502 provider_error; 503 provider_unavailable; 504 request_timeout.",
        "enum": [
          "authentication_failed",
          "insufficient_credit",
          "capability_unsupported",
          "invalid_request",
          "route_not_found",
          "method_not_allowed",
          "rate_limited",
          "concurrency_exhausted",
          "provider_error",
          "provider_unavailable",
          "relay_error",
          "idempotency_conflict",
          "request_in_progress",
          "request_already_started",
          "budget_exhausted",
          "lease_expired",
          "payload_too_large",
          "unsupported_media",
          "request_timeout"
        ]
      },
      "ErrorBody": {
        "type": "object",
        "description": "The normalized failure detail. message is human-readable and intentionally carries no provider payload; request_id is present whenever a Router request id had been minted before the failure.",
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string"
          },
          "hint": {
            "type": "string",
            "minLength": 1,
            "maxLength": 512,
            "pattern": "^[^\\r\\n]+$",
            "description": "Router-authored, single-line remediation that never contains provider payloads or customer content."
          },
          "retryable": {
            "type": "boolean"
          },
          "request_id": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "message",
          "hint",
          "retryable"
        ],
        "additionalProperties": false
      },
      "ErrorEvent": {
        "type": "object",
        "description": "The terminal error frame on a WebSocket session. Exactly one terminal frame — session.closed or error — ends every session.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        },
        "required": [
          "type",
          "error"
        ],
        "additionalProperties": false
      },
      "STTSessionConfigure": {
        "type": "object",
        "description": "Opens a streaming transcription. audio describes the raw binary frames the client will send: streamed audio has no container, so the format must be declared up front. The exact frame bytes are the session's idempotency content hash; later frames are intentionally outside the hash.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.configure"
            ]
          },
          "routing": {
            "$ref": "#/components/schemas/Routing",
            "description": "Omitted entirely, routing means {mode: auto, objective: balanced}."
          },
          "audio": {
            "$ref": "#/components/schemas/AudioConfig"
          },
          "language": {
            "type": "string",
            "description": "Optional language hint."
          },
          "options": {
            "$ref": "#/components/schemas/STTOptions",
            "description": "Transcription feature asks and per-provider settings for the whole session. They ride this frame — and therefore the session's idempotency hash — because they are session intent, not stream content: later frames cannot change them."
          }
        },
        "required": [
          "type",
          "audio"
        ],
        "additionalProperties": false
      },
      "STTTranslation": {
        "type": "object",
        "description": "Translate the spoken audio into target_language and return the translated words BESIDE the transcript: text stays the original words and the translation rides a separate translation field, so a caller that never asks reads exactly what it read before. Fails closed like the other canonical asks: a route whose model does not advertise capabilities.translation is refused capability_unsupported naming translation. One way only; the source is whatever is spoken.",
        "properties": {
          "target_language": {
            "type": "string",
            "description": "A BCP-47 language tag such as \"es\" or \"pt-BR\": a 2-3 letter primary subtag, then 1-8 character alphanumeric subtags, at most 35 characters. A shape check only; a language the routed vendor does not translate into is the vendor's error.",
            "minLength": 2,
            "maxLength": 35
          }
        },
        "required": [
          "target_language"
        ],
        "additionalProperties": false
      },
      "STTOptions": {
        "type": "object",
        "description": "What a caller asked a transcription to do beyond returning text. The same object rides POST /v1/stt/transcriptions and the STT session.configure frame, in the same vocabulary the local gateway uses. The canonical asks (diarization, keywords, noise_reduction, word_timestamps, translation) fail closed: a session whose routed model does not advertise the matching capability in GET /v1/models is rejected with capability_unsupported, never answered with the feature quietly missing. Names arrive lower case and are rejected otherwise rather than folded, because the idempotency content hash covers the configure frame bytes exactly as sent and one intent must have one spelling.",
        "properties": {
          "diarization": {
            "type": "boolean",
            "description": "Label each transcript segment with the speaker who said it. Omitted leaves the vendor default, which is not the same as false."
          },
          "keywords": {
            "type": "array",
            "description": "Vocabulary-biasing terms (names, products, jargon), translated to each vendor's own spelling. At most 100 terms of at most 64 characters each; both bounds count CHARACTERS, which JSON Schema cannot express, so they are normative here in prose and enforced by the Router.",
            "items": {
              "type": "string"
            }
          },
          "noise_reduction": {
            "type": "boolean",
            "description": "Ask the provider to clean the audio before transcribing."
          },
          "word_timestamps": {
            "type": "boolean",
            "description": "Return per-word timings on the batch transcription result. No streaming route advertises it, so on session.configure it is refused capability_unsupported."
          },
          "translation": {
            "$ref": "#/components/schemas/STTTranslation"
          },
          "provider_options": {
            "type": "object",
            "description": "The vendor's OWN settings, namespaced by provider name, e.g. {\"deepgram\": {\"numerals\": true}}. Values are scalars only (boolean, number, or string of at most 256 characters). At most 8 providers, each with at most 16 settings. Settings addressed to a provider the session does not reach are ignored by design, so they never narrow routing. The Router forwards only the keys the routed provider allow-lists and refuses the rest by name; keys it owns itself (model, language, encoding, sample_rate, credentials, and the canonical asks' own vendor spellings) are always refused.",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "string"
                  }
                ]
              }
            }
          }
        },
        "additionalProperties": false
      },
      "STTInputCommit": {
        "type": "object",
        "description": "Marks the end of the audio the client wants finalized.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "input.commit"
            ]
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": false
      },
      "STTSessionClose": {
        "type": "object",
        "description": "Asks the server to finish outstanding transcripts and end the session with a terminal session.closed frame.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.close"
            ]
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": false
      },
      "STTSessionReady": {
        "type": "object",
        "description": "Confirms admission and reports the concrete route. It is always the first event frame; the client must not send audio before it.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.ready"
            ]
          },
          "request_id": {
            "type": "string"
          },
          "route": {
            "$ref": "#/components/schemas/Route"
          }
        },
        "required": [
          "type",
          "request_id",
          "route"
        ],
        "additionalProperties": false
      },
      "STTTranscriptDelta": {
        "type": "object",
        "description": "An interim hypothesis; later deltas and the next transcript.final supersede it. Deltas are never empty — empty interim hypotheses are dropped at normalization, never forwarded. On a translating session a delta may advance only the translation, with empty text.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "transcript.delta"
            ]
          },
          "text": {
            "type": "string"
          },
          "translation": {
            "type": "string",
            "description": "The interim translation of the current span, present only on a session that asked for one. It trails text, so a delta may carry only one of the two; a delta with neither is never sent."
          }
        },
        "required": [
          "type",
          "text"
        ],
        "additionalProperties": false
      },
      "STTTranscriptFinal": {
        "type": "object",
        "description": "A finalized span of transcript with optional time-aligned segments. text may be empty: a finalized span of silence is legitimate.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "transcript.final"
            ]
          },
          "text": {
            "type": "string"
          },
          "segments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TranscriptSegment"
            }
          },
          "speaker": {
            "type": "string",
            "description": "Whoever said the whole finalized span, present only when the session asked for diarization and the provider attributed the turn. Most providers report one speaker per finalized turn rather than per span, so this is where a diarized session usually reads its labels; per-span attribution rides segments[].speaker instead. The vendor's own label, verbatim."
          },
          "translation": {
            "type": "string",
            "description": "The finalized translation delivered with this span, present only on a session that asked for one; text stays the original words. The vendor translates after it transcribes, so the translation of a span's last words can arrive on the NEXT transcript.final, and a final may carry a translation with empty text. Concatenate finals in order rather than pairing them one to one."
          }
        },
        "required": [
          "type",
          "text"
        ],
        "additionalProperties": false
      },
      "STTUsageUpdated": {
        "type": "object",
        "description": "Cumulative usage while the stream is live, so callers can watch spend before the terminal frame.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "usage.updated"
            ]
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        },
        "required": [
          "type",
          "usage"
        ],
        "additionalProperties": false
      },
      "STTSessionClosed": {
        "type": "object",
        "description": "The clean terminal frame carrying final usage.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.closed"
            ]
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        },
        "required": [
          "type",
          "usage"
        ],
        "additionalProperties": false
      },
      "TTSSessionConfigure": {
        "type": "object",
        "description": "Opens a streaming synthesis session. audio is the requested output format of the binary frames the server will send. The exact frame bytes are the session's idempotency content hash; later frames are intentionally outside the hash.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.configure"
            ]
          },
          "routing": {
            "$ref": "#/components/schemas/Routing",
            "description": "Omitted entirely, routing means {mode: auto, objective: balanced}."
          },
          "voice": {
            "type": "string",
            "description": "Optional: in auto mode the party that picks the provider picks a default voice for it."
          },
          "language": {
            "type": "string",
            "description": "Optional language hint. Auto routing ranks candidates on that language's benchmark board and the Router picks a voice curated for it; omitted means English."
          },
          "audio": {
            "$ref": "#/components/schemas/AudioConfig"
          }
        },
        "required": [
          "type",
          "audio"
        ],
        "additionalProperties": false
      },
      "TTSInputAppend": {
        "type": "object",
        "description": "Adds text to the utterance being assembled. Characters are counted against the reserved budget when appended, so an over-budget append fails before any provider work happens.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "input.append"
            ]
          },
          "text": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "text"
        ],
        "additionalProperties": false
      },
      "TTSInputCommit": {
        "type": "object",
        "description": "Finalizes the appended text as one utterance and starts synthesis.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "input.commit"
            ]
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": false
      },
      "TTSInputCancel": {
        "type": "object",
        "description": "Abandons uncommitted appended text and any in-flight synthesis for the current utterance.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "input.cancel"
            ]
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": false
      },
      "TTSSessionClose": {
        "type": "object",
        "description": "Asks the server to finish outstanding synthesis and end the session with a terminal session.closed frame.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.close"
            ]
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": false
      },
      "TTSSessionReady": {
        "type": "object",
        "description": "Confirms admission and reports the concrete route. It is always the first event frame; the client must not append text before it.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.ready"
            ]
          },
          "request_id": {
            "type": "string"
          },
          "route": {
            "$ref": "#/components/schemas/Route"
          }
        },
        "required": [
          "type",
          "request_id",
          "route"
        ],
        "additionalProperties": false
      },
      "TTSUtteranceStarted": {
        "type": "object",
        "description": "Announces the binary audio frames for one committed utterance. sequence is the 1-based utterance index within the session so clients can correlate audio with commits.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "utterance.started"
            ]
          },
          "sequence": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "type",
          "sequence"
        ],
        "additionalProperties": false
      },
      "TimingGranularity": {
        "type": "string",
        "description": "The unit a TimingSpan measures. Synthesis always reports word, even where the engine measured characters: the relay groups a character-level reading into words on the way out, so one client does not have to write that fold once per provider. character remains in the set for readings that are passed through ungrouped. The set is closed.",
        "enum": [
          "word",
          "character"
        ]
      },
      "TimingSpan": {
        "type": "object",
        "description": "One time-aligned span of synthesized speech, measured from the start of the utterance. end_ms is an end time, never a duration. Spans describe the caller's OWN text: a provider that also reports timings over its normalized reading (\"five dollars\" for \"$5\") has that reading dropped rather than carried, because a client walking its own string against normalized spans desynchronizes silently.",
        "properties": {
          "text": {
            "type": "string"
          },
          "start_ms": {
            "type": "integer",
            "minimum": 0
          },
          "end_ms": {
            "type": "integer",
            "minimum": 0,
            "description": "Must not precede start_ms."
          }
        },
        "required": [
          "text",
          "start_ms",
          "end_ms"
        ],
        "additionalProperties": false
      },
      "TTSUtteranceTimings": {
        "type": "object",
        "description": "Time-aligned spans for the utterance sharing its sequence number, emitted only when the route's model measures timings; a model that advertises neither word_timings nor character_timings never sends it. One utterance may produce SEVERAL of these events, all carrying the same sequence: a long render's spans are flushed as they accumulate rather than held for one frame that could exceed the stream's frame ceiling. The client concatenates them in arrival order. Every event for an utterance arrives before the utterance.done that closes it. Spans are individually well-formed but are NOT guaranteed to be mutually ordered: engines legitimately return adjacent spans that overlap, where one sound is still being articulated as the next begins.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "utterance.timings"
            ]
          },
          "sequence": {
            "type": "integer",
            "minimum": 1
          },
          "granularity": {
            "$ref": "#/components/schemas/TimingGranularity"
          },
          "spans": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/TimingSpan"
            }
          }
        },
        "required": [
          "type",
          "sequence",
          "granularity",
          "spans"
        ],
        "additionalProperties": false
      },
      "TTSUtteranceDone": {
        "type": "object",
        "description": "Marks the end of one utterance's binary audio frames.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "utterance.done"
            ]
          },
          "sequence": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "type",
          "sequence"
        ],
        "additionalProperties": false
      },
      "TTSUsageUpdated": {
        "type": "object",
        "description": "Cumulative usage while the stream is live, so callers can watch spend before the terminal frame.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "usage.updated"
            ]
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        },
        "required": [
          "type",
          "usage"
        ],
        "additionalProperties": false
      },
      "TTSSessionClosed": {
        "type": "object",
        "description": "The clean terminal frame carrying final usage.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.closed"
            ]
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        },
        "required": [
          "type",
          "usage"
        ],
        "additionalProperties": false
      },
      "NativeEvent": {
        "type": "object",
        "description": "A vendor-native JSON event. The Router reads only `type`; every other property is the vendor's and passes through unchanged.",
        "properties": {
          "type": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": true
      },
      "BidiClientEvent": {
        "type": "object",
        "description": "A forwardable Gemini Live client message. There is no type tag: the single top-level key IS the message type, so exactly one property is present and its value is the vendor's own document, forwarded unchanged. Only the two forwardable keys are admitted here; setup and realtimeInput have their own messages on the channel, so every client frame matches exactly one published alternative.",
        "properties": {
          "clientContent": {
            "type": "object",
            "additionalProperties": true
          },
          "toolResponse": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "minProperties": 1,
        "maxProperties": 1,
        "additionalProperties": false
      },
      "BidiServerEvent": {
        "type": "object",
        "description": "A Gemini Live server message, delivered byte-for-byte. Keyed by its union member (setupComplete, serverContent, toolCall, toolCallCancellation, goAway, sessionResumptionUpdate); the vendor may attach a usageMetadata sibling to the same message. The frame is the vendor's, so no upper property bound is published — a member Google adds tomorrow is relayed verbatim, not refused by this contract.",
        "minProperties": 1,
        "additionalProperties": true
      },
      "BidiSetup": {
        "type": "object",
        "description": "The first frame of a /v1/bidi session: the vendor's own setup message, validated strictly by the Router. Gemini Live frames are keyed by their single top-level field rather than a type tag.",
        "properties": {
          "setup": {
            "$ref": "#/components/schemas/BidiSetupConfig"
          }
        },
        "required": [
          "setup"
        ],
        "additionalProperties": false
      },
      "BidiSetupConfig": {
        "type": "object",
        "description": "The setup object of a /v1/bidi session. model is the exact Gemini Live model id, with or without the vendor's \"models/\" prefix. Every other field is a vendor document the Router bounds but never rewrites.",
        "properties": {
          "model": {
            "type": "string",
            "minLength": 1,
            "maxLength": 136
          },
          "generationConfig": {
            "type": "object",
            "description": "Everything the vendor files under generationConfig, forwarded verbatim: responseModalities, speechConfig, thinkingConfig, enableAffectiveDialog, mediaResolution, translationConfig and the sampling scalars. The SDK's flat thinking_config / enable_affective_dialog / media_resolution fields land here on the wire, not beside it."
          },
          "systemInstruction": {
            "type": "object",
            "description": "Instruction parts steering the live model."
          },
          "tools": {
            "type": "array",
            "maxItems": 64,
            "items": {
              "type": "object",
              "description": "One vendor tool declaration, forwarded verbatim."
            }
          },
          "sessionResumption": {
            "type": "object",
            "description": "Vendor session-resumption settings."
          },
          "contextWindowCompression": {
            "type": "object",
            "description": "Vendor context-window compression settings."
          },
          "inputAudioTranscription": {
            "type": "object",
            "description": "Enables the vendor's transcript of the caller; may carry customVocabulary and adaptationPhrases lists."
          },
          "outputAudioTranscription": {
            "type": "object",
            "description": "Enables the vendor's transcript of the model; may carry customVocabulary and adaptationPhrases lists."
          },
          "realtimeInputConfig": {
            "type": "object",
            "description": "Vendor-side VAD and turn-handling settings."
          },
          "proactivity": {
            "type": "object",
            "description": "Vendor proactive-audio setting (proactiveAudio)."
          },
          "historyConfig": {
            "type": "object",
            "description": "Vendor history-exchange setting (initialHistoryInClientContent)."
          },
          "safetySettings": {
            "type": "array",
            "description": "Vendor safety settings, forwarded verbatim. Absent from the Live reference page but on the wire schema and emitted by the vendor SDK.",
            "items": {
              "type": "object"
            }
          },
          "labels": {
            "type": "object",
            "description": "Vendor request labels (for example safety_identifier), forwarded verbatim."
          }
        },
        "required": [
          "model"
        ],
        "additionalProperties": false
      },
      "BidiAudioChunk": {
        "type": "object",
        "description": "Client audio for a /v1/bidi session. Both the current `audio` field and the older `mediaChunks` array are accepted because the vendor's own SDKs still emit the latter. `audioStreamEnd` marks the end of input audio.",
        "properties": {
          "realtimeInput": {
            "type": "object",
            "properties": {
              "audio": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "string",
                    "contentEncoding": "base64"
                  },
                  "mimeType": {
                    "type": "string"
                  }
                },
                "required": [
                  "data"
                ]
              },
              "mediaChunks": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "string",
                      "contentEncoding": "base64"
                    },
                    "mimeType": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "audioStreamEnd": {
                "type": "boolean"
              }
            }
          }
        },
        "required": [
          "realtimeInput"
        ]
      },
      "LiveSessionStart": {
        "type": "object",
        "description": "The first frame of a /v1/live session: the native GPT-Live session.start command, validated strictly by the Router.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session.start"
            ]
          },
          "event_id": {
            "type": "string",
            "maxLength": 256
          },
          "session": {
            "$ref": "#/components/schemas/LiveSessionConfig"
          }
        },
        "required": [
          "type",
          "session"
        ],
        "additionalProperties": false
      },
      "LiveSessionConfig": {
        "type": "object",
        "description": "The session object of session.start. model is the exact GPT-Live model id. instructions steer the live voice model and are never merged with delegation.responses.instructions. store must be omitted or false.",
        "properties": {
          "model": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "instructions": {
            "type": "string"
          },
          "audio": {
            "$ref": "#/components/schemas/LiveAudioConfig"
          },
          "input": {
            "type": "array",
            "maxItems": 256,
            "items": {
              "$ref": "#/components/schemas/LiveInputItem"
            }
          },
          "delegation": {
            "$ref": "#/components/schemas/LiveDelegationConfig"
          },
          "store": {
            "type": "boolean",
            "enum": [
              false
            ]
          }
        },
        "required": [
          "model"
        ],
        "additionalProperties": false
      },
      "LiveAudioConfig": {
        "type": "object",
        "description": "One PCM16 format for both directions, and the output voice.",
        "properties": {
          "format": {
            "$ref": "#/components/schemas/LiveAudioFormat"
          },
          "output": {
            "$ref": "#/components/schemas/LiveAudioOutput"
          }
        },
        "additionalProperties": false
      },
      "LiveAudioFormat": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "audio/pcm"
            ]
          },
          "rate": {
            "type": "integer",
            "enum": [
              16000,
              24000
            ]
          }
        },
        "required": [
          "type",
          "rate"
        ],
        "additionalProperties": false
      },
      "LiveAudioOutput": {
        "type": "object",
        "properties": {
          "voice": {
            "type": "string",
            "maxLength": 128,
            "description": "The output voice; marin when omitted."
          }
        },
        "additionalProperties": false
      },
      "LiveInputItem": {
        "type": "object",
        "description": "One seeded prior text message.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "message"
            ]
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant",
              "system",
              "developer"
            ]
          },
          "content": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/LiveContentPart"
            }
          }
        },
        "required": [
          "role",
          "content"
        ],
        "additionalProperties": false
      },
      "LiveContentPart": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "input_text",
              "output_text"
            ]
          },
          "text": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "type",
          "text"
        ],
        "additionalProperties": false
      },
      "LiveDelegationConfig": {
        "type": "object",
        "description": "client hands delegations to the application; responses lets GPT-Live call the configured Responses backend, whose model is required.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "client",
              "responses"
            ]
          },
          "responses": {
            "$ref": "#/components/schemas/LiveResponsesConfig"
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": false
      },
      "LiveResponsesConfig": {
        "type": "object",
        "description": "The Responses backend GPT-Live delegates to. tools are Responses function definitions or web_search entries kept verbatim; tool_choice, reasoning, and text are forwarded verbatim within their size bounds. Managed sessions additionally require max_output_tokens and a priced backend model.",
        "properties": {
          "model": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "instructions": {
            "type": "string"
          },
          "tools": {
            "type": "array",
            "maxItems": 64,
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "function",
                    "web_search"
                  ]
                }
              },
              "required": [
                "type"
              ],
              "additionalProperties": true
            }
          },
          "tool_choice": {},
          "parallel_tool_calls": {
            "type": "boolean"
          },
          "max_output_tokens": {
            "type": "integer",
            "minimum": 16,
            "maximum": 131072
          },
          "service_tier": {
            "type": "string",
            "enum": [
              "auto",
              "default",
              "flex",
              "priority"
            ]
          },
          "reasoning": {
            "type": "object",
            "additionalProperties": true
          },
          "text": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "model"
        ],
        "additionalProperties": false
      }
    }
  }
}
