Skip to content

refactor(gateway): route realtime API through RouterTrait - #690

Merged
CatherineSue merged 13 commits into
mainfrom
refactor/realtime-router-trait
Mar 10, 2026
Merged

CatherineSue merged 13 commits into
mainfrom
refactor/realtime-router-trait

Conversation

@CatherineSue

@CatherineSue CatherineSue commented Mar 10, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

PR #637 introduced realtime API support (WebSocket + REST) but bypassed RouterTrait entirely — handlers accessed state.context.worker_registry directly instead of going through state.router. This meant realtime traffic lacked:

  • Lazy model refresh — causing spurious 503s when workers haven't loaded model lists yet
  • Metrics — no request/duration/error recording for realtime endpoints
  • WASM middleware — realtime routes were in a separate route group without the middleware stack
  • Circuit breaker integration — no health tracking for realtime requests

Solution

Route realtime API through RouterTrait following the same pattern as v1/chat/completions, v1/messages, and other endpoints.

Changes

  • WorkerRegistry: Add find_best_external_worker() and any_external_worker_supports_model() as pure data query methods (no HTTP dependencies in core)
  • RouterTrait: Add route_realtime_rest() and route_realtime_ws() with default not-implemented stubs
  • OpenAIRouter: Implement both methods using existing select_worker_for_model flow with lazy refresh, metrics recording (record_router_request, record_router_duration, record_router_error), and circuit breaker integration
  • server.rs: Replace direct realtime handler imports with thin delegates to state.router.route_realtime_*(). Merge realtime routes into protected_routes so they share auth, concurrency, and WASM middleware
  • rest.rs / ws.rs: Strip down to shared helpers (proxy_response, build_upstream_ws_url) now that handler logic lives in the router
  • metrics.rs: Add ENDPOINT_REALTIME_* and CONNECTION_WEBSOCKET label constants

Test Plan

  • Existing realtime E2E tests continue to pass (WebSocket and REST endpoints)
  • OpenAIRouter::select_worker_for_model lazy refresh logic is exercised by realtime the same way as chat/completions
  • Metrics labels are registered and recorded for realtime traffic
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated

Summary by CodeRabbit

  • New Features

    • Unified v1 realtime API: WebSocket and REST endpoints for realtime sessions, client secrets, and transcription.
  • Enhancements

    • Better external worker selection for realtime workloads for improved load distribution and availability.
    • Expanded realtime metrics and connection labels for clearer observability.
    • Request validation added for realtime session and transcription payloads.
  • Refactor

    • Consolidated realtime routing and WebSocket handling into existing protected route middleware and simplified forwarding.

@github-actions github-actions Bot added model-gateway Model gateway crate changes realtime-api Realtime API related changes openai OpenAI router changes labels Mar 10, 2026
@coderabbitai

coderabbitai Bot commented Mar 10, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds OpenAI-compatible realtime REST and WebSocket routing, validation/types for realtime session requests, WorkerRegistry helpers for selecting external realtime workers, realtime metrics labels, router/server wiring for realtime flows, and RealtimeRegistry lifecycle integration.

Changes

Cohort / File(s) Summary
Worker Discovery
model_gateway/src/core/worker_registry.rs
Added find_best_external_worker(&self, model_id: &str) -> Option<Arc<dyn Worker>> and any_external_worker_supports_model(&self, model_id: &str) -> bool to pick least-loaded healthy external workers and to check model support.
Metrics Labels
model_gateway/src/observability/metrics.rs
Added realtime endpoint label constants (ENDPOINT_REALTIME, ENDPOINT_REALTIME_SESSIONS, ENDPOINT_REALTIME_CLIENT_SECRETS, ENDPOINT_REALTIME_TRANSCRIPTION) and CONNECTION_WEBSOCKET.
Router Trait / Manager
model_gateway/src/routers/mod.rs, model_gateway/src/routers/router_manager.rs
Extended RouterTrait with realtime route methods and added delegation methods in RouterManager to select a router and forward realtime REST/WS requests.
OpenAI Realtime Modules
model_gateway/src/routers/openai/realtime/rest.rs, model_gateway/src/routers/openai/realtime/ws.rs
Removed inline endpoint handlers; narrowed proxy_response visibility and exposed build_upstream_ws_url as pub(crate); moved proxy and WS handling into OpenAIRouter.
OpenAI Router Implementation
model_gateway/src/routers/openai/router.rs
Integrated realtime: added realtime_registry field, worker selection using new WorkerRegistry helpers, forward_realtime_rest, WS upgrade/registration flow, metrics instrumentation, and lifecycle/cleanup logic.
Server Routing
model_gateway/src/server.rs
Added v1_realtime_* handlers (WS + REST), mounted /v1/realtime/* endpoints under protected middleware, and unified routing composition.
Protocols / Validation
crates/protocols/src/realtime_session.rs
Expanded realtime session types (new fields), derived Validate, added Normalizable impls, validators, and new RealtimeClientSecretCreateRequest type with validation.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Server
    participant Router as OpenAIRouter
    participant Registry as WorkerRegistry
    participant Worker as ExternalWorker

    rect rgba(100,150,200,0.5)
    Client->>Server: realtime REST or WS request
    Server->>Router: route_realtime_rest(...) or route_realtime_ws(req)
    Router->>Registry: find_best_external_worker(model_id)
    Registry->>Registry: filter external, healthy workers
    Registry->>Registry: check supports_model + circuit breaker (can_execute)
    Registry-->>Router: selected ExternalWorker or None
    alt worker found
        Router->>Worker: proxy request / upgrade websocket
        Worker-->>Router: response / stream
        Router-->>Client: response / websocket stream
    else no worker
        Router-->>Client: error (no worker / model not found)
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

protocols

Suggested reviewers

  • key4ng
  • slin1237

Poem

🐰 I hopped through routes both new and bright,

Picked workers light as morning dew,
Sessions blossom, metrics hum at night,
Websockets stream and registries grew,
A rabbit cheers: realtime, anew! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: routing the realtime API through RouterTrait instead of bypassing it, which aligns with the core refactoring objective across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/realtime-router-trait

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

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the Realtime API by integrating its WebSocket and REST endpoints into the existing RouterTrait architecture. This change ensures that realtime traffic now leverages critical gateway features such as lazy model refresh, comprehensive metrics collection, WASM middleware, and circuit breaker integration, which were previously bypassed. The refactoring centralizes routing logic within the OpenAIRouter, leading to a more consistent and robust handling of all API traffic.

Highlights

  • Realtime API Integration: The Realtime API (WebSocket + REST) has been refactored to route through the RouterTrait, addressing previous limitations where it bypassed the core routing infrastructure.
  • Enhanced Worker Selection: Realtime traffic now benefits from lazy model refresh, preventing spurious 503 errors when workers are still loading model lists.
  • Comprehensive Metrics and Observability: Request, duration, and error metrics are now recorded for realtime endpoints, providing better visibility into their performance and health.
  • Middleware and Circuit Breaker Support: Realtime routes are now integrated into the protected_routes group, enabling them to utilize WASM middleware, authentication, concurrency limits, and circuit breaker integration for improved resilience.
  • Code Simplification: The rest.rs and ws.rs modules for realtime APIs have been significantly stripped down, with core handler logic moved into the OpenAIRouter.
Changelog
  • model_gateway/src/core/worker_registry.rs
    • Added find_best_external_worker to find the least-loaded healthy external worker for a model.
    • Added any_external_worker_supports_model to check if any external worker supports a given model.
  • model_gateway/src/observability/metrics.rs
    • Added new constants for realtime endpoints (ENDPOINT_REALTIME_*) and WebSocket connections (CONNECTION_WEBSOCKET) to support metrics collection.
  • model_gateway/src/routers/mod.rs
    • Introduced route_realtime_rest and route_realtime_ws methods to the RouterTrait with default 'not implemented' stubs.
  • model_gateway/src/routers/openai/realtime/rest.rs
    • Removed all specific handler logic for realtime REST endpoints, retaining only the proxy_response helper function.
  • model_gateway/src/routers/openai/realtime/ws.rs
    • Removed the ws_handler function and related logic, keeping only the RealtimeQueryParams struct and build_upstream_ws_url helper.
  • model_gateway/src/routers/openai/router.rs
    • Updated imports to include WebSocket, WebSocketUpgrade, Query, FromRequestParts, and WorkerLoadGuard.
    • Added realtime_registry to the OpenAIRouter struct for managing realtime sessions.
    • Replaced internal worker selection logic with calls to the new find_best_external_worker and any_external_worker_supports_model methods from WorkerRegistry.
    • Implemented the route_realtime_rest method to handle realtime REST requests, including model extraction, worker selection, authentication, load tracking, metrics recording, and error handling.
    • Implemented the route_realtime_ws method to handle realtime WebSocket upgrade requests, incorporating query parameter parsing, worker selection, authentication, metrics, and proxying to upstream workers.
  • model_gateway/src/server.rs
    • Removed direct imports of realtime_rest and realtime_ws handlers.
    • Added new asynchronous functions (v1_realtime_ws, v1_realtime_session, v1_realtime_client_secret, v1_realtime_transcription_session) to delegate realtime requests to the state.router.
    • Modified build_app to merge realtime routes directly into protected_routes, ensuring they share the same middleware stack (auth, concurrency, WASM).
    • Removed the separate realtime_routes router.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@mergify

This comment was marked as resolved.

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Mar 10, 2026
@CatherineSue
CatherineSue force-pushed the refactor/realtime-router-trait branch from 7a781e5 to 4ef0fc8 Compare March 10, 2026 02:41
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Mar 10, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request centralizes the routing logic for the realtime API by integrating it with the RouterTrait. However, critical security vulnerabilities related to credential leakage in route_realtime_rest and route_realtime_ws have been identified. These are cross-cutting concerns that, per repository guidelines, should ideally be addressed in a dedicated pull request to ensure comprehensive design and review. Additionally, I've identified areas for improvement in error handling: providing more accurate error messages when all workers for a model are unhealthy, and refining request query parsing to return more specific error details to the client. Addressing these points will significantly improve the security, consistency, and robustness of the gateway.

Comment thread model_gateway/src/routers/openai/router.rs Outdated
Comment thread model_gateway/src/routers/openai/router.rs Outdated
Comment thread model_gateway/src/core/worker_registry.rs Outdated
Comment thread model_gateway/src/routers/openai/router.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a781e52cf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/server.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/core/worker_registry.rs`:
- Around line 667-670: The helper any_external_worker_supports_model currently
restricts the scan to healthy-only workers (calls get_workers_filtered(...,
Some(RuntimeType::External), true)), which causes models that exist only on
unhealthy external workers to be treated as missing; update the call in
any_external_worker_supports_model to include unhealthy workers as well by
passing healthy_only = false (i.e., replace the final argument true with false)
so the check returns true if any external worker (healthy or not) supports the
model.

In `@model_gateway/src/routers/openai/router.rs`:
- Around line 1327-1344: The realtime WebSocket upgrade does not hold a
WorkerLoadGuard for the lifetime of the proxied session, so active WS sessions
aren't reflected in load selection and outcomes aren't recorded; create a
WorkerLoadGuard (from WorkerRegistry/WorkerLoadGuard) immediately after
selecting the worker and before calling super::realtime::proxy::run_ws_proxy,
keep the guard alive for the duration of the ws.on_upgrade async block (e.g.,
bind it to a local variable so it isn't dropped) and ensure you call the guard's
outcome recording method (or otherwise record success/failure) depending on
whether run_ws_proxy returns Ok or Err; reference session_id, cancel_token,
upstream_ws_url, auth_str, registry.clone(), and the run_ws_proxy call to locate
where to instantiate and record the WorkerLoadGuard.
- Around line 1163-1187: The code records success/duration from the upstream
resp before awaiting proxy_response(resp).await, so body-read failures (which
make proxy_response return 502) still count as successes; change the order and
logic: first call let response =
super::realtime::rest::proxy_response(resp).await (handling any Err case as an
upstream failure), then determine success from the proxy_response result/status,
and only after that call worker.record_outcome(success) and the appropriate
Metrics::record_router_duration or Metrics::record_router_error; update
references to resp/status to use the proxy_response outcome so circuit breaker
and realtime metrics reflect body-read failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ac05a67-7463-4368-8ac2-3f80f7e9776f

📥 Commits

Reviewing files that changed from the base of the PR and between 1be1e06 and 4ef0fc8.

📒 Files selected for processing (7)
  • model_gateway/src/core/worker_registry.rs
  • model_gateway/src/observability/metrics.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/openai/realtime/rest.rs
  • model_gateway/src/routers/openai/realtime/ws.rs
  • model_gateway/src/routers/openai/router.rs
  • model_gateway/src/server.rs

Comment thread model_gateway/src/core/worker_registry.rs Outdated
Comment thread model_gateway/src/routers/openai/router.rs Outdated
Comment thread model_gateway/src/routers/openai/router.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7659f82c5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/server.rs Outdated
.route("/v1/realtime", get(realtime_ws::ws_handler))
.route("/v1/realtime/sessions", post(realtime_rest::create_session))
// Realtime endpoints (same middleware as other protected routes)
.route("/v1/realtime", get(v1_realtime_ws))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep websocket upgrades out of WASM response rewriting

Placing /v1/realtime under protected_routes subjects websocket upgrades to middleware::wasm_middleware, whose OnResponse path reconstructs a new response from status/headers/body without preserving response extensions (model_gateway/src/middleware.rs OnResponse reconstruction), so the upgrade metadata from WebSocketUpgrade::on_upgrade is dropped. In deployments with enable_wasm=true and any OnResponse module configured, realtime websocket handshakes can no longer complete even though the route returns 101.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Separated a ws route. for now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

cc @slin1237

I wonder if we should fix this in wasm instead. I won't do it in this PR as it is kinda out of scope.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Already addressed in commit c9fad02 — the WebSocket route is excluded from WASM middleware.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/routers/openai/router.rs`:
- Around line 1093-1111: The code that extracts the model (the let model = if
endpoint == "/v1/realtime/client_secrets" { ... } else { ... } block) misses the
nested path used by the realtime transcription sessions endpoint, causing valid
requests to hit the error::bad_request branch; update the model extraction to
special-case "/v1/realtime/transcription_sessions" and read from
body.pointer("/input_audio_transcription/model").and_then(|v| v.as_str()) (or
similar) so that the variable model gets populated for that endpoint before the
match and backstop error::bad_request remains intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 94527e80-c646-48be-a5c5-87209195030a

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef0fc8 and c7659f8.

📒 Files selected for processing (1)
  • model_gateway/src/routers/openai/router.rs

Comment thread model_gateway/src/routers/openai/router.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19da07aa3c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/router_manager.rs Outdated
Comment thread model_gateway/src/routers/openai/router.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
model_gateway/src/routers/openai/router.rs (3)

1224-1237: ⚠️ Potential issue | 🔴 Critical

Don't ignore the transcription model when picking a worker.

The typed request already carries a model under body.audio.input.transcription.model, but this path hardcodes "realtime-transcription". Requests that specify a real transcription model can miss every matching worker and fail as routing errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/openai/router.rs` around lines 1224 - 1237, The
routing currently hardcodes the model key "realtime-transcription" in
route_realtime_transcription_session when calling forward_realtime_rest, which
ignores any model specified on the request at
body.audio.input.transcription.model and can cause mismatched worker selection;
update route_realtime_transcription_session to read
body.audio.input.transcription.model (if present and non-empty) and pass that
model string to forward_realtime_rest (falling back to "realtime-transcription"
only when absent), keeping the same endpoint
"/v1/realtime/transcription_sessions" and
metrics_labels::ENDPOINT_REALTIME_TRANSCRIPTION.

348-372: ⚠️ Potential issue | 🟠 Major

Still recording realtime REST success before proxying the body.

proxy_response(resp).await can still turn a 2xx upstream into a 502 when reading the body fails, but success is captured first and fed into record_outcome() / duration metrics. Those failures will still look healthy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/openai/router.rs` around lines 348 - 372, The code
records success/duration based on resp.status() before proxying, but
proxy_response(resp).await can fail/convert a 2xx into a 502; move the proxying
step before recording outcomes so the final response status (or error) is used:
call proxy_response(resp).await first, then derive success from the proxied
response (or from the error), call worker.record_outcome(success), and only then
invoke Metrics::record_router_duration or Metrics::record_router_error (using
start.elapsed() for duration) so metrics reflect the actual proxied result;
reference proxy_response, worker.record_outcome,
Metrics::record_router_duration, and Metrics::record_router_error to locate and
update the logic.

1277-1293: ⚠️ Potential issue | 🟠 Major

Keep worker/load tracking alive for the whole realtime WebSocket.

Once the 101 response is returned, the selected worker is no longer covered by a WorkerLoadGuard, and the run_ws_proxy() result never feeds back into worker outcomes or router duration metrics. Active sessions stay invisible to load-based selection, and upstream WebSocket failures don't affect health tracking.

Also applies to: 1361-1378

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/openai/router.rs` around lines 1277 - 1293, The
selected worker's load guard currently drops as soon as the 101 response is
returned, so wrap and hold the WorkerLoadGuard for the full lifetime of the
realtime WebSocket: after obtaining the worker via
select_worker_for_model(&model, auth_header.as_ref()).await (the Ok(w) branch),
keep the returned WorkerLoadGuard in a local variable that stays in scope, then
call run_ws_proxy(...) while that guard is alive (do not return the 101 response
immediately). When run_ws_proxy returns, map its Result into the existing worker
outcome and router duration metrics (the same Metrics::record_router_error /
success code paths used elsewhere) so upstream WebSocket failures and session
duration are recorded; ensure run_ws_proxy receives any needed references and
the guard is not moved out of scope prematurely so active sessions remain
visible to load-based selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/protocols/src/realtime_session.rs`:
- Around line 41-48: The validate_session_create_request function currently only
checks req.model.is_none() and allows empty or whitespace-only strings; update
this validation (and the analogous check around lines 478-485) to reject blank
models by trimming and verifying non-empty content (e.g., treat model as invalid
if model.map(|m| m.trim().is_empty()).unwrap_or(true)); return
ValidationError::new("model is required") when the trimmed model is empty or
missing so empty/whitespace-only models produce a 400 instead of routing errors.

---

Duplicate comments:
In `@model_gateway/src/routers/openai/router.rs`:
- Around line 1224-1237: The routing currently hardcodes the model key
"realtime-transcription" in route_realtime_transcription_session when calling
forward_realtime_rest, which ignores any model specified on the request at
body.audio.input.transcription.model and can cause mismatched worker selection;
update route_realtime_transcription_session to read
body.audio.input.transcription.model (if present and non-empty) and pass that
model string to forward_realtime_rest (falling back to "realtime-transcription"
only when absent), keeping the same endpoint
"/v1/realtime/transcription_sessions" and
metrics_labels::ENDPOINT_REALTIME_TRANSCRIPTION.
- Around line 348-372: The code records success/duration based on resp.status()
before proxying, but proxy_response(resp).await can fail/convert a 2xx into a
502; move the proxying step before recording outcomes so the final response
status (or error) is used: call proxy_response(resp).await first, then derive
success from the proxied response (or from the error), call
worker.record_outcome(success), and only then invoke
Metrics::record_router_duration or Metrics::record_router_error (using
start.elapsed() for duration) so metrics reflect the actual proxied result;
reference proxy_response, worker.record_outcome,
Metrics::record_router_duration, and Metrics::record_router_error to locate and
update the logic.
- Around line 1277-1293: The selected worker's load guard currently drops as
soon as the 101 response is returned, so wrap and hold the WorkerLoadGuard for
the full lifetime of the realtime WebSocket: after obtaining the worker via
select_worker_for_model(&model, auth_header.as_ref()).await (the Ok(w) branch),
keep the returned WorkerLoadGuard in a local variable that stays in scope, then
call run_ws_proxy(...) while that guard is alive (do not return the 101 response
immediately). When run_ws_proxy returns, map its Result into the existing worker
outcome and router duration metrics (the same Metrics::record_router_error /
success code paths used elsewhere) so upstream WebSocket failures and session
duration are recorded; ensure run_ws_proxy receives any needed references and
the guard is not moved out of scope prematurely so active sessions remain
visible to load-based selection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9cd8a146-a5bf-4f5f-b05d-bb6a7fece40a

📥 Commits

Reviewing files that changed from the base of the PR and between c7659f8 and 2ae979e.

📒 Files selected for processing (5)
  • crates/protocols/src/realtime_session.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/openai/router.rs
  • model_gateway/src/routers/router_manager.rs
  • model_gateway/src/server.rs

Comment thread crates/protocols/src/realtime_session.rs
Comment thread model_gateway/src/routers/router_manager.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8eb6ee6456

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/router_manager.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
model_gateway/src/routers/openai/router.rs (1)

1357-1374: ⚠️ Potential issue | 🟠 Major

Worker load and outcome tracking missing for WebSocket sessions.

After ws.on_upgrade() returns the 101 response, no WorkerLoadGuard spans the WebSocket session lifetime. This means:

  1. Load blindness: Active realtime sessions are invisible to find_best_external_worker(), potentially causing unbalanced routing to already-busy workers.
  2. Circuit breaker gaps: Neither success nor failure of the upstream WebSocket connection is recorded via worker.record_outcome(), so repeated WS failures won't trip the circuit breaker.

The session is registered with realtime_registry for lifecycle management, but worker load accounting requires a separate mechanism.

🔧 Suggested approach

Create a WorkerLoadGuard before upgrade and pass it into the on_upgrade closure to keep it alive for the session duration. Record outcome based on run_ws_proxy result:

+        let load_guard = WorkerLoadGuard::new(worker.clone(), Some(&parts.headers));
+
         ws.on_upgrade(move |socket: WebSocket| async move {
-            if let Err(e) = run_ws_proxy(
+            let result = run_ws_proxy(
                 socket,
                 &upstream_ws_url,
                 &auth_str,
                 registry.clone(),
                 session_id.clone(),
                 cancel_token,
             )
-            .await
-            {
-                tracing::error!(session_id, error = %e, "Realtime WebSocket proxy error");
-            }
+            .await;
+
+            // Record outcome for circuit breaker
+            let success = result.is_ok();
+            load_guard.worker().record_outcome(success);
+            drop(load_guard); // Explicit drop for clarity
+
+            if let Err(e) = result {
+                tracing::error!(session_id, error = %e, "Realtime WebSocket proxy error");
+            }

             // Cleanup: remove session on disconnect
             registry.remove_session(&session_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/openai/router.rs` around lines 1357 - 1374, The
WebSocket path creates a realtime session but never holds a WorkerLoadGuard for
the session lifetime or records worker outcomes; before calling
ws.on_upgrade(...) create/acquire a WorkerLoadGuard (the same kind used by
find_best_external_worker) and move it into the on_upgrade closure so it lives
for the entire session, then after awaiting run_ws_proxy(...) call
worker.record_outcome(success_or_failure) based on the Result from run_ws_proxy
to update the circuit breaker; also ensure registry.remove_session(&session_id)
remains after the upgrade and that the guard is dropped only when the closure
exits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@model_gateway/src/routers/openai/router.rs`:
- Around line 1357-1374: The WebSocket path creates a realtime session but never
holds a WorkerLoadGuard for the session lifetime or records worker outcomes;
before calling ws.on_upgrade(...) create/acquire a WorkerLoadGuard (the same
kind used by find_best_external_worker) and move it into the on_upgrade closure
so it lives for the entire session, then after awaiting run_ws_proxy(...) call
worker.record_outcome(success_or_failure) based on the Result from run_ws_proxy
to update the circuit breaker; also ensure registry.remove_session(&session_id)
remains after the upgrade and that the guard is dropped only when the closure
exits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9241807a-6f63-40c0-b408-55d06cb3cdf2

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae979e and 8eb6ee6.

📒 Files selected for processing (2)
  • model_gateway/src/routers/openai/router.rs
  • model_gateway/src/server.rs

… realtime metrics labels

Add find_best_external_worker() and any_external_worker_supports_model() as
pure data query methods on WorkerRegistry. These provide reusable worker
selection logic without HTTP dependencies in the core module.

Add metrics label constants for realtime endpoints (sessions, client_secrets,
transcription) and WebSocket connection type.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…endpoints

Realtime API handlers (WebSocket, REST) previously bypassed RouterTrait
entirely, accessing worker_registry directly. This caused realtime traffic
to miss lazy model refresh (spurious 503s), metrics, and WASM middleware.

Add route_realtime_rest() and route_realtime_ws() to RouterTrait with
default not-implemented stubs. Implement both in OpenAIRouter using the
existing select_worker_for_model flow with lazy refresh, metrics recording,
and circuit breaker integration.

Move realtime routes from a separate route group into protected_routes in
server.rs so they share the same middleware stack (auth, concurrency, WASM)
as all other endpoints. Thin handlers in server.rs delegate to
state.router.route_realtime_*() following the standard pattern.

Strip rest.rs and ws.rs down to shared helpers (proxy_response,
build_upstream_ws_url) now that handler logic lives in the router.

Signed-off-by: Chang Su <chang.s.su@oracle.com>

# Conflicts:
#	model_gateway/src/routers/mod.rs
Replace inline super::realtime::* paths with top-level crate:: imports
to satisfy clippy absolute_paths lint and improve readability.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…dpoints

Replace untyped Json<Value> with ValidatedJson<TypedStruct> for realtime
REST handlers, matching the pattern used by all other endpoints (chat,
messages, responses, etc.).

- Add RealtimeClientSecretCreateRequest wrapper type with nested session
- Add Validate + Normalizable impls with model-required validation for
  RealtimeSessionCreateRequest and RealtimeClientSecretCreateRequest
- Split route_realtime_rest into three typed RouterTrait methods:
  route_realtime_session, route_realtime_client_secret,
  route_realtime_transcription_session
- Extract shared forwarding logic into forward_realtime_rest helper
- Add RouterManager delegation for all realtime methods (fixes 501
  fallthrough when state.router is RouterManager)

Signed-off-by: Chang Su <chang.s.su@oracle.com>
WASM OnResponse reconstructs the response from status/headers/body,
dropping response extensions that carry the WebSocket upgrade future.
Move the /v1/realtime WS route to a separate route group with auth
and concurrency middleware but without WASM, preventing broken
handshakes when enable_wasm=true.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Return the actual Axum rejection response instead of a hardcoded
error message, providing more accurate detail on whether the query
string was missing or failed to deserialize.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
@CatherineSue
CatherineSue force-pushed the refactor/realtime-router-trait branch from 8eb6ee6 to 99f6a2e Compare March 10, 2026 05:07

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99f6a2e1e1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/server.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
crates/protocols/src/realtime_session.rs (1)

41-48: ⚠️ Potential issue | 🟡 Minor

Reject blank realtime models during validation.

These schema hooks still only check is_none(), so "" and whitespace-only model values pass ValidatedJson and fail later as routing errors instead of a 400.

🛠️ Suggested fix
 fn validate_session_create_request(
     req: &RealtimeSessionCreateRequest,
 ) -> Result<(), ValidationError> {
-    if req.model.is_none() {
+    let missing_model = req
+        .model
+        .as_deref()
+        .map(str::trim)
+        .filter(|model| !model.is_empty())
+        .is_none();
+    if missing_model {
         return Err(ValidationError::new("model is required"));
     }
     Ok(())
 }
@@
 fn validate_client_secret_create_request(
     req: &RealtimeClientSecretCreateRequest,
 ) -> Result<(), ValidationError> {
-    if req.session.model.is_none() {
+    let missing_model = req
+        .session
+        .model
+        .as_deref()
+        .map(str::trim)
+        .filter(|model| !model.is_empty())
+        .is_none();
+    if missing_model {
         return Err(ValidationError::new("session.model is required"));
     }
     Ok(())
 }

Also applies to: 478-485

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/protocols/src/realtime_session.rs` around lines 41 - 48, The
validate_session_create_request function currently only checks
req.model.is_none(), allowing empty or whitespace-only strings; update it to
treat empty or all-whitespace model values as invalid by checking the string
content (e.g., by calling trim() or equivalent on req.model.as_ref()) and return
Err(ValidationError::new("model is required")) when the model is missing or
blank; apply the same change to the other validation hook noted (the duplicate
at the other validate function location around the 478-485 area) so both reject
empty/whitespace-only model strings.
model_gateway/src/routers/router_manager.rs (1)

755-819: ⚠️ Potential issue | 🟠 Major

Use model-aware router selection for realtime delegation.

In IGW, these four methods still call select_router_for_request(..., None), so realtime traffic is dispatched by the generic default/PD heuristic instead of the session/query model. That can still send /v1/realtime* to routers that only inherit the 501 stub. Pass body.model / body.session.model into selection, parse the WS model query before dispatch, and route transcription sessions to the OpenAI router explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/router_manager.rs` around lines 755 - 819, The four
realtime handlers (route_realtime_session, route_realtime_client_secret,
route_realtime_transcription_session, route_realtime_ws) are calling
select_router_for_request(..., None) and thus ignore the request model; update
each to supply the appropriate model when selecting a router: for
route_realtime_session and route_realtime_client_secret pass body.model (or
body.session.model if present) into select_router_for_request, for
route_realtime_transcription_session parse the model from body.session.model and
explicitly route transcription sessions to the OpenAI router (i.e., choose the
OpenAI router identifier instead of the generic heuristic), and for
route_realtime_ws extract/parse the model from the WS request's query string
before calling select_router_for_request; keep the same NOT_FOUND fallback
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/core/worker_registry.rs`:
- Around line 666-670: Update the docstring for
any_external_worker_supports_model to state explicitly that it only scans
healthy external workers (it calls get_workers_filtered with healthy_only =
true), so callers understand this helper excludes unhealthy workers and that 503
is reserved for healthy-but-circuit-open cases while unhealthy workers may lead
to 404; reference the function name any_external_worker_supports_model and its
call to get_workers_filtered(RuntimeType::External, healthy_only = true) so
maintainers can see the intended behavior.

---

Duplicate comments:
In `@crates/protocols/src/realtime_session.rs`:
- Around line 41-48: The validate_session_create_request function currently only
checks req.model.is_none(), allowing empty or whitespace-only strings; update it
to treat empty or all-whitespace model values as invalid by checking the string
content (e.g., by calling trim() or equivalent on req.model.as_ref()) and return
Err(ValidationError::new("model is required")) when the model is missing or
blank; apply the same change to the other validation hook noted (the duplicate
at the other validate function location around the 478-485 area) so both reject
empty/whitespace-only model strings.

In `@model_gateway/src/routers/router_manager.rs`:
- Around line 755-819: The four realtime handlers (route_realtime_session,
route_realtime_client_secret, route_realtime_transcription_session,
route_realtime_ws) are calling select_router_for_request(..., None) and thus
ignore the request model; update each to supply the appropriate model when
selecting a router: for route_realtime_session and route_realtime_client_secret
pass body.model (or body.session.model if present) into
select_router_for_request, for route_realtime_transcription_session parse the
model from body.session.model and explicitly route transcription sessions to the
OpenAI router (i.e., choose the OpenAI router identifier instead of the generic
heuristic), and for route_realtime_ws extract/parse the model from the WS
request's query string before calling select_router_for_request; keep the same
NOT_FOUND fallback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 54f1669d-bcc6-4437-aa1b-4339236f23e5

📥 Commits

Reviewing files that changed from the base of the PR and between 8eb6ee6 and 99f6a2e.

📒 Files selected for processing (9)
  • crates/protocols/src/realtime_session.rs
  • model_gateway/src/core/worker_registry.rs
  • model_gateway/src/observability/metrics.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/openai/realtime/rest.rs
  • model_gateway/src/routers/openai/realtime/ws.rs
  • model_gateway/src/routers/openai/router.rs
  • model_gateway/src/routers/router_manager.rs
  • model_gateway/src/server.rs

Comment thread model_gateway/src/core/worker_registry.rs Outdated
find_best_external_worker and any_external_worker_supports_model are
only called from OpenAIRouter. Move them back as private methods to
keep WorkerRegistry free of caller-specific logic.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Rename find_best_external_worker -> find_best_worker_for_model and
any_external_worker_supports_model -> any_worker_supports_model to
match the original names from sgl-project/sglang#15611. Place them
right before select_worker_for_model where they originally lived.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Move forward_realtime_rest and route_realtime_ws bodies into free
pub(crate) functions in their respective submodule files, reducing
router.rs by ~250 lines while keeping realtime code colocated with
its helpers. The trait impls in router.rs become thin delegators that
handle worker selection then forward to the extracted functions.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Empty and whitespace-only model strings now fail validation with a 400
instead of passing through and failing later as routing errors.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…ion request

The OpenAI transcription session API has top-level model, language, and
prompt fields that were missing from RealtimeTranscriptionSessionCreateRequest.
This caused the router to use a hardcoded synthetic model id instead of
the client-specified model. Now uses the actual model for worker routing
and validates it is present.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Move WebSocket query param extraction and model validation to server.rs,
matching the pattern used by REST endpoints (ValidatedJson in server.rs).
The route_realtime_ws trait method now takes a model parameter.

RouterManager passes the model to select_router_for_request for all
realtime endpoints (REST and WS), ensuring correct router selection in
IGW deployments with multiple routers.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Move proxy_response and build_upstream_ws_url to the bottom of their
respective files, matching their original positions for easier diff
comparison.

Signed-off-by: Chang Su <chang.s.su@oracle.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42b1672110

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/router.rs
Comment thread model_gateway/src/server.rs
@CatherineSue
CatherineSue merged commit 7962745 into main Mar 10, 2026
59 of 62 checks passed
@CatherineSue
CatherineSue deleted the refactor/realtime-router-trait branch March 10, 2026 18:20
@coderabbitai coderabbitai Bot mentioned this pull request Mar 10, 2026
4 of 6 tasks
smfirmin pushed a commit to smfirmin/smg that referenced this pull request Mar 11, 2026
…t#690)

Signed-off-by: Sydney Firmin <sydney.firmin@oracle.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model-gateway Model gateway crate changes openai OpenAI router changes realtime-api Realtime API related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants