Embeddings, audio & realtime
Vectors, speech-to-text, text-to-speech, realtime voice over WebSocket and the ephemeral tokens that make browser clients safe.
Beyond chat, the gateway serves four OpenAI-compatible media surfaces. They take the same key, the same rate limits and write the same usage records — but none of them starts a billable task, so they never count against a task cap.
Embeddings
POST /v1/embeddings. Default model codai-embed resolves to voyage-code-3, chosen for code retrieval. Input is a string or a non-empty array of strings; optional dimensions and user. Only encoding_format: "float" is accepted.
curl https://ai.codai.ro/v1/embeddings \
-H "Authorization: Bearer $CODAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "codai-embed", "input": ["hello world", "vector me up"] }'{
"object": "list",
"model": "voyage-code-3",
"data": [
{ "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, …] },
{ "object": "embedding", "index": 1, "embedding": [0.0789, 0.0012, …] }
],
"usage": { "prompt_tokens": 7, "total_tokens": 7 }
}If the requested embedding model is unavailable the gateway may serve a compatible alternative and discloses it in x-codai-embed-fallback: <requested>-><served>. Check the header when vector spaces must stay identical across writes and reads.
Speech-to-text
POST /v1/audio/transcriptions, multipart form, 25 MB maximum. Fields: file (required), model (default codai-transcribe), and the optional language, prompt, response_format, temperature — forwarded as-is.
model | Backend |
|---|---|
codai-transcribe, whisper-1, whisper | Whisper |
gpt-4o-mini-transcribe | gpt-4o-mini-transcribe |
curl https://ai.codai.ro/v1/audio/transcriptions \
-H "Authorization: Bearer $CODAI_API_KEY" \
-F [email protected] \
-F model=codai-transcribe \
-F language=ro{ "text": "Let's start with the deploy checklist." }Billing is flat per audio minute, estimated from the upload size (about 1 MB per minute of compressed voice, minimum one minute) and recorded as cost_micro_usd on the usage row.
Text-to-speech
POST /v1/audio/speech. JSON body: input (required, ≤ 4 096 characters), model (default codai-tts), voice, response_format (default mp3), speed, and — for the expressive model only — instructions (≤ 1 000 characters). The response body is the audio bytes.
model | Backend | Default voice | Notes |
|---|---|---|---|
codai-tts, tts-1, tts | Standard TTS | alloy | Cheap, neutral. |
codai-tts-expressive, gpt-4o-mini-tts | gpt-4o-mini-tts | marin | Takes instructions such as "calm, warm, short Romanian, slightly amused". Voices: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse, marin, cedar. |
curl https://ai.codai.ro/v1/audio/speech \
-H "Authorization: Bearer $CODAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "codai-tts", "input": "Hello from codai.", "voice": "alloy" }' \
--output hello.mp3Billing is flat per 1 000 input characters.
Ephemeral tokens
Browsers and webviews must never hold your long-lived key. Instead, mint a short-lived scoped token server-side with POST /v1/tokens and hand that to the client. Scopes: realtime, audio, embeddings — chat is deliberately not mintable. TTL 60–3 600 s, default 600.
curl https://ai.codai.ro/v1/tokens \
-H "Authorization: Bearer $CODAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "scope": "realtime", "ttl_seconds": 600 }'{ "token": "codai_eph_v1.…", "expires_at": "2026-09-23T12:10:00.000Z", "scope": "realtime" }The token is used exactly like an API key — Authorization: Bearer codai_eph_v1.… — but only on its scope's surface. It cannot mint further tokens (403 forbidden). Revoking the minting key or downgrading its tier invalidates outstanding tokens within the principal-cache window.
Realtime voice
WSS /v1/realtime?model=<alias> — bidirectional audio over a single WebSocket, relayed verbatim to the upstream protocol.
model | Protocol | Backend |
|---|---|---|
codai-realtime (default for OpenAI-style clients) | OpenAI Realtime | gpt-realtime-2.1 |
codai-transcribe-live | OpenAI Realtime, intent=transcription | Streaming STT — partials and turn events, no model reply |
codai-voice | Gemini Live BidiGenerateContent | gemini-live-2.5-flash-native-audio |
If model is omitted, the gateway assumes codai-voice.
Authenticating the upgrade
Raw codai_… keys are rejected in the query string (close code 4401). They would leak into request logs, proxies and browser history. Browsers must use an ephemeral realtime token.
- Browser:
wss://ai.codai.ro/v1/realtime?model=codai-realtime&key=codai_eph_v1.… - Server: send
Authorization: Bearer <key or token>on the upgrade request —?key=is not needed.
// token fetched from your own backend, which called POST /v1/tokens
const ws = new WebSocket(
`wss://ai.codai.ro/v1/realtime?model=codai-realtime&key=${encodeURIComponent(token)}`,
);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'session.update', session: { instructions: 'Answer briefly.' } }));
};
ws.onmessage = (ev) => {
const event = JSON.parse(ev.data);
if (event.type === 'response.audio.delta') {
/* append base64 PCM to your player */
}
};Close codes you may see: 4401 invalid or disallowed credential, 4503 the requested lane is not configured on this gateway, 1011 internal error. Usage is recorded when the session closes, with its duration.