Skip to content

feat(interactions): Register Gemini Router - #679

Merged
XinyueZhang369 merged 1 commit into
mainfrom
xz/non-stream-interactions-request
Mar 10, 2026
Merged

XinyueZhang369 merged 1 commit into
mainfrom
xz/non-stream-interactions-request

Conversation

@XinyueZhang369

@XinyueZhang369 XinyueZhang369 commented Mar 9, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Problem

The model gateway currently supports OpenAI and Anthropic as external provider backends, but lacks support for Google's Gemini API. Users who want to route requests through Gemini-compatible endpoints cannot do so.

Solution

Add the routing infrastructure and registration plumbing for Gemini as a new routing mode in the model gateway. This PR wires up the config, CLI, job queue, router factory, server endpoint, and router manager so that the
Gemini router is fully registered and routable. The router implementation stubs (worker selection, request building, upstream execution, response processing) and integration tests will follow in a subsequent PR. Refer to #417 for more details of the gemini router structure

Changes

  • Gemini routing mode: Add RoutingMode::Gemini variant to config types, validation, CLI --backend gemini flag, and job queue worker initialization
  • Job queue refactor: Consolidate duplicate OpenAI/Anthropic worker submission into shared submit_external_worker_jobs helper, add Gemini to the combined match arm
  • Router registration: Register GeminiRouter in the router factory (both single-router and IGW multi-router modes), add HTTP_GEMINI router ID, include in create_all_routers
  • Server endpoint: Add POST /v1/interactions route with ValidatedJson<InteractionsRequest> extractor
  • RouterTrait: Add default route_interactions method (returns 501) so existing routers are unaffected
  • Router manager: Implement route_interactions dispatch and provider-aware routing in get_router_for_model using ProviderType
  • GeminiRouter: Construct from AppContext with SharedComponents (client, worker_registry, mcp_orchestrator, request_timeout), implement RouterTrait::route_interactions
  • Protocol: Implement Normalizable for InteractionsRequest for ValidatedJson extractor compatibility

Test Plan

  • Existing tests pass — no behavioral changes to OpenAI/Anthropic paths
  • cargo build succeeds with new Gemini config variants
  • Integration tests for the Gemini router (mock server, round-trip, error cases, circuit breaker) will be added in the follow-up PR
  • run cargo run -p smg --bin smg -- --backend gemini --worker-urls "https://generativelanguage.googleapis.com" --port 8080 to start smg for gemini backend. The router starts successfully and return 501 not implemented for /v1/interactions request
Screenshot 2026-03-09 at 9 13 41 AM Screenshot 2026-03-09 at 4 16 07 PM
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated

Summary by CodeRabbit

  • New Features

    • Gemini backend added with configurable worker URLs, routing, and HTTP-only backend handling
    • New /v1/interactions endpoint and Interactions API routing support
    • Gemini model discovery via /v1beta/models and provider-specific auth (x-goog-api-key)
    • Worker selection, payload transformation, upstream request/response handling, and metadata patching for Gemini flows
  • Tests

    • Comprehensive end-to-end tests and a Mock Gemini server for interactions testing

@github-actions github-actions Bot added tests Test changes model-gateway Model gateway crate changes gemini Gemini router changes labels Mar 9, 2026
@coderabbitai

coderabbitai Bot commented Mar 9, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request adds Gemini backend routing support to the model gateway. It introduces a new RoutingMode::Gemini configuration option with worker URL management, refactors external worker job submission into a unified path for multiple providers, adds Gemini router factory construction, implements a new route_interactions method across the router trait hierarchy, and wires the Interactions API endpoint through the server layer to support Gemini backend requests.

Changes

Cohort / File(s) Summary
Protocol Types
crates/protocols/src/interactions.rs
Added Normalizable trait implementation for InteractionsRequest with empty body, enabling protocol normalization support.
Configuration System
model_gateway/src/config/builder.rs, model_gateway/src/config/types.rs, model_gateway/src/config/validation.rs
Added new RoutingMode::Gemini { worker_urls } variant and builder method; extended worker_count() and mode_type() helpers; validation allows empty URLs for dynamic worker addition and disallows service discovery for Gemini.
Core Routing Logic
model_gateway/src/job_queue.rs, model_gateway/src/main.rs
Refactored external worker job submission into unified submit_external_worker_jobs() helper handling OpenAI/Anthropic/Gemini; added Backend::Gemini variant to CLI configuration and routing construction logic.
Router Factory & Trait
model_gateway/src/routers/factory.rs, model_gateway/src/routers/mod.rs
Added create_gemini_router() factory method and HTTP_GEMINI router ID; updated factory routing logic to handle Gemini (HTTP-only) and integrated into IGW router list; added default route_interactions() method to RouterTrait.
Gemini Router Implementation
model_gateway/src/routers/gemini/context.rs, model_gateway/src/routers/gemini/router.rs
Added request_timeout field to SharedComponents; refactored GeminiRouter::new() to accept AppContext instead of individual components; replaced route_chat with route_interactions accepting InteractionsRequest parameter; added internal healthy atomic bool.
Router Integration
model_gateway/src/routers/router_manager.rs, model_gateway/src/server.rs
Implemented route_interactions() in RouterManager with provider-specific routing logic for Gemini/Anthropic; added /v1/interactions HTTP endpoint and handler; updated readiness checks for Gemini worker health.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

protocols

Suggested reviewers

  • key4ng
  • slin1237
  • CatherineSue

Poem

🐰 A Gemini backend hops into view,
With routing refined and workers on cue,
Interactions flow through the gateway so bright,
Unified submission makes everything right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(interactions): Register Gemini Router' accurately captures the primary change—adding Gemini router support to the interactions API—as evidenced by router registration across factory, manager, and server files.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch xz/non-stream-interactions-request

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 enhances the model gateway by integrating Google's Gemini API. It establishes a new routing mode that allows the gateway to process non-streaming, non-tool interaction requests for Gemini models. The changes span configuration, worker management, and a dedicated request-response flow, ensuring seamless communication with Gemini-compatible endpoints and laying the groundwork for future streaming and tool-use capabilities.

Highlights

  • Google Gemini API Integration: Added initial support for Google's Gemini API, specifically for non-streaming and no-tool call interactions, expanding the model gateway's backend capabilities.
  • New Routing Mode: Introduced a new RoutingMode::Gemini variant across configuration, validation, and CLI arguments to enable Gemini-specific request handling.
  • Gemini-Specific API Handling: Implemented dedicated logic for Gemini's model discovery endpoint (/v1beta/models), x-goog-api-key authentication, and request/response payload transformations.
  • Dedicated Gemini Router: Developed a GeminiRouter that orchestrates worker selection, request building, upstream execution, and response processing for Gemini interactions.
  • Comprehensive Testing: Included a new suite of end-to-end integration tests with a mock Gemini server to validate the new functionality, covering various success and error scenarios.
Changelog
  • crates/protocols/src/interactions.rs
    • Implemented the Normalizable trait for InteractionsRequest.
  • model_gateway/src/config/builder.rs
    • Added a gemini_mode builder method to configure the router for Gemini.
  • model_gateway/src/config/types.rs
    • Extended RoutingMode enum to include a Gemini variant.
    • Updated num_workers and router_type_name methods to support the new Gemini routing mode.
  • model_gateway/src/config/validation.rs
    • Implemented validation logic for RoutingMode::Gemini worker URLs.
    • Explicitly disallowed service discovery for Gemini routing mode.
  • model_gateway/src/core/job_queue.rs
    • Implemented worker initialization for Gemini routing mode, submitting AddWorker jobs for configured endpoints.
  • model_gateway/src/core/steps/worker/external/discover_models.rs
    • Defined GeminiModelsResponse and GeminiModelInfo structs for parsing Gemini's model list format.
    • Modified fetch_models to use /v1beta/models and x-goog-api-key for Gemini, and to parse its specific model response structure.
  • model_gateway/src/main.rs
    • Integrated Backend::Gemini as a new command-line backend option.
    • Updated CliArgs::to_routing_mode and CliArgs::get_connection_mode to handle the Gemini backend.
  • model_gateway/src/routers/factory.rs
    • Imported GeminiRouter and defined HTTP_GEMINI router ID.
    • Updated RouterFactory::create_router to instantiate GeminiRouter for RoutingMode::Gemini.
    • Implemented create_gemini_router function.
    • Included HTTP_GEMINI in create_all_routers for multi-router setups.
  • model_gateway/src/routers/gemini/context.rs
    • Removed #[expect(dead_code)] attributes from SharedComponents, RequestContext, RequestInput, and ProcessingState structs.
    • Updated the comment for upstream_url in ProcessingState to reflect the /v1beta/interactions path.
  • model_gateway/src/routers/gemini/mod.rs
    • Added mod utils; to export Gemini-specific utility functions.
  • model_gateway/src/routers/gemini/router.rs
    • Refactored GeminiRouter::new to accept Arc<AppContext> and extract necessary components.
    • Implemented the route_interactions method for GeminiRouter as part of the RouterTrait.
  • model_gateway/src/routers/gemini/state.rs
    • Removed #[expect(dead_code)] attributes from LoadPreviousInteraction and ProcessResponse states.
  • model_gateway/src/routers/gemini/steps/non_stream_execution.rs
    • Implemented the core logic for executing non-streaming Gemini interaction requests, including HTTP POST, circuit breaker integration, and error handling.
  • model_gateway/src/routers/gemini/steps/request_building.rs
    • Implemented request payload serialization and applied transformations such as model override and setting store: false.
    • Added transform_payload helper function to modify the upstream request body.
  • model_gateway/src/routers/gemini/steps/response_processing.rs
    • Implemented response processing, including patching metadata like model, agent, store, and previous_interaction_id from the original request.
    • Added patch_response_metadata and is_missing_or_empty helper functions for response manipulation.
  • model_gateway/src/routers/gemini/steps/worker_selection.rs
    • Implemented worker selection logic, including model ID resolution, finding the least-loaded healthy worker, and refreshing external worker models if no suitable worker is found.
  • model_gateway/src/routers/gemini/utils.rs
    • Added a new file utils.rs containing extract_gemini_auth_header for handling Gemini's x-goog-api-key.
  • model_gateway/src/routers/header_utils.rs
    • Modified apply_provider_headers to specifically handle ApiProvider::Gemini by using x-goog-api-key and stripping any 'Bearer ' prefix.
  • model_gateway/src/routers/mod.rs
    • Imported InteractionsRequest from openai_protocol.
    • Extended the RouterTrait with a new route_interactions asynchronous method.
  • model_gateway/src/routers/router_manager.rs
    • Imported InteractionsRequest.
    • Updated RouterManager::get_router_id_for_mode to correctly map Gemini routing modes to HTTP_GEMINI or GRPC_REGULAR.
    • Implemented the route_interactions method for RouterManager to dispatch Gemini interaction requests.
  • model_gateway/src/server.rs
    • Imported InteractionsRequest.
    • Updated the readiness endpoint to include RoutingMode::Gemini in its health checks.
    • Added a new v1_interactions handler for the /v1/interactions endpoint.
    • Registered the /v1/interactions route in the HTTP server.
  • model_gateway/tests/api/interactions_api_test.rs
    • Added a new file interactions_api_test.rs containing end-to-end tests for the Gemini Interactions API, covering various scenarios like non-streaming requests, model/agent handling, response metadata patching, model not found, missing model/agent, auth forwarding, multiple workers, model ID override, and validation.
  • model_gateway/tests/api/mod.rs
    • Included the new interactions_api_test module in the API test suite.
  • model_gateway/tests/common/mock_gemini_server.rs
    • Added a new file mock_gemini_server.rs which implements a mock Gemini API server for testing /v1beta/interactions and /v1beta/models endpoints.
  • model_gateway/tests/common/mod.rs
    • Added pub mod mock_gemini_server; to the common test utilities.
    • Updated create_test_context to register mock Gemini workers when RoutingMode::Gemini is configured.
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. ↩

@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: 64f2a656bf

ℹ️ 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/gemini/steps/request_building.rs Outdated
Comment thread model_gateway/src/routers/router_manager.rs

@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 introduces support for Google's Gemini API as a new routing mode in the model gateway. A high-severity security vulnerability has been identified: the gateway leaks user-supplied provider-specific API keys (e.g., Gemini keys) to other configured external providers (e.g., OpenAI, Anthropic) during model discovery. While the issue is identified here, addressing such cross-cutting security concerns, especially those requiring design decisions, should ideally be handled in a dedicated pull request. Additionally, I've identified several opportunities for improvement related to code duplication, robust error handling (preferring explicit logging over silent failures), and performance optimizations, which are detailed in the specific comments.

Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/core/job_queue.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/routers/gemini/router.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: 8

🤖 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/job_queue.rs`:
- Around line 561-595: The Gemini branch currently builds a generic external
worker config and loses the RoutingMode::Gemini identity, causing discovery
(discover_models.rs) to re-infer provider from config.url; fix by carrying the
routing mode/provider through the worker config/workflow metadata instead of
relying on URL inference: update build_external_worker_config (and its callers
in this branch) to accept and set an explicit provider/routing_mode field
(preserving RoutingMode::Gemini), ensure Job::AddWorker's config includes that
provider flag, and update discover_models.rs to prefer the explicit
provider/routing_mode on the worker config/workflow before falling back to
URL-based heuristics so Gemini proxies/custom hostnames correctly use Gemini
discovery and x-goog-api-key paths.

In `@model_gateway/src/routers/gemini/steps/worker_selection.rs`:
- Around line 154-160: The fragile URL-based check using
worker.url().contains("googleapis.com") should be replaced with the
provider-type check used elsewhere: test for ProviderType::Gemini (e.g.
worker.provider_type() == ProviderType::Gemini or matching worker.provider())
and set models_path to "/v1beta/models" for Gemini and "/v1/models" otherwise
(update the models_path assignment where worker.url() is currently used); also
add the necessary import/qualifier for ProviderType to mirror the logic in
discover_models.rs so proxied/custom domains are handled consistently.

In `@model_gateway/src/routers/gemini/utils.rs`:
- Around line 15-18: The code currently sets user_auth from headers.and_then(|h|
h.get("x-goog-api-key").cloned()) which treats an empty header value as present
and prevents fallback to worker_api_key; change the logic so user_auth only wins
if the header value is non-empty (trimmed), e.g. extract the header string,
check that it's not empty after trimming, and only then convert to HeaderValue;
otherwise let worker_api_key be used via the existing
worker_api_key.and_then(|k| HeaderValue::from_str(k).ok()) fallback. Ensure you
update the expression that builds user_auth (and the final
user_auth.or_else(...) chain) to perform the emptiness check before constructing
the HeaderValue.

In `@model_gateway/src/routers/header_utils.rs`:
- Around line 177-183: The Gemini branch currently calls auth.to_str() and uses
strip_prefix("Bearer ") which is case-sensitive; update the handling in the
ApiProvider::Gemini block so you detect and remove a "Bearer " scheme
case-insensitively before setting x-goog-api-key: e.g. get the auth_str from
auth.to_str(), check auth_str.to_ascii_lowercase().starts_with("bearer ") (or
compare a lowercased slice) and if true slice off the first 7 bytes to produce
api_key (also trim surrounding whitespace), otherwise use auth_str as-is, then
pass that api_key to req.header("x-goog-api-key", api_key) to ensure lowercase
"bearer" is handled correctly.

In `@model_gateway/src/routers/router_manager.rs`:
- Around line 609-631: route_interactions currently selects the model directly
from model_id or body fields and calls select_router_for_request, skipping
IGW-specific resolution; update route_interactions to call resolve_model_id (the
same helper used by other routes) with headers, model_id, and
body.model/body.agent to derive the final selected_model before calling
select_router_for_request, so IGW fallback/validation is applied consistently
and error handling matches other route methods (refer to resolve_model_id,
route_interactions, select_router_for_request, and InteractionsRequest to locate
the relevant code).

In `@model_gateway/tests/api/interactions_api_test.rs`:
- Around line 145-171: The test_interactions_model_not_found currently accepts
503 which masks discovery/refresh failures; change the assertion in
test_interactions_model_not_found to require StatusCode::NOT_FOUND only (remove
the || StatusCode::SERVICE_UNAVAILABLE branch) so a missing-model request routed
via RouterFactory::create_gemini_router and router.route_interactions(...) must
produce 404; if you need to cover refresh/discovery failures, add a separate
test that simulates an unhealthy upstream and asserts SERVICE_UNAVAILABLE.
- Around line 279-309: The test test_interactions_forwards_api_key_header
currently passes because MockGeminiServer::check_auth accepts either
x-goog-api-key or Authorization, so add an explicit assertion that the mock
actually received the Gemini-specific header: update the mock (or the test) to
capture the incoming header name/value in MockGeminiServer::check_auth (or via
new_with_auth) and assert it equals "x-goog-api-key: test-key-123" in
test_interactions_forwards_api_key_header, or alternatively change
MockGeminiServer::check_auth to reject Authorization headers and only accept
x-goog-api-key so the test fails if bearer-only forwarding occurs.

In `@model_gateway/tests/common/mock_gemini_server.rs`:
- Around line 181-207: The mock_models function currently returns an
OpenAI-style payload; update it to validate auth by calling check_auth(State)
and to emit Gemini's payload shape: a JSON object with "models": [ { "name":
"models/..." }, ... ]. Keep the same model identifiers from mock_models
(gemini-2.5-flash, gemini-2.5-pro, deep-research-pro-preview-12-2025) but wrap
them as "models/{id}" in the "name" fields, and return that JSON via
Json(...).into_response() so the mock matches the production discovery contract
checked in discover_models.rs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 19a72d8e-4ee4-496d-9dfb-17c6e37780a6

📥 Commits

Reviewing files that changed from the base of the PR and between 7a8b0e2 and 64f2a65.

📒 Files selected for processing (25)
  • crates/protocols/src/interactions.rs
  • model_gateway/src/config/builder.rs
  • model_gateway/src/config/types.rs
  • model_gateway/src/config/validation.rs
  • model_gateway/src/core/job_queue.rs
  • model_gateway/src/core/steps/worker/external/discover_models.rs
  • model_gateway/src/main.rs
  • model_gateway/src/routers/factory.rs
  • model_gateway/src/routers/gemini/context.rs
  • model_gateway/src/routers/gemini/mod.rs
  • model_gateway/src/routers/gemini/router.rs
  • model_gateway/src/routers/gemini/state.rs
  • model_gateway/src/routers/gemini/steps/non_stream_execution.rs
  • model_gateway/src/routers/gemini/steps/request_building.rs
  • model_gateway/src/routers/gemini/steps/response_processing.rs
  • model_gateway/src/routers/gemini/steps/worker_selection.rs
  • model_gateway/src/routers/gemini/utils.rs
  • model_gateway/src/routers/header_utils.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/router_manager.rs
  • model_gateway/src/server.rs
  • model_gateway/tests/api/interactions_api_test.rs
  • model_gateway/tests/api/mod.rs
  • model_gateway/tests/common/mock_gemini_server.rs
  • model_gateway/tests/common/mod.rs
💤 Files with no reviewable changes (1)
  • model_gateway/src/routers/gemini/state.rs

Comment thread model_gateway/src/core/job_queue.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/routers/gemini/utils.rs Outdated
Comment thread model_gateway/src/routers/header_utils.rs Outdated
Comment thread model_gateway/src/routers/router_manager.rs
Comment thread model_gateway/tests/api/interactions_api_test.rs Outdated
Comment thread model_gateway/tests/api/interactions_api_test.rs Outdated
Comment thread model_gateway/tests/common/mock_gemini_server.rs Outdated
CatherineSue

This comment was marked as outdated.

CatherineSue

This comment was marked as 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: 9

🤖 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/steps/worker/external/discover_models.rs`:
- Around line 232-245: The discovery currently switches models_path and auth
header based on provider derived from the URL (ProviderType::from_url in
execute()), which fails for Gemini behind proxies; instead drive the branch from
the configured routing/provider metadata (e.g., use the configured provider
field such as config.provider or route.provider) rather than the URL-derived
provider. Update the code around models_path/models_url and header selection
(the provider, models_path, models_url, and the req.header(...) branches) to
consult the configured provider metadata and use that to choose "/v1beta/models"
and "x-goog-api-key" for Gemini, leaving other providers to the default
"/v1/models" and Bearer header.

In `@model_gateway/src/routers/gemini/steps/non_stream_execution.rs`:
- Around line 87-105: The code currently calls
worker.circuit_breaker().record_failure() for any non-success HTTP status and
returns internal_error on JSON parse failures; update the logic so that
circuit_breaker.record_failure() is only invoked for upstream 5xx (server)
responses (i.e., check response.status().is_server_error()), while 4xx responses
should not trip the breaker and should return the upstream status and sanitized
body as currently done; also change the JSON parse error branch (where
response.json().await fails and error::internal_error is used) to return a 502
Bad Gateway style error (use the project's bad-gateway helper or return a 502
status with a descriptive message) since parse failures are an upstream contract
error rather than an internal error.

In `@model_gateway/src/routers/gemini/steps/request_building.rs`:
- Around line 83-86: The code in request_building.rs currently inserts "model"
into the serialized request when ctx.input.model_id is Some (see
ctx.input.model_id and obj.insert("model"...)) but does not remove or reject an
existing "agent" field, which can cause a conflicting payload; update the logic
in the request-building block (where obj is modified) to either reject when both
ctx.input.model_id and an "agent" key are present (return an error early) or
normalize by removing obj.remove("agent") before inserting "model" so the path
model wins and only "model" is forwarded.

In `@model_gateway/src/routers/gemini/steps/response_processing.rs`:
- Around line 67-68: The code unconditionally echoes the caller's store flag via
obj.insert("store".to_string(), Value::Bool(req.store)); which advertises
durability the gateway doesn't provide yet; change the logic that sets "store"
so it only returns true when persistence is actually implemented (or when a
runtime/feature flag indicates persistence exists). Concretely, replace the
unconditional insert with a guarded value (e.g. Value::Bool(persistence_enabled
&& req.store) or always Value::Bool(false) until Phase 5 is implemented) in the
same response construction location (the obj.insert call) so responses never
claim storage unless real persistence is present.

In `@model_gateway/src/routers/gemini/steps/worker_selection.rs`:
- Around line 154-160: The current URL substring check (is_gemini =
worker.url().contains("googleapis.com")) is brittle; update the detection logic
used to set is_gemini and models_path to rely on explicit provider/metadata
fields instead of URL matching: read a dedicated field such as worker.provider()
or worker.metadata().get("provider") and treat values like "google" or "gemini"
as Gemini providers, set models_path accordingly ("/v1beta/models" for Gemini
else "/v1/models"), and only fall back to a URL contains check as a last-resort
safeguard; change references to is_gemini and models_path in this file
(worker.url(), is_gemini, models_path) to use the new provider/metadata-based
check.
- Around line 150-165: The HTTP call in refresh_worker_models currently uses
client.get(&url) without an explicit per-request timeout; update the request
built in refresh_worker_models (the req variable created from client.get(&url)
and passed to apply_provider_headers) to include a reasonable
.timeout(Duration::from_secs(...)) so the discovery request fails fast for
slow/unresponsive workers (import std::time::Duration and choose an appropriate
timeout, e.g., 2–5s). Ensure the timeout is applied before sending the request
so apply_provider_headers and subsequent await use the timed request.

In `@model_gateway/src/routers/gemini/utils.rs`:
- Around line 10-18: The extract_gemini_auth_header helper currently only checks
x-goog-api-key and the worker_api_key; update it to mirror header_utils.rs by
also accepting an Authorization: Bearer <token> header as a valid Gemini API key
source. Implement precedence: prefer headers.get("x-goog-api-key") first, then
parse headers.get("authorization") for a Bearer token (strip the "Bearer "
prefix and validate), and finally fall back to worker_api_key; return a
HeaderValue built from the selected token (using
HeaderValue::from_str(...).ok()). Ensure you reference the
extract_gemini_auth_header function and HeaderMap/worker_api_key parameters when
making the change.

In `@model_gateway/tests/common/mock_gemini_server.rs`:
- Around line 181-207: The mock_models handler returns an OpenAI-style schema
and doesn't enforce Gemini auth; update the mock_models function (and its use of
State<Arc<MockServerState>>) to return Gemini's contract: a JSON object with
"models": [{ "name": "models/<model-id>" , ... }] instead of "data", and
require/check the Gemini auth header on requests to /v1beta/models (validate the
Authorization header or call into MockServerState to assert the expected bearer
token) so tests exercise the new parser/header behavior.

In `@model_gateway/tests/common/mod.rs`:
- Around line 412-429: create_test_context() seeds Gemini external workers but
create_test_context_with_parsers() and create_test_context_with_mcp_config() do
not; update those helper functions to mirror the Gemini worker registration
block from the diff: detect RoutingMode::Gemini { worker_urls, .. } and for each
url build the same models vector and Arc<dyn Worker> via
BasicWorkerBuilder::new(url).worker_type(WorkerType::Regular).runtime_type(RuntimeType::External).models(models).build()
then call app_context.worker_registry.register(worker) so the registry is
populated for Gemini in all test-context helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef83b86c-8c3f-42a5-9aac-cb8f1e7a80bf

📥 Commits

Reviewing files that changed from the base of the PR and between 7a8b0e2 and 64f2a65.

📒 Files selected for processing (25)
  • crates/protocols/src/interactions.rs
  • model_gateway/src/config/builder.rs
  • model_gateway/src/config/types.rs
  • model_gateway/src/config/validation.rs
  • model_gateway/src/core/job_queue.rs
  • model_gateway/src/core/steps/worker/external/discover_models.rs
  • model_gateway/src/main.rs
  • model_gateway/src/routers/factory.rs
  • model_gateway/src/routers/gemini/context.rs
  • model_gateway/src/routers/gemini/mod.rs
  • model_gateway/src/routers/gemini/router.rs
  • model_gateway/src/routers/gemini/state.rs
  • model_gateway/src/routers/gemini/steps/non_stream_execution.rs
  • model_gateway/src/routers/gemini/steps/request_building.rs
  • model_gateway/src/routers/gemini/steps/response_processing.rs
  • model_gateway/src/routers/gemini/steps/worker_selection.rs
  • model_gateway/src/routers/gemini/utils.rs
  • model_gateway/src/routers/header_utils.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/router_manager.rs
  • model_gateway/src/server.rs
  • model_gateway/tests/api/interactions_api_test.rs
  • model_gateway/tests/api/mod.rs
  • model_gateway/tests/common/mock_gemini_server.rs
  • model_gateway/tests/common/mod.rs
💤 Files with no reviewable changes (1)
  • model_gateway/src/routers/gemini/state.rs

Comment thread model_gateway/src/core/steps/worker/external/discover_models.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/non_stream_execution.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/request_building.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/response_processing.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/routers/gemini/utils.rs Outdated
Comment thread model_gateway/tests/common/mock_gemini_server.rs Outdated
Comment thread model_gateway/tests/common/mod.rs Outdated

@CatherineSue CatherineSue left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the work on this! Some feedback below.

Size: At 1380+ additions / 25 files, this is above our 1k line guideline. Consider splitting model discovery (GeminiModelsResponse, worker_selection refresh) into a separate PR — perhaps focus on /v1/models support first, then the interactions router steps.

Reference: Please add a reference to PR #417 in the description since this builds directly on that scaffolding.

Lint: Looks like CI lint is currently failing — please fix.

See inline comments for specifics.

Comment thread model_gateway/src/core/steps/worker/external/discover_models.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/main.rs Outdated
Comment thread model_gateway/tests/common/mock_gemini_server.rs Outdated
Comment thread crates/protocols/src/interactions.rs Outdated
Comment thread model_gateway/src/core/steps/worker/external/discover_models.rs Outdated
Comment thread model_gateway/src/routers/gemini/router.rs
Comment thread model_gateway/src/core/steps/worker/external/discover_models.rs
@XinyueZhang369
XinyueZhang369 force-pushed the xz/non-stream-interactions-request branch from 64f2a65 to 7784367 Compare March 9, 2026 17:42

@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: 77843671cd

ℹ️ 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/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/response_processing.rs Outdated
Comment thread model_gateway/src/routers/gemini/steps/worker_selection.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 (12)
model_gateway/tests/common/mod.rs (1)

412-429: ⚠️ Potential issue | 🟠 Major

Mirror this Gemini worker registration in the other test-context helpers.

create_test_context() now seeds Gemini workers, but create_test_context_with_parsers() and create_test_context_with_mcp_config() still only register OpenAI workers. Tests using either helper with RoutingMode::Gemini will start with an empty registry.

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

In `@model_gateway/tests/common/mod.rs` around lines 412 - 429,
create_test_context() seeds Gemini external workers but
create_test_context_with_parsers() and create_test_context_with_mcp_config()
still only register OpenAI workers; update both helpers to mirror the Gemini
worker registration logic: detect RoutingMode::Gemini and for each url build the
same Vec<ModelCard> and Arc<dyn Worker> via
BasicWorkerBuilder::new(url).worker_type(WorkerType::Regular).runtime_type(RuntimeType::External).models(models).build()
and call app_context.worker_registry.register(worker) so the registry is
populated the same way as in create_test_context().
model_gateway/src/routers/gemini/steps/non_stream_execution.rs (2)

87-105: ⚠️ Potential issue | 🟠 Major

Only count upstream/server failures against the circuit breaker.

This currently trips the breaker for every non-2xx, so repeated client 4xxs can evict a healthy Gemini worker. The JSON parse branch is also an upstream contract failure and should surface as 502, not internal_error.

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

In `@model_gateway/src/routers/gemini/steps/non_stream_execution.rs` around lines
87 - 105, Only record circuit breaker failures for upstream/server errors and
treat JSON parse failures as upstream contract failures: change the
response.status() branch so worker.circuit_breaker().record_failure() is called
only when status.is_server_error() (5xx) while still returning non-2xx statuses
to the client; in the response.json().await Err(e) branch treat it as a 502
upstream error (use StatusCode::BAD_GATEWAY) and call
worker.circuit_breaker().record_failure() before returning an error response
instead of using error::internal_error("parse_error", ...), so parsing failures
count against the breaker and client 4xx responses do not.

63-68: ⚠️ Potential issue | 🟠 Major

Don’t let URL sniffing choose Gemini auth headers.

apply_provider_headers() derives the provider from upstream_url, so a Gemini worker behind localhost, a proxy, or a custom domain is treated as Generic and gets Authorization instead of x-goog-api-key. The mock server explicitly tolerates that fallback, so tests still pass, but real proxied Gemini backends will fail auth.

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

In `@model_gateway/src/routers/gemini/steps/non_stream_execution.rs` around lines
63 - 68, apply_provider_headers currently sniffs provider from upstream_url
which causes Gemini workers behind proxies/domains to be treated as Generic and
send Authorization instead of x-goog-api-key; change the call site in
non_stream_execution.rs so provider detection is explicit: after computing
auth_header via extract_gemini_auth_header(ctx.input.headers.as_ref(),
worker.api_key()), call the variant of apply_provider_headers that accepts an
explicit provider hint (or add one) and pass the Gemini/provider-for-api-key
enum so the request built by
ctx.components.client.post(upstream_url).json(&payload) will use the
x-goog-api-key header; if no such API exists add a short overload to
apply_provider_headers to accept a Provider enum (or a flag) and ensure it
prefers the API-key header when auth_header is an API key.
model_gateway/tests/common/mock_gemini_server.rs (1)

181-207: ⚠️ Potential issue | 🟠 Major

Make the mock /v1beta/models endpoint match Gemini’s real contract.

Production discovery expects Gemini auth plus { "models": [{ "name": "models/..." }] }, but this mock still returns OpenAI-style data[].id and skips check_auth(). Any test that exercises refresh/discovery is validating the wrong protocol.

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

In `@model_gateway/tests/common/mock_gemini_server.rs` around lines 181 - 207, The
mock_models handler is returning OpenAI-style data[].id and not enforcing Gemini
auth; update the mock /v1beta/models implementation (mock_models) to call the
existing check_auth() at the start and return the Gemini-shaped JSON: top-level
"models": [ { "name": "models/<model-id>" }, ... ] (e.g.,
"models/gemini-2.5-flash", "models/gemini-2.5-pro",
"models/deep-research-pro-preview-12-2025") instead of data[].id so tests
validate the real Gemini discovery contract.
model_gateway/src/core/steps/worker/external/discover_models.rs (1)

232-245: ⚠️ Potential issue | 🟠 Major

Don’t decide Gemini discovery from the URL alone.

execute() still derives provider from ProviderType::from_url(&config.url), so Gemini backends behind localhost, a proxy, or any custom domain fall through to /v1/models plus Bearer auth. That makes the new /v1beta/models and x-goog-api-key path unusable for proxied Gemini deployments.

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

In `@model_gateway/src/core/steps/worker/external/discover_models.rs` around lines
232 - 245, The code chooses Gemini based solely on
ProviderType::from_url(&config.url) inside execute(), causing proxied/custom
domains to miss the /v1beta/models path and x-goog-api-key header; change the
detection to prefer an explicit provider indicator from the config (e.g., a
config.provider or config.api_type field) when available and only fall back to
ProviderType::from_url(&config.url), then use that resolved provider variable
for models_path and header selection (the logic that sets models_path/models_url
and the header branches that call uses_x_api_key() or check for
ProviderType::Gemini) so proxied Gemini deployments are handled correctly.
model_gateway/src/routers/header_utils.rs (1)

177-183: ⚠️ Potential issue | 🟡 Minor

Normalize the Bearer scheme case-insensitively before setting x-goog-api-key.

strip_prefix("Bearer ") leaves lowercase or mixed-case schemes in the forwarded key, so authorization: bearer ... reaches Gemini as an invalid API key.

Suggested fix
-                    let api_key = auth_str.strip_prefix("Bearer ").unwrap_or(auth_str);
-                    req = req.header("x-goog-api-key", api_key);
+                    let api_key = auth_str
+                        .split_once(' ')
+                        .filter(|(scheme, token)| {
+                            scheme.eq_ignore_ascii_case("bearer") && !token.trim().is_empty()
+                        })
+                        .map(|(_, token)| token.trim())
+                        .unwrap_or(auth_str.trim());
+                    if !api_key.is_empty() {
+                        req = req.header("x-goog-api-key", api_key);
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/header_utils.rs` around lines 177 - 183, In the
ApiProvider::Gemini branch where you read auth_header and compute api_key,
handle the "Bearer " scheme case-insensitively: parse auth.to_str() into tokens
(e.g., splitn on whitespace) and if the first token equals "bearer" ignoring
case, use the second token as api_key; otherwise use the entire auth_str; then
set req.header("x-goog-api-key", api_key). This ensures mixed- or lower-case
"bearer" schemes are normalized before forwarding to Gemini.
model_gateway/src/main.rs (1)

1033-1035: 🛠️ Refactor suggestion | 🟠 Major

Keep all HTTP-only external backends explicit in the connection_mode match.

This branch now special-cases OpenAI and Gemini but still leaves Anthropic to the fallback URL heuristic. It works today, but the external-backend invariant is being encoded here, so Anthropic should be included too.

Suggested fix
-            RoutingMode::OpenAI { .. } | RoutingMode::Gemini { .. } => ConnectionMode::Http,
+            RoutingMode::OpenAI { .. }
+            | RoutingMode::Anthropic { .. }
+            | RoutingMode::Gemini { .. } => ConnectionMode::Http,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/main.rs` around lines 1033 - 1035, The match for computing
connection_mode in main.rs currently special-cases RoutingMode::OpenAI and
RoutingMode::Gemini to ConnectionMode::Http but leaves Anthropic to the fallback
heuristic; update the match expression inside wherever connection_mode is
computed (the match on &mode that returns ConnectionMode::Http or
Self::determine_connection_mode(&all_urls)) to also explicitly match
RoutingMode::Anthropic { .. } => ConnectionMode::Http so Anthropic is treated as
an HTTP-only external backend alongside OpenAI and Gemini.
model_gateway/src/core/job_queue.rs (1)

706-718: ⚠️ Potential issue | 🟠 Major

Carry the Gemini provider in the worker config, not just in log strings.

submit_external_worker_jobs still builds the same generic external WorkerSpec for Gemini as for OpenAI/Anthropic. That means any later provider-specific behavior has to re-infer Gemini from the URL, and this repo's current heuristic only recognizes googleapis.com; Gemini proxies, custom hosts, and localhost workers will miss Gemini discovery/auth handling.

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

In `@model_gateway/src/core/job_queue.rs` around lines 706 - 718,
submit_external_worker_jobs currently constructs a generic external WorkerSpec
via build_external_worker_config without preserving the provider_name, so Gemini
must not be inferred from URL strings; update the code to pass provider_name
into build_external_worker_config (or extend the config/WorkerSpec constructor)
and ensure the returned WorkerSpec includes a provider field set to
provider_name (e.g., "gemini", "openai", "anthropic"); also update any
downstream logic that checks the URL to instead read the WorkerSpec.provider to
drive provider-specific auth/behavior in functions that consume WorkerSpec.
model_gateway/src/routers/gemini/utils.rs (1)

10-18: ⚠️ Potential issue | 🟠 Major

Honor Gemini auth precedence here: non-empty x-goog-api-key → Authorization: Bearer → worker key.

This helper only reads x-goog-api-key, so the same BYOK request can be accepted in model_gateway/src/routers/header_utils.rs but treated as unauthenticated in Gemini worker-selection/request-building paths. It also lets an empty x-goog-api-key suppress the worker-key fallback.

Suggested fix
 pub(crate) fn extract_gemini_auth_header(
     headers: Option<&HeaderMap>,
     worker_api_key: Option<&String>,
 ) -> Option<HeaderValue> {
-    // Passthrough: try user's x-goog-api-key header first
-    let user_auth = headers.and_then(|h| h.get("x-goog-api-key").cloned());
-
-    // Return user's key if provided, otherwise use worker's API key as-is (no Bearer prefix)
-    user_auth.or_else(|| worker_api_key.and_then(|k| HeaderValue::from_str(k).ok()))
+    let user_auth = headers.and_then(|h| {
+        h.get("x-goog-api-key")
+            .and_then(|v| v.to_str().ok().map(str::trim))
+            .filter(|v| !v.is_empty())
+            .map(|v| v.to_owned())
+            .or_else(|| {
+                h.get("authorization")
+                    .and_then(|v| v.to_str().ok())
+                    .and_then(|auth| auth.split_once(' '))
+                    .filter(|(scheme, token)| {
+                        scheme.eq_ignore_ascii_case("bearer") && !token.trim().is_empty()
+                    })
+                    .map(|(_, token)| token.trim().to_owned())
+            })
+    });
+
+    user_auth
+        .and_then(|key| HeaderValue::from_str(&key).ok())
+        .or_else(|| worker_api_key.and_then(|k| HeaderValue::from_str(k).ok()))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/gemini/utils.rs` around lines 10 - 18,
extract_gemini_auth_header currently only reads x-goog-api-key and treats an
empty value as present, which suppresses falling back to Authorization: Bearer
or the worker key; update the function to (1) prefer a non-empty
"x-goog-api-key" header, (2) if that is absent or empty, check the
"authorization" header and use it if it contains a Bearer token, and (3)
otherwise fall back to worker_api_key (as a HeaderValue without adding
"Bearer"). Ensure you test for empty header values when reading
headers.and_then(...).cloned() and parse the "authorization" header string to
accept only "Bearer <token>" before returning it.
model_gateway/tests/api/interactions_api_test.rs (2)

165-170: ⚠️ Potential issue | 🟠 Major

Keep the missing-model assertion strict.

With a healthy mock upstream, this path should be 404 Not Found. Allowing 503 Service Unavailable masks regressions in the miss/refresh flow and lets the wrong failure mode pass.

🧪 Tighten the assertion
-    assert!(
-        response.status() == StatusCode::NOT_FOUND
-            || response.status() == StatusCode::SERVICE_UNAVAILABLE,
-        "Expected 404 or 503, got {}",
-        response.status()
-    );
+    assert_eq!(response.status(), StatusCode::NOT_FOUND);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/tests/api/interactions_api_test.rs` around lines 165 - 170, The
test currently allows either 404 or 503, which hides regressions; update the
assertion in the interactions test to require only StatusCode::NOT_FOUND by
removing the OR branch and asserting response.status() == StatusCode::NOT_FOUND
(and keep the same failure message), so the missing-model path fails if a 503 is
returned; locate the assertion using response.status() and StatusCode::NOT_FOUND
in interactions_api_test.rs and tighten it accordingly.

277-306: ⚠️ Potential issue | 🟠 Major

This still doesn't prove Gemini auth was forwarded as x-goog-api-key.

The test itself notes that localhost takes the generic header path and that the mock accepts both x-goog-api-key and Authorization. A regression back to bearer-only forwarding would still pass. Make the mock capture or reject the header shape so this assertion proves the Gemini contract.

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

In `@model_gateway/tests/api/interactions_api_test.rs` around lines 277 - 306, The
test currently can pass even if only Authorization was forwarded; modify the
test and mock so it verifies the exact header shape: update MockGeminiServer
(constructed via MockGeminiServer::new_with_auth) to record incoming request
headers and fail or return 400 if Authorization bearer is present instead of
x-goog-api-key, then in test_interactions_forwards_api_key_header assert the
mock recorded "x-goog-api-key" == "test-key-123" (and assert that
"authorization" is absent) after calling router.route_interactions;
alternatively add an explicit mock expectation that only x-goog-api-key is
accepted rather than accepting both.
model_gateway/src/routers/gemini/steps/request_building.rs (1)

88-97: ⚠️ Potential issue | 🟠 Major

model_id is overloaded, so agent requests with a path override can still diverge.

At this point ctx.input.model_id can mean either “explicit URL override” or just “the resolved body target”. When an agent request comes through a model-path route, worker selection already routed on model_id, but this transform leaves the payload targeting agent, so the chosen worker and forwarded target can disagree. Split those into separate fields, or reject agent on model-path routes before this step.

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

In `@model_gateway/src/routers/gemini/steps/request_building.rs` around lines 88 -
97, The current logic uses ctx.input.model_id for both “explicit model-path URL
override” and the resolved body target, causing agent requests routed via a
model-path to be left targeting agent while the worker was selected by model_id;
update the code to distinguish these cases: add or use a separate field (e.g.
model_path_override or resolved_model_id) so that when populating the outgoing
body (the obj.insert("model", ...) branch) you only inject "model" when the
model was explicitly intended for the body (resolved_model_id) and not when
model_path_override was used for routing, or alternatively reject requests where
original_request.agent.is_some() while a model-path override is present before
this transform; ensure checks reference ctx.input.model_id (or the new
model_path_override/resolved_model_id), ctx.input.original_request.model, and
ctx.input.original_request.agent to determine the correct 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/routers/gemini/steps/worker_selection.rs`:
- Around line 49-54: The code currently calls
extract_gemini_auth_header(ctx.input.headers.as_ref(), None) which forces no
worker-local API key and causes select_worker (which may invoke
refresh_external_models) to fetch /v1beta/models unauthenticated; change the
call so the second argument supplies the worker-specific API key when available
(derive the per-worker x-goog-api-key from the worker configuration or selection
context and pass it into extract_gemini_auth_header) so select_worker and any
refresh_external_models calls use per-worker auth rather than always None.

In `@model_gateway/src/server.rs`:
- Around line 247-250: The current code sets model_id =
body.model.as_deref().or(body.agent.as_deref()), which incorrectly treats
body.agent as a model id; change this to only use the explicit model field
(e.g., model_id = body.model.as_deref()) and return a 400 Bad Request when model
is missing instead of falling back to body.agent, leaving any future agent→model
resolution to an explicit separate step; update the call to
state.router.route_interactions(...) to receive the validated model_id (or
short-circuit with the 400) so agent identifiers are never passed into
route_interactions as a model id.

---

Duplicate comments:
In `@model_gateway/src/core/job_queue.rs`:
- Around line 706-718: submit_external_worker_jobs currently constructs a
generic external WorkerSpec via build_external_worker_config without preserving
the provider_name, so Gemini must not be inferred from URL strings; update the
code to pass provider_name into build_external_worker_config (or extend the
config/WorkerSpec constructor) and ensure the returned WorkerSpec includes a
provider field set to provider_name (e.g., "gemini", "openai", "anthropic");
also update any downstream logic that checks the URL to instead read the
WorkerSpec.provider to drive provider-specific auth/behavior in functions that
consume WorkerSpec.

In `@model_gateway/src/core/steps/worker/external/discover_models.rs`:
- Around line 232-245: The code chooses Gemini based solely on
ProviderType::from_url(&config.url) inside execute(), causing proxied/custom
domains to miss the /v1beta/models path and x-goog-api-key header; change the
detection to prefer an explicit provider indicator from the config (e.g., a
config.provider or config.api_type field) when available and only fall back to
ProviderType::from_url(&config.url), then use that resolved provider variable
for models_path and header selection (the logic that sets models_path/models_url
and the header branches that call uses_x_api_key() or check for
ProviderType::Gemini) so proxied Gemini deployments are handled correctly.

In `@model_gateway/src/main.rs`:
- Around line 1033-1035: The match for computing connection_mode in main.rs
currently special-cases RoutingMode::OpenAI and RoutingMode::Gemini to
ConnectionMode::Http but leaves Anthropic to the fallback heuristic; update the
match expression inside wherever connection_mode is computed (the match on &mode
that returns ConnectionMode::Http or Self::determine_connection_mode(&all_urls))
to also explicitly match RoutingMode::Anthropic { .. } => ConnectionMode::Http
so Anthropic is treated as an HTTP-only external backend alongside OpenAI and
Gemini.

In `@model_gateway/src/routers/gemini/steps/non_stream_execution.rs`:
- Around line 87-105: Only record circuit breaker failures for upstream/server
errors and treat JSON parse failures as upstream contract failures: change the
response.status() branch so worker.circuit_breaker().record_failure() is called
only when status.is_server_error() (5xx) while still returning non-2xx statuses
to the client; in the response.json().await Err(e) branch treat it as a 502
upstream error (use StatusCode::BAD_GATEWAY) and call
worker.circuit_breaker().record_failure() before returning an error response
instead of using error::internal_error("parse_error", ...), so parsing failures
count against the breaker and client 4xx responses do not.
- Around line 63-68: apply_provider_headers currently sniffs provider from
upstream_url which causes Gemini workers behind proxies/domains to be treated as
Generic and send Authorization instead of x-goog-api-key; change the call site
in non_stream_execution.rs so provider detection is explicit: after computing
auth_header via extract_gemini_auth_header(ctx.input.headers.as_ref(),
worker.api_key()), call the variant of apply_provider_headers that accepts an
explicit provider hint (or add one) and pass the Gemini/provider-for-api-key
enum so the request built by
ctx.components.client.post(upstream_url).json(&payload) will use the
x-goog-api-key header; if no such API exists add a short overload to
apply_provider_headers to accept a Provider enum (or a flag) and ensure it
prefers the API-key header when auth_header is an API key.

In `@model_gateway/src/routers/gemini/steps/request_building.rs`:
- Around line 88-97: The current logic uses ctx.input.model_id for both
“explicit model-path URL override” and the resolved body target, causing agent
requests routed via a model-path to be left targeting agent while the worker was
selected by model_id; update the code to distinguish these cases: add or use a
separate field (e.g. model_path_override or resolved_model_id) so that when
populating the outgoing body (the obj.insert("model", ...) branch) you only
inject "model" when the model was explicitly intended for the body
(resolved_model_id) and not when model_path_override was used for routing, or
alternatively reject requests where original_request.agent.is_some() while a
model-path override is present before this transform; ensure checks reference
ctx.input.model_id (or the new model_path_override/resolved_model_id),
ctx.input.original_request.model, and ctx.input.original_request.agent to
determine the correct behavior.

In `@model_gateway/src/routers/gemini/utils.rs`:
- Around line 10-18: extract_gemini_auth_header currently only reads
x-goog-api-key and treats an empty value as present, which suppresses falling
back to Authorization: Bearer or the worker key; update the function to (1)
prefer a non-empty "x-goog-api-key" header, (2) if that is absent or empty,
check the "authorization" header and use it if it contains a Bearer token, and
(3) otherwise fall back to worker_api_key (as a HeaderValue without adding
"Bearer"). Ensure you test for empty header values when reading
headers.and_then(...).cloned() and parse the "authorization" header string to
accept only "Bearer <token>" before returning it.

In `@model_gateway/src/routers/header_utils.rs`:
- Around line 177-183: In the ApiProvider::Gemini branch where you read
auth_header and compute api_key, handle the "Bearer " scheme case-insensitively:
parse auth.to_str() into tokens (e.g., splitn on whitespace) and if the first
token equals "bearer" ignoring case, use the second token as api_key; otherwise
use the entire auth_str; then set req.header("x-goog-api-key", api_key). This
ensures mixed- or lower-case "bearer" schemes are normalized before forwarding
to Gemini.

In `@model_gateway/tests/api/interactions_api_test.rs`:
- Around line 165-170: The test currently allows either 404 or 503, which hides
regressions; update the assertion in the interactions test to require only
StatusCode::NOT_FOUND by removing the OR branch and asserting response.status()
== StatusCode::NOT_FOUND (and keep the same failure message), so the
missing-model path fails if a 503 is returned; locate the assertion using
response.status() and StatusCode::NOT_FOUND in interactions_api_test.rs and
tighten it accordingly.
- Around line 277-306: The test currently can pass even if only Authorization
was forwarded; modify the test and mock so it verifies the exact header shape:
update MockGeminiServer (constructed via MockGeminiServer::new_with_auth) to
record incoming request headers and fail or return 400 if Authorization bearer
is present instead of x-goog-api-key, then in
test_interactions_forwards_api_key_header assert the mock recorded
"x-goog-api-key" == "test-key-123" (and assert that "authorization" is absent)
after calling router.route_interactions; alternatively add an explicit mock
expectation that only x-goog-api-key is accepted rather than accepting both.

In `@model_gateway/tests/common/mock_gemini_server.rs`:
- Around line 181-207: The mock_models handler is returning OpenAI-style
data[].id and not enforcing Gemini auth; update the mock /v1beta/models
implementation (mock_models) to call the existing check_auth() at the start and
return the Gemini-shaped JSON: top-level "models": [ { "name":
"models/<model-id>" }, ... ] (e.g., "models/gemini-2.5-flash",
"models/gemini-2.5-pro", "models/deep-research-pro-preview-12-2025") instead of
data[].id so tests validate the real Gemini discovery contract.

In `@model_gateway/tests/common/mod.rs`:
- Around line 412-429: create_test_context() seeds Gemini external workers but
create_test_context_with_parsers() and create_test_context_with_mcp_config()
still only register OpenAI workers; update both helpers to mirror the Gemini
worker registration logic: detect RoutingMode::Gemini and for each url build the
same Vec<ModelCard> and Arc<dyn Worker> via
BasicWorkerBuilder::new(url).worker_type(WorkerType::Regular).runtime_type(RuntimeType::External).models(models).build()
and call app_context.worker_registry.register(worker) so the registry is
populated the same way as in create_test_context().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7438bf5-df71-4e4c-b606-1d7c4086f449

📥 Commits

Reviewing files that changed from the base of the PR and between 64f2a65 and 7784367.

📒 Files selected for processing (25)
  • crates/protocols/src/interactions.rs
  • model_gateway/src/config/builder.rs
  • model_gateway/src/config/types.rs
  • model_gateway/src/config/validation.rs
  • model_gateway/src/core/job_queue.rs
  • model_gateway/src/core/steps/worker/external/discover_models.rs
  • model_gateway/src/main.rs
  • model_gateway/src/routers/factory.rs
  • model_gateway/src/routers/gemini/context.rs
  • model_gateway/src/routers/gemini/mod.rs
  • model_gateway/src/routers/gemini/router.rs
  • model_gateway/src/routers/gemini/state.rs
  • model_gateway/src/routers/gemini/steps/non_stream_execution.rs
  • model_gateway/src/routers/gemini/steps/request_building.rs
  • model_gateway/src/routers/gemini/steps/response_processing.rs
  • model_gateway/src/routers/gemini/steps/worker_selection.rs
  • model_gateway/src/routers/gemini/utils.rs
  • model_gateway/src/routers/header_utils.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/router_manager.rs
  • model_gateway/src/server.rs
  • model_gateway/tests/api/interactions_api_test.rs
  • model_gateway/tests/api/mod.rs
  • model_gateway/tests/common/mock_gemini_server.rs
  • model_gateway/tests/common/mod.rs
💤 Files with no reviewable changes (1)
  • model_gateway/src/routers/gemini/state.rs

Comment thread model_gateway/src/routers/gemini/steps/worker_selection.rs Outdated
Comment thread model_gateway/src/server.rs
@mergify

mergify Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Hi @XinyueZhang369, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Mar 9, 2026
Signed-off-by: XinyueZhang369 <zoeyzhang369@gmail.com>
@XinyueZhang369
XinyueZhang369 force-pushed the xz/non-stream-interactions-request branch from 7784367 to 7b0d57b Compare March 9, 2026 23:22
@XinyueZhang369

Copy link
Copy Markdown
Collaborator Author

Thanks for the work on this! Some feedback below.

Size: At 1380+ additions / 25 files, this is above our 1k line guideline. Consider splitting model discovery (GeminiModelsResponse, worker_selection refresh) into a separate PR — perhaps focus on /v1/models support first, then the interactions router steps.

Reference: Please add a reference to PR #417 in the description since this builds directly on that scaffolding.

Lint: Looks like CI lint is currently failing — please fix.

See inline comments for specifics.

Thanks for reviewing!
I removed the the model discovery but still having more than 1000+ changes, so I now only keep the changes needed to register the gemini router with router manager. I'll add tests in followup router step implement PR, but I still did manual e2e test that I attached in the PR description, does this sound good?

@github-actions github-actions Bot removed the tests Test changes label Mar 9, 2026
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Mar 9, 2026

@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: 7b0d57b569

ℹ️ 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

@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 (3)
model_gateway/src/server.rs (1)

242-252: ⚠️ Potential issue | 🟠 Major

Don't treat agent as a model id in the phase-1 interactions path.

body.model.as_deref().or(body.agent.as_deref()) lets agent-only requests route on an agent identifier instead of failing fast for a missing model. That both hides the expected 400 path and can send an invalid id into worker selection.

Suggested fix
-    let model_id = body.model.as_deref().or(body.agent.as_deref());
+    let model_id = body.model.as_deref();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/server.rs` around lines 242 - 252, In v1_interactions,
don't use body.agent as a fallback for model id; change model_id to only use
body.model.as_deref() (remove .or(body.agent.as_deref())), and if model is None
return a 400 error immediately (instead of calling
state.router.route_interactions) so agent-only requests fail fast; update the
logic in v1_interactions to perform this check before calling
state.router.route_interactions.
model_gateway/src/routers/router_manager.rs (1)

614-626: ⚠️ Potential issue | 🟠 Major

Resolve the model before selecting a router in IGW mode.

route_interactions is the only IGW request path here that skips resolve_model_id. When the request omits all model selectors, this falls through to select_router_for_request(headers, None), so single-model deployments won’t infer the only model and multi-model deployments won’t return the expected 400; the request can land on an arbitrary/default router instead.

Proposed fix
     async fn route_interactions(
         &self,
         headers: Option<&HeaderMap>,
         body: &InteractionsRequest,
         model_id: Option<&str>,
     ) -> Response {
         let selected_model = model_id.or(body.model.as_deref()).or(body.agent.as_deref());
-        let router = self.select_router_for_request(headers, selected_model);
+        let effective_model_id = if self.enable_igw {
+            match self.resolve_model_id(selected_model) {
+                Ok(id) => Some(id),
+                Err(err_response) => return *err_response,
+            }
+        } else {
+            None
+        };
+
+        let effective_model = effective_model_id.as_deref().or(selected_model);
+        let router = self.select_router_for_request(headers, effective_model);
 
         if let Some(router) = router {
             router
-                .route_interactions(headers, body, selected_model)
+                .route_interactions(headers, body, effective_model)
                 .await
         } else {
             (
                 StatusCode::NOT_FOUND,
                 "No router available to handle interactions request",
🤖 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 614 - 626,
route_interactions currently calls select_router_for_request with selected_model
possibly None, skipping resolve_model_id; change it to first call
resolve_model_id(headers, body.model.as_deref(), body.agent.as_deref()) (or the
existing resolve_model_id helper) to obtain the effective model id (and
handle/return any error Response it may produce), then pass that resolved model
id into select_router_for_request and into router.route_interactions; update
references to selected_model so they use the resolved id rather than the
original optional chain.
model_gateway/src/core/job_queue.rs (1)

706-718: ⚠️ Potential issue | 🟠 Major

Carry the provider through the AddWorker config, not just the log strings.

provider_name is only used for messages here. The helper still builds a provider-agnostic WorkerSpec, so if the registration/discovery path still infers provider from config.url, Gemini endpoints behind proxies/custom hostnames will regress to the generic/OpenAI path. That also breaks the new provider-aware branch in model_gateway/src/routers/router_manager.rs (Lines 232-236) when provider_for_model() is missing.

Please verify that an explicit provider/routing-mode field is threaded from build_external_worker_config through workflow data into discovery, and that discovery prefers it over URL heuristics. If the only Gemini signal is still URL-based (/v1beta/models, x-goog-api-key, hostname checks), this issue is still present.

#!/bin/bash
set -euo pipefail

echo "== WorkerSpec fields =="
fd 'worker.rs$' . -x rg -n -C3 --type rust 'pub struct WorkerSpec|provider|routing_mode' {}

echo
echo "== External worker config and workflow data =="
rg -n -C4 --type rust 'build_external_worker_config\s*\(|submit_external_worker_jobs\s*\(|create_worker_workflow_data\s*\(' .

echo
echo "== Discovery/provider selection =="
fd 'discover_models.rs$' . -x rg -n -C4 --type rust 'provider|routing_mode|v1beta/models|x-goog-api-key|url' {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/core/job_queue.rs` around lines 706 - 718,
submit_external_worker_jobs currently only passes provider_name into logs while
build_external_worker_config produces a provider-agnostic config, so thread an
explicit provider/routing_mode through the config and into the WorkerSpec and
workflow data creation: update build_external_worker_config to accept/return a
provider or routing_mode field, ensure create_worker_workflow_data copies that
provider/routing_mode into the workflow payload, and make the discovery/provider
selection (used by provider_for_model/router_manager) prefer this explicit field
over URL heuristics (v1beta paths, x-goog-api-key, hostname checks); touch
submit_external_worker_jobs, build_external_worker_config,
create_worker_workflow_data, WorkerSpec, and the discovery/provider_for_model
logic to propagate and prefer the explicit provider.
🤖 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/server.rs`:
- Line 597: The server currently only mounts "/v1/interactions" so path-based
model overrides never reach route_interactions; add a second route for
"/v1/interactions/{model_id}" and implement a handler (e.g.,
v1_interactions_with_model) that extracts the path parameter (model_id) and
forwards it into the same logic used by route_interactions (or call
route_interactions with the extracted model_id) so the route_interactions code
can apply the model-id override; update the route registration to include
post(v1_interactions_with_model) alongside the existing post(v1_interactions).

---

Duplicate comments:
In `@model_gateway/src/core/job_queue.rs`:
- Around line 706-718: submit_external_worker_jobs currently only passes
provider_name into logs while build_external_worker_config produces a
provider-agnostic config, so thread an explicit provider/routing_mode through
the config and into the WorkerSpec and workflow data creation: update
build_external_worker_config to accept/return a provider or routing_mode field,
ensure create_worker_workflow_data copies that provider/routing_mode into the
workflow payload, and make the discovery/provider selection (used by
provider_for_model/router_manager) prefer this explicit field over URL
heuristics (v1beta paths, x-goog-api-key, hostname checks); touch
submit_external_worker_jobs, build_external_worker_config,
create_worker_workflow_data, WorkerSpec, and the discovery/provider_for_model
logic to propagate and prefer the explicit provider.

In `@model_gateway/src/routers/router_manager.rs`:
- Around line 614-626: route_interactions currently calls
select_router_for_request with selected_model possibly None, skipping
resolve_model_id; change it to first call resolve_model_id(headers,
body.model.as_deref(), body.agent.as_deref()) (or the existing resolve_model_id
helper) to obtain the effective model id (and handle/return any error Response
it may produce), then pass that resolved model id into select_router_for_request
and into router.route_interactions; update references to selected_model so they
use the resolved id rather than the original optional chain.

In `@model_gateway/src/server.rs`:
- Around line 242-252: In v1_interactions, don't use body.agent as a fallback
for model id; change model_id to only use body.model.as_deref() (remove
.or(body.agent.as_deref())), and if model is None return a 400 error immediately
(instead of calling state.router.route_interactions) so agent-only requests fail
fast; update the logic in v1_interactions to perform this check before calling
state.router.route_interactions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cbc7362e-1aaa-40af-b2ec-9feb6d6aee61

📥 Commits

Reviewing files that changed from the base of the PR and between 7784367 and 7b0d57b.

📒 Files selected for processing (12)
  • crates/protocols/src/interactions.rs
  • model_gateway/src/config/builder.rs
  • model_gateway/src/config/types.rs
  • model_gateway/src/config/validation.rs
  • model_gateway/src/core/job_queue.rs
  • model_gateway/src/main.rs
  • model_gateway/src/routers/factory.rs
  • model_gateway/src/routers/gemini/context.rs
  • model_gateway/src/routers/gemini/router.rs
  • model_gateway/src/routers/mod.rs
  • model_gateway/src/routers/router_manager.rs
  • model_gateway/src/server.rs

Comment thread model_gateway/src/server.rs
@XinyueZhang369 XinyueZhang369 changed the title feat(interactions): Implement non-stream and no tool call interactions request handling feat(interactions): Register Gemini Router Mar 10, 2026
@XinyueZhang369
XinyueZhang369 merged commit 1be1e06 into main Mar 10, 2026
63 of 65 checks passed
@XinyueZhang369
XinyueZhang369 deleted the xz/non-stream-interactions-request branch March 10, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gemini Gemini router changes model-gateway Model gateway crate changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants