Skip to content

feat(realtime): add OpenAI-compatible WebRTC voice endpoints#500

Merged
SantiagoDePolonia merged 8 commits into
mainfrom
feat/voice-webrtc
Jul 7, 2026
Merged

feat(realtime): add OpenAI-compatible WebRTC voice endpoints#500
SantiagoDePolonia merged 8 commits into
mainfrom
feat/voice-webrtc

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the OpenAI GA realtime WebRTC surface to the gateway, completing the voice story started with the websocket proxy. All routes are gated by the existing REALTIME_ENABLED (default true) — no new configuration.

Endpoint What it does
POST /v1/realtime/calls WebRTC SDP exchange. Accepts both GA shapes: raw application/sdp offer with ?model=, or multipart sdp + session (JSON) fields. Relays the SDP answer and a gateway-relative Location: /v1/realtime/calls/{call_id}.
POST /v1/realtime/client_secrets Mints ephemeral browser credentials, routed by session.model (transcription-model fallback), relayed verbatim.
GET /v1/realtime?call_id=… Attaches to an existing call as a sideband websocket. Calls created through the same instance are routed via an in-memory registry; explicit model+provider params work statelessly otherwise.

User-visible impact

  • Browser/mobile clients can do WebRTC voice through the gateway without provider API keys: the gateway injects credentials during signaling and rewrites the requested model to the resolved provider model, so aliases and virtual models work.
  • Usage tracking for WebRTC: media and events flow peer-to-provider and never transit the gateway, so after creating a call the gateway attaches its own best-effort sideband observer websocket and records usage per response.done (endpoint /v1/realtime/calls, audio/text token details and cost priced as usual). Gateway-relayed sideband attaches for registry-known calls skip the usage tap to avoid double counting.
  • Same gates as every model endpoint: auth, model access, budgets, and rate limits (WebRTC signaling counts toward request windows; concurrent-scope rules can't span the call lifetime since only signaling transits the gateway — documented).
  • Sessions minted via client_secrets authenticate directly against the provider and bypass the gateway (inherent to the ephemeral-key flow; documented).

Provider-specific behavior

  • New optional core.RealtimeCallProvider interface (mirrors RealtimeProvider), implemented by OpenAI, xAI, and Azure OpenAI:
    • xAI shares OpenAI's URL shape. Live-verified: client_secrets mints ephemeral secrets through the gateway; the calls endpoint exists upstream but xAI gates WebRTC per team, so unauthorized accounts get the upstream 403 relayed.
    • Azure OpenAI uses its GA v1 surface (<resource>/openai/v1/realtime/{calls,client_secrets}, api-key header, no api-version); unit-tested against the documented shape (no Azure key available for a live check).
    • Bailian deliberately not wired: WebRTC is allowlist-only with a per-customer endpoint provided by sales (public hosts 404), and the documented flow returns no call id for sideband observation. Z.ai has no WebRTC realtime. Both keep websocket-only support.
  • Passthrough (/p/openai/v1/realtime/calls, …/client_secrets) already relays these endpoints opaquely — verified, no changes needed.

Verification

  • Unit tests: URL derivation, provider targets, router capability narrowing, call registry (TTL/cap/eviction), sideband observer, offer parsing/multipart rebuild, handler flows (both content types, model rewrite, Location rewrite, error mapping, attach + double-count guard, disabled/missing-model/unknown-model cases).
  • Live E2E against a mock upstream: SDP + multipart call creation, credential injection, Location rewrite, client secrets relay, sideband attach by bare call_id (101 + frame relay), observer-recorded usage rows in SQLite with exact audio-rate cost math, 401/404 negative cases, passthrough parity.

Note: the swagger regen also picks up previously ungenerated /admin/rate-limits* paths (pre-existing drift).

Possible follow-ups (out of scope): typed call-control proxies (hangup etc. — available via passthrough today), WebRTC for additional providers.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added realtime call attachment support via call_id.
    • Introduced WebRTC signaling and credential flows: POST /v1/realtime/calls and POST /v1/realtime/client_secrets.
  • Documentation
    • Expanded realtime API/config documentation and OpenAPI specs, including conditional model requirements, supported query parameters, and additional response/error codes.
  • Bug Fixes
    • Improved realtime request routing/validation and refined usage tracking for attached sessions to avoid double counting.

Expose the GA realtime WebRTC surface behind REALTIME_ENABLED:

- POST /v1/realtime/calls: model-routed SDP exchange accepting both GA
  shapes (application/sdp offer with ?model=, or multipart sdp + session
  JSON). Provider credentials are injected, the requested model is
  rewritten to the resolved provider model (aliases/virtual models work),
  and the SDP answer is relayed with a gateway-relative Location header.
- POST /v1/realtime/client_secrets: ephemeral browser credentials routed
  by session.model (with the transcription-model fallback), gated by the
  same model-access, budget, and rate-limit rules as other endpoints.
- GET /v1/realtime?call_id=...: sideband websocket attach to an existing
  call, routed via an in-memory call registry (explicit model+provider
  params as the stateless fallback).

WebRTC media and events flow directly between client and provider, so
the gateway attaches a best-effort sideband observer websocket to each
call it creates and records usage per response.done (endpoint
/v1/realtime/calls); gateway-relayed attaches for registry-known calls
skip the usage tap to avoid double counting.

Providers opt in via the new core.RealtimeCallProvider interface
(OpenAI only for now). Swagger regen also picks up previously
ungenerated /admin/rate-limits paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 6, 2026, 11:02 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds realtime call attach via call_id, new /v1/realtime/calls and /v1/realtime/client_secrets endpoints, provider/router/server support, an in-memory call registry, websocket observation, and updated configuration and API documentation.

Changes

Realtime signaling feature

Layer / File(s) Summary
Core contracts and URL helpers
internal/core/realtime.go, internal/core/endpoints.go, internal/providers/realtime_url.go, internal/providers/realtime_url_test.go
Adds CallID, new realtime signaling target types, expands realtime endpoint classification and body modes, and refactors realtime URL construction with attach and HTTP signaling helpers.
Provider realtime targets and tests
internal/providers/openai/realtime.go, internal/providers/azure/realtime.go, internal/providers/xai/realtime.go, internal/providers/*/realtime_test.go
Adds realtime attach, calls, and client-secret target resolution in OpenAI, Azure, and xAI providers with matching unit tests.
Router realtime target resolution
internal/providers/router.go, internal/providers/router_realtime_test.go
Propagates CallID through realtime routing and adds model-to-provider resolution for call and client-secret targets, with router tests and mock provider support.
Call registry and websocket observer
internal/realtime/registry.go, internal/realtime/registry_test.go, internal/realtime/observer.go, internal/realtime/observer_test.go, internal/realtime/proxy.go
Adds an in-memory call registry with TTL and eviction plus a websocket observer for sideband usage tracking, with tests and heartbeat refactoring.
Handler and route wiring
internal/server/handlers.go, internal/server/http.go
Wires realtime dependencies into the handler, adds realtime call and client-secret routes, and updates GET /v1/realtime documentation on the handler surface.
Realtime attach handling
internal/server/realtime_service.go
Reads call_id on websocket sessions, resolves registry-backed routes, suppresses duplicate usage taps on attached sessions, and passes explicit tap callbacks through proxying.
Realtime calls and client secrets
internal/server/realtime_webrtc_service.go, internal/server/realtime_webrtc_service_test.go
Implements SDP and JSON signaling handlers, model rewriting, upstream forwarding, call registration, location rewriting, sideband observation, and the corresponding handler tests.

Realtime and API documentation

Layer / File(s) Summary
Documentation and generated specs
.env.template, CLAUDE.md, docs/advanced/api-endpoints.mdx, cmd/gomodel/docs/docs.go, docs/openapi.json
Expands realtime configuration and API docs and adds the new realtime endpoint definitions across generated docs and OpenAPI artifacts.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • ENTERPILOT/GoModel#394: Extends the same realtime websocket reverse-proxy and server-side realtime routing surface.
  • ENTERPILOT/GoModel#396: Adds the Azure realtime provider foundation that this PR extends with call attach and signaling targets.
  • ENTERPILOT/GoModel#486: Also changes xAI realtime target logic in internal/providers/xai/realtime.go.

Poem

I hopped through the signals, light as air,
With call_id tucked in my fluffy care.
New routes sprang up, docs bloomed bright,
And websockets hummed through the night. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding OpenAI-compatible WebRTC voice endpoints.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/voice-webrtc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jul 6, 2026

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge. The new WebRTC signaling endpoints follow existing patterns, credentials are correctly injected server-side without client exposure, and the double-count prevention for sideband observers is sound.

The implementation is well-tested (unit tests plus live E2E), the auth/budget/rate-limit gates are applied consistently, and the observer lifecycle is bounded. The two observations above are defensive edge cases that do not affect the happy path and are unlikely with well-behaved providers.

internal/server/realtime_webrtc_service.go — the realtimeCallIDFromLocation helper and the observer goroutine lifetime could benefit from a second look for the edge cases noted.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Gateway
    participant Provider

    Note over Client,Provider: POST /v1/realtime/calls (SDP exchange)
    Client->>Gateway: POST /v1/realtime/calls
    Gateway->>Gateway: resolve model, auth, rate-limit
    Gateway->>Provider: POST /v1/realtime/calls (injected creds, resolved model)
    Provider-->>Gateway: 201 SDP answer + Location header
    Gateway->>Gateway: register call_id, rewrite Location
    Gateway-->>Client: "201 SDP answer + Location: /v1/realtime/calls/{call_id}"
    Gateway-)Provider: wss sideband observer goroutine (records usage)

    Note over Client,Provider: GET /v1/realtime?call_id= (sideband attach)
    Client->>Gateway: "GET /v1/realtime?call_id={call_id}"
    Gateway->>Gateway: lookup registry (or use explicit model+provider)
    Gateway->>Provider: wss upgrade with call_id
    Provider-->>Gateway: 101 Switching Protocols
    Gateway-->>Client: 101 Switching Protocols
    Note over Gateway: tap=nil for registry-known calls

    Note over Client,Provider: POST /v1/realtime/client_secrets
    Client->>Gateway: POST /v1/realtime/client_secrets
    Gateway->>Gateway: resolve model, auth, rate-limit, rewrite session.model
    Gateway->>Provider: POST /v1/realtime/client_secrets (injected creds)
    Provider-->>Gateway: ephemeral token
    Gateway-->>Client: ephemeral token
    Note over Client,Provider: Client connects directly to provider (bypasses gateway)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Gateway
    participant Provider

    Note over Client,Provider: POST /v1/realtime/calls (SDP exchange)
    Client->>Gateway: POST /v1/realtime/calls
    Gateway->>Gateway: resolve model, auth, rate-limit
    Gateway->>Provider: POST /v1/realtime/calls (injected creds, resolved model)
    Provider-->>Gateway: 201 SDP answer + Location header
    Gateway->>Gateway: register call_id, rewrite Location
    Gateway-->>Client: "201 SDP answer + Location: /v1/realtime/calls/{call_id}"
    Gateway-)Provider: wss sideband observer goroutine (records usage)

    Note over Client,Provider: GET /v1/realtime?call_id= (sideband attach)
    Client->>Gateway: "GET /v1/realtime?call_id={call_id}"
    Gateway->>Gateway: lookup registry (or use explicit model+provider)
    Gateway->>Provider: wss upgrade with call_id
    Provider-->>Gateway: 101 Switching Protocols
    Gateway-->>Client: 101 Switching Protocols
    Note over Gateway: tap=nil for registry-known calls

    Note over Client,Provider: POST /v1/realtime/client_secrets
    Client->>Gateway: POST /v1/realtime/client_secrets
    Gateway->>Gateway: resolve model, auth, rate-limit, rewrite session.model
    Gateway->>Provider: POST /v1/realtime/client_secrets (injected creds)
    Provider-->>Gateway: ephemeral token
    Gateway-->>Client: ephemeral token
    Note over Client,Provider: Client connects directly to provider (bypasses gateway)
Loading

Reviews (2): Last reviewed commit: "fix(realtime): apply the relay heartbeat..." | Re-trigger Greptile

…responses

Pin the protocol-mandated Content-Type (application/sdp for call answers,
application/json for client secrets) instead of echoing the upstream
header, and set X-Content-Type-Options: nosniff on relayed bodies, so a
misbehaving upstream cannot turn the relay into browser-rendered markup
(CodeQL go/reflected-xss). Add tests for capability gating, malformed
multipart bodies, unreachable upstreams, missing Location headers, and
invalid client-secret JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmd/gomodel/docs/docs.go (1)

4396-4522: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Missing 429 response for rate-limited realtime endpoints.

Both POST /v1/realtime/calls and POST /v1/realtime/client_secrets call enforceRateLimit before forwarding to the provider (per internal/server/realtime_webrtc_service.go), yet neither documents a 429 response, unlike sibling endpoints such as /v1/chat/completions and /v1/embeddings which document 429 for the same gate.

📝 Suggested fix
                 "responses": {
                     "201": {
                         "description": "SDP answer",
                         ...
                     },
+                    "429": {
+                        "description": "Too Many Requests",
+                        "schema": {
+                            "$ref": "`#/definitions/core.OpenAIErrorEnvelope`"
+                        }
+                    },
                     "501": {

(and similarly for /v1/realtime/client_secrets)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/gomodel/docs/docs.go` around lines 4396 - 4522, Add a documented 429
response to both realtime OpenAPI operations because `enforceRateLimit` can
reject them before provider forwarding. Update the Swagger entries for the `POST
/v1/realtime/calls` and `POST /v1/realtime/client_secrets` operations in
`docs.go` so they match sibling rate-limited endpoints like `chatCompletions`
and `embeddings`, using the same `core.OpenAIErrorEnvelope` schema for the `429`
response.
docs/openapi.json (1)

6482-6570: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add request bodies for both realtime POST endpoints
/v1/realtime/calls and /v1/realtime/client_secrets still have no requestBody, so client generators and the docs UI can’t model the SDP/multipart payload or the JSON session body. Add explicit body annotations to the handlers and regenerate the spec.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/openapi.json` around lines 6482 - 6570, The `/v1/realtime/calls` POST
operation is missing an explicit request body, so update the handler annotations
for the realtime create-call endpoint to declare the accepted `application/sdp`
and multipart payloads (including the JSON session field), then regenerate
`docs/openapi.json`. Also check the companion `/v1/realtime/client_secrets` POST
handler and add its request body annotation too, using the existing realtime
route symbols so the generated spec and client generators can model both
payloads correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/gomodel/docs/docs.go`:
- Around line 4396-4461: The OpenAPI spec for the realtime call endpoint is out
of sync with the realtime handler’s actual media types. Update the
/v1/realtime/calls operation in docs.go so its consumes includes both
application/sdp and multipart/form-data, and its produces matches the SDP answer
returned by the handler instead of text/plain. Use the existing realtime call
definition to keep the request/response contract aligned for client generation
and validation.

In `@internal/realtime/registry_test.go`:
- Around line 60-79: Current coverage in TestCallRegistryEvictsAtCapacity only
checks inserting a brand-new call at capacity; add a focused case around
Registry.Register that re-registers an existing call ID when len(r.entries) is
already maxCalls. Verify the existing entry is updated in place and that no
unrelated call is evicted, so Lookup on a different preexisting ID still
succeeds after the second Register call.

In `@internal/realtime/registry.go`:
- Around line 51-64: The Register method in CallRegistry should avoid evicting
when re-registering an existing call ID at capacity. Check whether callID
already exists in r.entries after pruneLocked and before calling
evictSoonestLocked, and only evict when this is a new entry that would actually
increase the map size. Keep the overwrite path in Register unchanged so existing
entries are refreshed without removing an unrelated route.

In `@internal/server/handlers.go`:
- Around line 339-357: The Swagger annotations for RealtimeCalls only declare
multipart form data, but the endpoint also supports raw application/sdp offers
with the model query parameter. Update the doc comments on Handler.RealtimeCalls
to reflect both accepted request content types by adding the raw SDP media type
alongside mpfd, while keeping the existing description of the dual input paths
accurate.

In `@internal/server/realtime_service.go`:
- Around line 79-98: When realtime_service.go handles a successful
s.calls.Lookup(callID) in the attach path, make the registered call’s Model and
Provider fully override any client-supplied model/providerHint instead of only
backfilling empty values. Update the logic in the call_id lookup branch so later
checks in prepare and enforceRateLimit always use the registry values, and add a
test covering call_id with a conflicting model to verify it resolves to the
registered call’s model/provider.

In `@internal/server/realtime_webrtc_service.go`:
- Around line 193-201: The fallback in the realtime signaling request path is
using an unbounded client when s.httpClient is nil. Update the request logic in
the realtime WebRTC service method that calls client.Do so it never falls back
to http.DefaultClient; instead use a bounded default client consistent with
httpclient.NewDefaultHTTPClient(), or make the httpClient dependency required so
the nil path cannot happen.

---

Outside diff comments:
In `@cmd/gomodel/docs/docs.go`:
- Around line 4396-4522: Add a documented 429 response to both realtime OpenAPI
operations because `enforceRateLimit` can reject them before provider
forwarding. Update the Swagger entries for the `POST /v1/realtime/calls` and
`POST /v1/realtime/client_secrets` operations in `docs.go` so they match sibling
rate-limited endpoints like `chatCompletions` and `embeddings`, using the same
`core.OpenAIErrorEnvelope` schema for the `429` response.

In `@docs/openapi.json`:
- Around line 6482-6570: The `/v1/realtime/calls` POST operation is missing an
explicit request body, so update the handler annotations for the realtime
create-call endpoint to declare the accepted `application/sdp` and multipart
payloads (including the JSON session field), then regenerate
`docs/openapi.json`. Also check the companion `/v1/realtime/client_secrets` POST
handler and add its request body annotation too, using the existing realtime
route symbols so the generated spec and client generators can model both
payloads correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ca216231-99d2-4ecb-9fed-dc7614800d70

📥 Commits

Reviewing files that changed from the base of the PR and between 14d4bde and 0ee2e3d.

📒 Files selected for processing (22)
  • .env.template
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • docs/advanced/api-endpoints.mdx
  • docs/openapi.json
  • internal/core/endpoints.go
  • internal/core/realtime.go
  • internal/providers/openai/realtime.go
  • internal/providers/openai/realtime_test.go
  • internal/providers/realtime_url.go
  • internal/providers/realtime_url_test.go
  • internal/providers/router.go
  • internal/providers/router_realtime_test.go
  • internal/realtime/observer.go
  • internal/realtime/observer_test.go
  • internal/realtime/registry.go
  • internal/realtime/registry_test.go
  • internal/server/handlers.go
  • internal/server/http.go
  • internal/server/realtime_service.go
  • internal/server/realtime_webrtc_service.go
  • internal/server/realtime_webrtc_service_test.go

Comment thread cmd/gomodel/docs/docs.go
Comment thread internal/realtime/registry_test.go
Comment thread internal/realtime/registry.go
Comment thread internal/server/handlers.go
Comment thread internal/server/realtime_service.go
Comment thread internal/server/realtime_webrtc_service.go
…nd spec

- Attach by call_id: the registry route now overrides a client-supplied
  model/provider instead of only backfilling, so model-access and
  rate-limit checks cannot be steered toward a model the registered call
  is not running.
- Call registry: re-registering an existing call id at capacity
  overwrites in place instead of evicting an unrelated entry.
- Signaling requests never fall back to the unbounded http.DefaultClient;
  the nil-client path now uses the configured-timeout default client.
- OpenAPI: declare both accepted call content types (application/sdp +
  multipart), the application/sdp answer, request bodies for both
  realtime POST endpoints, and 429 responses on the rate-limited
  realtime routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

Addressed the two outside-diff findings in fd0395e as well: both realtime POST operations now document 429 responses (core.OpenAIErrorEnvelope, matching the sibling rate-limited endpoints, plus the GET /v1/realtime route which shares the same gate), and both declare request bodies — the SDP/multipart payload for /v1/realtime/calls and the JSON session body for /v1/realtime/client_secrets — so client generators can model the payloads.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/openapi.json (1)

6533-6548: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Drop the application/json 201 response from POST /v1/realtime/calls (docs/openapi.json:6533-6548). RealtimeCalls always relays the SDP answer as application/sdp; keeping a JSON success arm misleads API consumers and codegen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/openapi.json` around lines 6533 - 6548, The POST /v1/realtime/calls 201
response in the RealtimeCalls OpenAPI spec currently advertises both
application/sdp and application/json, but it should only describe the SDP
answer. Update the response definition in docs/openapi.json so the 201 success
arm for RealtimeCalls removes the application/json content and leaves
application/sdp as the only response format.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/openapi.json`:
- Around line 6654-6664: The client-secret request body schema is too generic as
an untyped object, so update the OpenAPI definition for the requestBody in the
client-secret endpoint to expose the known fields described in the text. Keep
session.model (and the nested transcription model if applicable) plus optional
expires_after as explicit, loosely typed properties, and allow
additionalProperties so the remaining payload can still pass through verbatim.
- Around line 6517-6532: The multipart request body for the handler is still
being documented as an opaque string, so the generated OpenAPI schema does not
expose the actual multipart fields. Update the request documentation in
internal/server/handlers.go for the mpfd handling so it describes the multipart
form explicitly, either by using separate formData parameters or a struct-backed
multipart schema, and make sure the request shape includes both the sdp and
session parts.

---

Outside diff comments:
In `@docs/openapi.json`:
- Around line 6533-6548: The POST /v1/realtime/calls 201 response in the
RealtimeCalls OpenAPI spec currently advertises both application/sdp and
application/json, but it should only describe the SDP answer. Update the
response definition in docs/openapi.json so the 201 success arm for
RealtimeCalls removes the application/json content and leaves application/sdp as
the only response format.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d41aa3c6-4410-4d74-8b2a-de77c9c6f337

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee2e3d and fd0395e.

📒 Files selected for processing (8)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/realtime/registry.go
  • internal/realtime/registry_test.go
  • internal/server/handlers.go
  • internal/server/realtime_service.go
  • internal/server/realtime_webrtc_service.go
  • internal/server/realtime_webrtc_service_test.go

Comment thread docs/openapi.json
Comment thread docs/openapi.json
SantiagoDePolonia and others added 2 commits July 7, 2026 12:09
xAI mirrors OpenAI's shape exactly, so calls, client secrets, and
call_id sideband attach derive from the base URL via the shared
helpers. Verified live: /v1/realtime/client_secrets mints ephemeral
secrets through the gateway; /v1/realtime/calls exists upstream but is
team-gated by xAI, so unauthorized accounts get the 403 relayed.

Azure OpenAI uses its GA v1 surface at
<resource>/openai/v1/realtime/{calls,client_secrets} (api-key header,
no api-version), with sideband attach on the same GA path; the model
query parameter selects the deployment.

Bailian is deliberately not wired: its WebRTC endpoint is
allowlist-only and provided per customer by sales (probes 404 on both
public hosts), and the documented flow returns no call id for sideband
observation. Z.ai has no WebRTC realtime; both keep websocket-only
support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SantiagoDePolonia and others added 2 commits July 7, 2026 12:33
Main's realtime hardening (#497) added a ping/pong heartbeat to the
websocket relay so silently dead peers tear sessions down. The WebRTC
sideband observer has the same exposure — a dead provider connection
left it blocked in Read until the 6h call TTL. The heartbeat loop is
now shared: relay pings both peers, the observer pings its upstream,
and a ping timeout ends the observation immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

@coderabbitai continue

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@SantiagoDePolonia, continuing the review now — I'll pick up the changes from commit fd0395e8 (429 documentation and request bodies for both realtime POST endpoints) along with any other updates since the last pass.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/providers/azure/realtime.go (1)

90-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared base-URL normalization to avoid drift between realtimeURL and realtimeGARoot.

realtimeGARoot (lines 90-110) and the unchanged realtimeURL (lines 123-148) both parse resourceRootBaseURL, validate u.Host, and strip /openai/v1//openai suffixes — differing only in the final scheme mapping, path suffix, and query params. Also, in realtimeAttachURL (line 81) the TrimSuffix(u.Path, "/realtime") + "/realtime" is a no-op: realtimeGARoot already guarantees the path ends in /realtime, so this can be dropped.

As per coding guidelines, "avoid adding abstractions until a repeated pattern clearly justifies them" — this duplication is now a repeated pattern worth consolidating so a future change to the path-stripping logic doesn't silently diverge between the GA and legacy realtime URL builders.

♻️ Proposed refactor
+// azureRealtimeBase parses and normalizes the configured base URL down to its
+// resource root, stripping any existing /openai[/v1] or deployment sub-path.
+func (p *Provider) azureRealtimeBase() (*url.URL, error) {
+	root := resourceRootBaseURL(p.GetBaseURL())
+	u, err := url.Parse(root)
+	if err != nil || u.Host == "" {
+		return nil, core.NewInvalidRequestError("invalid azure realtime base url: "+root, err)
+	}
+	path := strings.TrimRight(u.Path, "/")
+	path = strings.TrimSuffix(path, "/openai/v1")
+	path = strings.TrimSuffix(path, "/openai")
+	u.Path = path
+	u.RawQuery = ""
+	return u, nil
+}
+
 func (p *Provider) realtimeGARoot() (*url.URL, error) {
-	root := resourceRootBaseURL(p.GetBaseURL())
-	u, err := url.Parse(root)
-	if err != nil || u.Host == "" {
-		return nil, core.NewInvalidRequestError("invalid azure realtime base url: "+root, err)
-	}
+	u, err := p.azureRealtimeBase()
+	if err != nil {
+		return nil, err
+	}
 	switch strings.ToLower(u.Scheme) {
 	case "https", "wss", "":
 		u.Scheme = "https"
 	case "http", "ws":
 		u.Scheme = "http"
 	default:
 		return nil, core.NewInvalidRequestError("unsupported azure realtime base url scheme: "+u.Scheme, nil)
 	}
-	path := strings.TrimRight(u.Path, "/")
-	path = strings.TrimSuffix(path, "/openai/v1")
-	path = strings.TrimSuffix(path, "/openai")
-	u.Path = path + "/openai/v1/realtime"
-	u.RawQuery = ""
+	u.Path += "/openai/v1/realtime"
 	return u, nil
 }
 func (p *Provider) realtimeAttachURL(callID string) (string, error) {
 	u, err := p.realtimeGARoot()
 	if err != nil {
 		return "", err
 	}
 	switch strings.ToLower(u.Scheme) {
 	case "https":
 		u.Scheme = "wss"
 	case "http":
 		u.Scheme = "ws"
 	}
-	u.Path = strings.TrimSuffix(u.Path, "/realtime") + "/realtime"
 	q := url.Values{}
 	q.Set("call_id", callID)
 	u.RawQuery = q.Encode()
 	return u.String(), nil
 }

realtimeURL (line 123) can similarly delegate to azureRealtimeBase() for the parse/strip step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/azure/realtime.go` around lines 90 - 148, The Azure
realtime URL builders duplicate the same base-URL parsing and path normalization
logic, which can drift over time. Extract the shared
`resourceRootBaseURL`/`url.Parse`/host validation and `/openai[/v1]` stripping
into a common helper used by both `realtimeGARoot` and `realtimeURL`, keeping
only the scheme mapping, final path suffix, and query assembly different. Also
simplify `realtimeAttachURL` by removing the redundant `/realtime`
trim-then-append since `realtimeGARoot` already returns a `/realtime` path.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/providers/azure/realtime.go`:
- Around line 90-148: The Azure realtime URL builders duplicate the same
base-URL parsing and path normalization logic, which can drift over time.
Extract the shared `resourceRootBaseURL`/`url.Parse`/host validation and
`/openai[/v1]` stripping into a common helper used by both `realtimeGARoot` and
`realtimeURL`, keeping only the scheme mapping, final path suffix, and query
assembly different. Also simplify `realtimeAttachURL` by removing the redundant
`/realtime` trim-then-append since `realtimeGARoot` already returns a
`/realtime` path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 632b82a4-b6e2-4bd2-8b45-e30443cb9871

📥 Commits

Reviewing files that changed from the base of the PR and between fd0395e and e8d249d.

📒 Files selected for processing (12)
  • .env.template
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/providers/azure/realtime.go
  • internal/providers/azure/realtime_test.go
  • internal/providers/xai/realtime.go
  • internal/providers/xai/realtime_test.go
  • internal/realtime/observer.go
  • internal/realtime/observer_test.go
  • internal/realtime/proxy.go
  • internal/server/handlers.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/providers/azure/realtime.go (1)

56-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating the three near-identical URL builders.

realtimeHTTPTarget, realtimeAttachURL, and realtimeURL all follow the same pattern: get root via realtimeRoot, append a path, optionally set query params, return. Extracting a small private helper (e.g., buildRealtimeURL(insecure, secure, path string, query url.Values)) would reduce repetition across these three call sites.

♻️ Example consolidation
-func (p *Provider) realtimeHTTPTarget(req *core.RealtimeRequest, endpoint string) (*core.RealtimeHTTPTarget, error) {
-	if req == nil || strings.TrimSpace(req.Model) == "" {
-		return nil, core.NewInvalidRequestError("model is required for realtime calls", nil)
-	}
-	u, err := p.realtimeRoot("http", "https")
-	if err != nil {
-		return nil, err
-	}
-	u.Path += "/openai/v1/realtime/" + endpoint
-	return &core.RealtimeHTTPTarget{URL: u.String(), Headers: p.realtimeAuthHeaders()}, nil
-}
-
-func (p *Provider) realtimeAttachURL(callID string) (string, error) {
-	u, err := p.realtimeRoot("ws", "wss")
-	if err != nil {
-		return "", err
-	}
-	u.Path += "/openai/v1/realtime"
-	q := url.Values{}
-	q.Set("call_id", callID)
-	u.RawQuery = q.Encode()
-	return u.String(), nil
-}
+func (p *Provider) buildRealtimeURL(insecure, secure, path string, query url.Values) (*url.URL, error) {
+	u, err := p.realtimeRoot(insecure, secure)
+	if err != nil {
+		return nil, err
+	}
+	u.Path += path
+	if query != nil {
+		u.RawQuery = query.Encode()
+	}
+	return u, nil
+}
+
+func (p *Provider) realtimeHTTPTarget(req *core.RealtimeRequest, endpoint string) (*core.RealtimeHTTPTarget, error) {
+	if req == nil || strings.TrimSpace(req.Model) == "" {
+		return nil, core.NewInvalidRequestError("model is required for realtime calls", nil)
+	}
+	u, err := p.buildRealtimeURL("http", "https", "/openai/v1/realtime/"+endpoint, nil)
+	if err != nil {
+		return nil, err
+	}
+	return &core.RealtimeHTTPTarget{URL: u.String(), Headers: p.realtimeAuthHeaders()}, nil
+}
+
+func (p *Provider) realtimeAttachURL(callID string) (string, error) {
+	q := url.Values{}
+	q.Set("call_id", callID)
+	u, err := p.buildRealtimeURL("ws", "wss", "/openai/v1/realtime", q)
+	if err != nil {
+		return "", err
+	}
+	return u.String(), nil
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/azure/realtime.go` around lines 56 - 96, The three
realtime URL builders in Provider are duplicating the same root-path-query
assembly logic. Extract a shared private helper around realtimeRoot that accepts
the path and optional query values, then update realtimeHTTPTarget,
realtimeAttachURL, and realtimeURL to call it so the URL construction is
centralized and easier to maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/providers/azure/realtime.go`:
- Around line 56-96: The three realtime URL builders in Provider are duplicating
the same root-path-query assembly logic. Extract a shared private helper around
realtimeRoot that accepts the path and optional query values, then update
realtimeHTTPTarget, realtimeAttachURL, and realtimeURL to call it so the URL
construction is centralized and easier to maintain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6260647c-f74d-43bf-a368-beb5870c13c8

📥 Commits

Reviewing files that changed from the base of the PR and between e8d249d and a40d8f4.

📒 Files selected for processing (1)
  • internal/providers/azure/realtime.go

@SantiagoDePolonia SantiagoDePolonia merged commit 2c3ab68 into main Jul 7, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants