Support OpenAI Realtime session control endpoints (JSON-only)
Split from #53 — covers the stateless JSON-only session control surface for OpenAI Realtime. The SIP/SDP calls endpoint and the WebSocket audio transport are tracked separately.
Background
Split out from #53 (Feature Request: Support embeddings and OpenAI Realtime API endpoints). Embeddings + streaming on /v1/chat/completions and /v1/responses are already supported; this issue covers only the Realtime JSON session control surface that is currently missing.
Searched src/ for realtime, client_secrets, realtime/sessions, realtime/transcription_sessions, realtime/translations — zero hits. The only WebSocket route registered in src/proxy_app/main.py:2213 is /v1/ws, which is a quota-stats push channel (quota_stats / error_event JSON envelopes), not an OpenAI Realtime transport.
Requested endpoints
| Method |
Path |
Purpose |
POST |
/v1/realtime/client_secrets |
Ephemeral Realtime session token |
POST |
/v1/realtime/sessions |
Realtime session config (audio model) |
POST |
/v1/realtime/transcription_sessions |
Transcription session config |
POST |
/v1/realtime/translations |
Realtime translation session config |
All four share the same shape: JSON request in, JSON response out, stateless passthrough.
Why these four are one issue
These endpoints share:
- JSON body shape (no SDP, no binary, no streaming)
- Stateless handler shape mirroring existing
/v1/embeddings (main.py:1423-1488) and /v1/chat/completions (main.py:948-1104)
- Per-exception error mapping (
main.py:1490-1513) — same litellm exception → HTTP status mapping
- Request-logger integration —
log_request_to_console(...) and RawIOLogger.log_request(...) both accept parsed JSON
- Single small PR — each route is ~15 lines; bundling the four keeps the change reviewable
Splitting into one issue per endpoint would inflate review surface for no design benefit. They share enough code that shipping them together is the right granularity.
Why client_secrets is NOT separated out
client_secrets is the ephemeral-token issuer used by clients that don't want to embed a long-lived OpenAI API key in their browser/SDK. It's JSON-only and stateless — same shape as the other three. There is no design call here that doesn't also apply to sessions, transcription_sessions, and translations. Bundling is correct.
Proposed initial implementation
Scope: thin passthrough, no response transformation. Behavior per endpoint:
-
Routing. Register each path in src/proxy_app/main.py alongside /v1/chat/completions (line 948), /v1/responses (line 1108), /v1/embeddings (line 1423). All four routes are simple @app.post(...) handlers that:
- Apply the standard
verify_api_key dependency (main.py:1429 style).
- Forward the parsed JSON request body to the resolved provider via
RotatingClient/litellm.
- Return the upstream response body and status unchanged.
-
Provider resolution. Reuse client.resolve_latest_async(...) (main.py:1001) and apply_model_alias(...) (main.py:998) so gpt-4o-realtime-preview follows the same alias rules as chat models. The example provider already maps gpt-4o-realtime-preview → gpt-4o (src/rotator_library/providers/example_provider.py:326,344-345) — verify whether this should be removed for users who specifically target Realtime models.
-
Error mapping. Reuse the existing per-exception mapping block (main.py:1490-1513 style) so auth/rate-limit/timeout errors get the same status codes clients already see on chat completions.
-
Logging. Pipe through log_request_to_console(...) (main.py:1439) and RawIOLogger if ENABLE_RAW_LOGGING is set (main.py:1422 style), matching the existing pattern on /v1/embeddings.
-
Quotas. Per-credential quota accounting applies — these endpoints are billable but low-cardinality.
-
Tests. Add tests/ cases under the existing chat-completions / embeddings test style: happy-path round-trip for each of the four endpoints; 401 path with no PROXY_API_KEY; 400 path on malformed JSON; 429 path on rate limit (use a mock upstream).
Out of scope here
POST /v1/realtime/calls — separate issue (application/sdp body, raw body pass-through, different transport shape).
GET /v1/realtime WebSocket audio transport — separate issue.
- Streaming passthrough on
/v1/chat/completions and /v1/responses — already implemented (main.py:1031-1047, main.py:1152-1192).
References (HEAD on dev: d77a246)
src/proxy_app/main.py:1423-1488 — existing /v1/embeddings handler (closest analog for the new routes)
src/proxy_app/main.py:178-183 — EmbeddingRequest Pydantic model
src/proxy_app/main.py:948-1104 — chat_completions handler (alias resolution + raw logging pattern)
src/proxy_app/main.py:1108-1239 — responses_api handler (transcoder + streaming pattern)
src/proxy_app/main.py:1490-1513 — per-exception error mapping
src/rotator_library/providers/example_provider.py:326,344-345 — Realtime model alias map
src/rotator_library/providers/utilities/codex_ws_transport.py — existing outbound WebSocket plumbing (not directly reusable but reference for session/affinity patterns)
Checklist for review
Support OpenAI Realtime session control endpoints (JSON-only)
Split from #53 — covers the stateless JSON-only session control surface for OpenAI Realtime. The SIP/SDP calls endpoint and the WebSocket audio transport are tracked separately.
Background
Split out from #53 (Feature Request: Support embeddings and OpenAI Realtime API endpoints). Embeddings + streaming on
/v1/chat/completionsand/v1/responsesare already supported; this issue covers only the Realtime JSON session control surface that is currently missing.Searched
src/forrealtime,client_secrets,realtime/sessions,realtime/transcription_sessions,realtime/translations— zero hits. The only WebSocket route registered insrc/proxy_app/main.py:2213is/v1/ws, which is a quota-stats push channel (quota_stats/error_eventJSON envelopes), not an OpenAI Realtime transport.Requested endpoints
POST/v1/realtime/client_secretsPOST/v1/realtime/sessionsPOST/v1/realtime/transcription_sessionsPOST/v1/realtime/translationsAll four share the same shape: JSON request in, JSON response out, stateless passthrough.
Why these four are one issue
These endpoints share:
/v1/embeddings(main.py:1423-1488) and/v1/chat/completions(main.py:948-1104)main.py:1490-1513) — same litellm exception → HTTP status mappinglog_request_to_console(...)andRawIOLogger.log_request(...)both accept parsed JSONSplitting into one issue per endpoint would inflate review surface for no design benefit. They share enough code that shipping them together is the right granularity.
Why
client_secretsis NOT separated outclient_secretsis the ephemeral-token issuer used by clients that don't want to embed a long-lived OpenAI API key in their browser/SDK. It's JSON-only and stateless — same shape as the other three. There is no design call here that doesn't also apply tosessions,transcription_sessions, andtranslations. Bundling is correct.Proposed initial implementation
Scope: thin passthrough, no response transformation. Behavior per endpoint:
Routing. Register each path in
src/proxy_app/main.pyalongside/v1/chat/completions(line 948),/v1/responses(line 1108),/v1/embeddings(line 1423). All four routes are simple@app.post(...)handlers that:verify_api_keydependency (main.py:1429style).RotatingClient/litellm.Provider resolution. Reuse
client.resolve_latest_async(...)(main.py:1001) andapply_model_alias(...)(main.py:998) sogpt-4o-realtime-previewfollows the same alias rules as chat models. The example provider already mapsgpt-4o-realtime-preview→gpt-4o(src/rotator_library/providers/example_provider.py:326,344-345) — verify whether this should be removed for users who specifically target Realtime models.Error mapping. Reuse the existing per-exception mapping block (
main.py:1490-1513style) so auth/rate-limit/timeout errors get the same status codes clients already see on chat completions.Logging. Pipe through
log_request_to_console(...)(main.py:1439) andRawIOLoggerifENABLE_RAW_LOGGINGis set (main.py:1422style), matching the existing pattern on/v1/embeddings.Quotas. Per-credential quota accounting applies — these endpoints are billable but low-cardinality.
Tests. Add
tests/cases under the existing chat-completions / embeddings test style: happy-path round-trip for each of the four endpoints; 401 path with noPROXY_API_KEY; 400 path on malformed JSON; 429 path on rate limit (use a mock upstream).Out of scope here
POST /v1/realtime/calls— separate issue (application/sdpbody, raw body pass-through, different transport shape).GET /v1/realtimeWebSocket audio transport — separate issue./v1/chat/completionsand/v1/responses— already implemented (main.py:1031-1047,main.py:1152-1192).References (HEAD on
dev:d77a246)src/proxy_app/main.py:1423-1488— existing/v1/embeddingshandler (closest analog for the new routes)src/proxy_app/main.py:178-183—EmbeddingRequestPydantic modelsrc/proxy_app/main.py:948-1104—chat_completionshandler (alias resolution + raw logging pattern)src/proxy_app/main.py:1108-1239—responses_apihandler (transcoder + streaming pattern)src/proxy_app/main.py:1490-1513— per-exception error mappingsrc/rotator_library/providers/example_provider.py:326,344-345— Realtime model alias mapsrc/rotator_library/providers/utilities/codex_ws_transport.py— existing outbound WebSocket plumbing (not directly reusable but reference for session/affinity patterns)Checklist for review
gpt-4o-realtime-previewalias mapping inexample_provider.pyshould remain or be Realtime-awareclient_secrets(likely no — ephemeral and short-lived)