Skip to content

fix(gateway): use URL-based detection to eliminate classify race condition - #839

Merged
slin1237 merged 5 commits into
mainfrom
slin/improve-classify-detection
Mar 21, 2026
Merged

slin1237 merged 5 commits into
mainfrom
slin/improve-classify-detection

Conversation

@slin1237

@slin1237 slin1237 commented Mar 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Fix the race condition in ClassifyWorkerTypeStep where local backends starting up were misclassified as External.

Refs: #820

What changed

Rewrote the auto-detection logic in classify.rs to eliminate the unreliable /v1/models reachability heuristic:

Step Signal Result
1 Any explicit runtime_type Classify immediately — External or Local
2 URL matches known cloud provider (openai.com, anthropic.com, x.ai, googleapis.com) External
3 /health responds Local
4 gRPC health responds Local
5 /v1/models owned_by matches sglang/vllm/trtllm Local
6 Nothing conclusive Default Local

Why

The old heuristic was: /v1/models responds + no /health = External. This caused a race condition — local backends (sglang, vllm) can serve /v1/models before /health is ready during startup, causing them to be misclassified as External. This is the root cause of the issue described in #820.

The new approach uses two safer signals:

  • URL-based detection via ProviderType::from_url() — instant, deterministic, zero race conditions
  • owned_by field from /v1/models response — confirms local backends rather than inferring External

How

  • Removed is_models_endpoint_reachable() (status-code-only check)
  • Added probe_models_owned_by() which parses the /v1/models response body and checks if the first model's owned_by field matches a known local backend
  • Added ProviderType::from_url() check before any probing — known cloud URLs are classified instantly
  • Combined the explicit External and explicit Local branches into a single is_specified() check

Trade-off: External providers on private IPs (e.g., http://10.0.0.1:8080 proxying to OpenAI) must set runtime_type: external explicitly. URL-based detection cannot identify them.

Test plan

  • cargo check -p smg — compiles clean
  • cargo test -p smg — all tests pass (0 failures)
  • cargo clippy -p smg --all-targets --all-features -- -D warnings — no warnings
  • Manual: register a worker at https://api.openai.com with no runtime_type → should classify as External via URL detection
  • Manual: register a local sglang worker with no runtime_type during startup → should classify as Local (not External)

Summary by CodeRabbit

  • Bug Fixes

    • Improved worker classification: explicit runtime settings and URL heuristics more reliably distinguish local vs. cloud workers; model-list probing now uses ownership metadata before marking a backend local; inconclusive probes default to local.
  • Tests

    • Added an end-to-end test validating mixed local and external worker registration, health polling, classification, and model discovery.
  • Chores

    • Added a 4‑GPU E2E workflow job and included it in PR test gating.

@slin1237
slin1237 requested a review from CatherineSue as a code owner March 20, 2026 21:21
@github-actions github-actions Bot added the model-gateway Model gateway crate changes label Mar 20, 2026
@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 robustness of worker classification within the gateway by addressing a critical race condition. The changes introduce a more deterministic and reliable approach to distinguish between local and external worker types, primarily through URL-based detection and explicit owned_by field verification, thereby preventing misclassifications that previously occurred during backend startup.

Highlights

  • Race Condition Fix: Eliminated a race condition in ClassifyWorkerTypeStep that misclassified local backends as External during startup.
  • Improved Classification Logic: Implemented a new, more reliable auto-detection strategy for worker types, prioritizing explicit runtime_type and URL-based detection.
  • URL-based Provider Detection: Introduced a mechanism to instantly classify workers as External if their URL matches known cloud providers (e.g., OpenAI, Anthropic).
  • owned_by Field Utilization: Enhanced local backend detection by parsing the owned_by field from /v1/models responses to confirm local backend types like sglang, vllm, or trtllm.
  • Removed Unreliable Heuristic: Deprecated the /v1/models reachability check, which was prone to misclassification due to server errors during startup.
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.

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. ↩

@coderabbitai

coderabbitai Bot commented Mar 20, 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

Worker classification now respects explicit runtime_type, uses URL heuristics for known cloud providers, probes /v1/models to parse owned_by (recognizing sglang, vllm, trtllm) to mark locals, retains /health and gRPC probes, and falls back to Local when inconclusive.

Changes

Cohort / File(s) Summary
Worker Classification Logic
model_gateway/src/core/steps/worker/classify.rs
Rewrote classification flow: explicit runtime_type is authoritative; unspecified runtimes use ProviderType::from_url for cloud heuristics; replaced boolean /v1/models reachability check with probe_models_owned_by that parses owned_by and classifies Local when it matches sglang, vllm, trtllm; retains /health and gRPC probes; defaults to Local.
End-to-end tests — worker API
e2e_test/router/test_worker_api.py
Added TestIGWMixedWorkerClassification.test_mixed_local_and_external_workers: launches local sglang workers, posts external worker entries to gateway, polls for healthy workers, asserts runtime_type classification by URL and owned_by heuristics, verifies model discovery, includes cleanup and env-key skips; added time and httpx usage.
CI workflows
.github/workflows/pr-test-rust.yml
Added GPU E2E job e2e-4gpu-gateway (sglang, 4-GPU runner) and integrated it into finish job dependencies and failure gating.

Sequence Diagram(s)

sequenceDiagram
  participant Gateway as Gateway
  participant Worker as Worker (endpoint)
  participant Heuristics as URL/Runtime Heuristics
  participant Prober as Prober (/v1/models, /health, gRPC)

  Gateway->>Worker: receive add_worker(config)
  Gateway->>Heuristics: is runtime_type specified?
  alt specified && runtime_type == External
    Heuristics-->>Gateway: classify External
  else specified && runtime_type != External
    Heuristics-->>Gateway: classify Local
  else unspecified
    Heuristics->>Heuristics: ProviderType::from_url(config.url)
    alt URL matches known cloud provider
      Heuristics-->>Gateway: classify External
    else
      Gateway->>Prober: GET /v1/models -> parse owned_by
      Prober-->>Gateway: owned_by or None
      alt owned_by in {sglang, vllm, trtllm}
        Gateway-->>Worker: classify Local
      else
        Gateway->>Prober: probe /health + gRPC health
        alt health OK
          Prober-->>Gateway: health OK -> classify Local
        else
          Gateway-->>Worker: classify Local (fallback)
        end
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • CatherineSue
  • key4ng
  • XinyueZhang369

Poem

🐰
I hopped the gateway, sniffed each URL and key,
Marked clouds as External, locals stayed with me,
I read the models, owners called their name,
sglang, vllm, trtllm — tagged without shame,
A merry hop — classification done with glee 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: fixing a race condition in worker classification via URL-based detection, which is the core objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slin/improve-classify-detection

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

…ition

The classify step previously treated a /v1/models response (without
/health) as proof of an External worker. This caused a race condition:
local backends (sglang, vllm) that serve /v1/models before /health is
ready were misclassified as External.

What changed:
- classify.rs: replace /v1/models reachability check with two safer
  signals: (a) URL-based detection via ProviderType::from_url() for
  known cloud providers (openai.com, anthropic.com, x.ai, googleapis.com)
  and (b) /v1/models owned_by field matching known local backends
  (sglang, vllm, trtllm). Combine the explicit External and explicit
  Local branches into a single is_specified() check.

New detection logic:
  1. Any explicit runtime → classify immediately (External or Local)
  2. URL matches known cloud provider → External
  3. /health responds → Local
  4. gRPC health responds → Local
  5. /v1/models owned_by matches local backend → Local
  6. Nothing conclusive → default Local

Why: the old heuristic (/v1/models responds + no /health = External)
was unreliable during backend startup. URL-based detection is instant,
deterministic, and has zero race conditions. For external providers on
private IPs (e.g., a proxy to OpenAI), users must set runtime: external
explicitly — this is a reasonable requirement since URL detection cannot
identify them.

Refs: #820

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237
slin1237 force-pushed the slin/improve-classify-detection branch from 20a1f62 to bb5af00 Compare March 20, 2026 21:22

@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 effectively addresses a race condition in worker classification by introducing a more robust detection logic. The new approach prioritizes explicit configuration, uses URL-based heuristics for known external providers, and inspects the /v1/models response to identify local backends, which is a significant improvement over the previous, less reliable method. The changes are well-documented and the trade-offs are clearly explained. I have one suggestion to further improve the code's robustness and maintainability.

Comment on lines +52 to +59
let body: serde_json::Value = resp.json().await.ok()?;
let owned_by = body
.get("data")?
.as_array()?
.first()?
.get("owned_by")?
.as_str()?
.to_lowercase();

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.

medium

The current implementation uses serde_json::Value and a chain of ? operators to extract the owned_by field. While this works, it can be brittle and harder to read. Using dedicated structs for deserialization would be more robust, type-safe, and self-documenting, clearly defining the expected structure of the JSON response.

    #[derive(serde::Deserialize)]
    struct Model {
        owned_by: String,
    }
    #[derive(serde::Deserialize)]
    struct ModelList {
        data: Vec<Model>,
    }
    let models: ModelList = resp.json().await.ok()?;
    let owned_by = models.data.first()?.owned_by.to_lowercase();

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

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

// Explicit local runtime (Sglang, Vllm, Trtllm) → Local
if config.runtime_type.is_specified() {
// 3. URL matches known cloud provider → External (no probing needed)
if let Some(provider) = ProviderType::from_url(&config.url) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard provider URL match with domain boundary

Classifying as External purely on ProviderType::from_url(&config.url) here can produce false positives because from_url currently matches hosts with ends_with("openai.com")/similar, so domains like notopenai.com are treated as OpenAI. In this step that means we return early as External and skip /health/gRPC probes, so a local worker on such a hostname is misclassified and routed through the wrong workflow. Please add a strict host/domain-boundary check before using URL-based auto-classification in this branch.

Useful? React with 👍 / 👎.

@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

Caution

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

⚠️ Outside diff range comments (1)
model_gateway/src/core/steps/worker/classify.rs (1)

140-158: 🧹 Nitpick | 🔵 Trivial

/v1/models is now only influencing the debug path.

Lines 141-149 and Lines 152-158 both end in WorkerKind::Local, so this request cannot change the classification anymore. On the slow path it just adds another timeout window and authenticated /v1/models call during registration. Either drop the probe or use it to differentiate a real non-local case.

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

In `@model_gateway/src/core/steps/worker/classify.rs` around lines 140 - 158, The
/v1/models probe (probe_models_owned_by) currently only ever leads to setting
context.data.worker_kind = Some(WorkerKind::Local), duplicating the fallback and
only adding extra latency; remove the probe_models_owned_by call or make it
meaningful by using its result to choose a non-local kind: call
probe_models_owned_by(&config.url, ...) and if it returns Some(owned_by) then
set context.data.worker_kind = Some(WorkerKind::Local) only when owned_by
matches the local identifier (otherwise set context.data.worker_kind =
Some(WorkerKind::Remote) or another appropriate non-local enum variant), and
ensure you return Ok(StepResult::Success) after setting the kind so the
slow-path timeout is not incurred unnecessarily.
🤖 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/classify.rs`:
- Around line 105-111: ProviderType::from_url currently uses host.ends_with(...)
which false-positives lookalike domains; change its matching logic in
crates/protocols/src/worker.rs (the ProviderType::from_url helper) to require
exact host equality or a dot-prefixed suffix (e.g., host == "openai.com" ||
host.ends_with(".openai.com")) for each provider, and update all provider checks
(OpenAI, x.ai, googleapis/generative language) accordingly so subdomains match
but lookalikes like myopenai.com or notgoogleapis.com do not. Also add
regression tests covering the specified boundary cases (api.openai.com → match,
myopenai.com → no match, api.x.ai → match, teamx.ai → no match,
generativelanguage.googleapis.com → match, notgoogleapis.com → no match) to
validate the fix.

---

Outside diff comments:
In `@model_gateway/src/core/steps/worker/classify.rs`:
- Around line 140-158: The /v1/models probe (probe_models_owned_by) currently
only ever leads to setting context.data.worker_kind = Some(WorkerKind::Local),
duplicating the fallback and only adding extra latency; remove the
probe_models_owned_by call or make it meaningful by using its result to choose a
non-local kind: call probe_models_owned_by(&config.url, ...) and if it returns
Some(owned_by) then set context.data.worker_kind = Some(WorkerKind::Local) only
when owned_by matches the local identifier (otherwise set
context.data.worker_kind = Some(WorkerKind::Remote) or another appropriate
non-local enum variant), and ensure you return Ok(StepResult::Success) after
setting the kind so the slow-path timeout is not incurred unnecessarily.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57a64d90-2494-4bdf-be0e-8764a5de36ac

📥 Commits

Reviewing files that changed from the base of the PR and between ccf54bf and bb5af00.

📒 Files selected for processing (1)
  • model_gateway/src/core/steps/worker/classify.rs

Comment on lines +105 to 111
if let Some(provider) = ProviderType::from_url(&config.url) {
debug!(
"Worker {} explicitly configured as {} → Local",
config.url, config.runtime_type
"Worker {} URL matches known provider ({}) → External",
config.url, provider
);
context.data.worker_kind = Some(WorkerKind::Local);
context.data.worker_kind = Some(WorkerKind::External);
return Ok(StepResult::Success);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ProviderType::from_url implementation =="
fd -0 '^worker.rs$' | xargs -0 rg -n -C2 \
  'pub fn from_url|ends_with\("openai\.com"\)|ends_with\("x\.ai"\)|ends_with\("anthropic\.com"\)|ends_with\("googleapis\.com"\)'

echo
echo "== Boundary examples for raw suffix matching =="
python - <<'PY'
examples = [
    ("api.openai.com", "openai.com"),
    ("myopenai.com", "openai.com"),
    ("api.x.ai", "x.ai"),
    ("teamx.ai", "x.ai"),
    ("generativelanguage.googleapis.com", "googleapis.com"),
    ("notgoogleapis.com", "googleapis.com"),
]
for host, suffix in examples:
    print(f"{host:35} endswith({suffix}) = {host.endswith(suffix)}")
PY

Repository: lightseekorg/smg

Length of output: 1629


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check usage of WorkerKind::External after classification =="
rg -n 'WorkerKind::External' --type rust -B2 -A5 | head -60

echo
echo "== Check if there are other validations in the classify function =="
cat -n model_gateway/src/core/steps/worker/classify.rs | head -150

echo
echo "== Look for tests that might cover provider detection =="
fd -0 'test|spec' | xargs -0 rg -l 'ProviderType|from_url' | head -10

Repository: lightseekorg/smg

Length of output: 10119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Look for tests in crates/protocols for ProviderType =="
find . -name '*.rs' -path '*/test*' -o -name '*_test.rs' -o -name 'tests.rs' | xargs grep -l 'ProviderType\|from_url' 2>/dev/null || echo "No test files found"

echo
echo "== Search for any test modules in worker.rs =="
tail -100 crates/protocols/src/worker.rs | grep -A50 '#\[cfg(test)\]' || echo "No test module found in worker.rs"

echo
echo "== Check classify.rs for any unit tests =="
tail -100 model_gateway/src/core/steps/worker/classify.rs | grep -A50 '#\[cfg(test)\]' || echo "No test module found in classify.rs"

echo
echo "== Verify the actual fix needed: show the from_url function in full =="
sed -n '214,226p' crates/protocols/src/worker.rs

Repository: lightseekorg/smg

Length of output: 843


Fix hostname boundary matching in ProviderType::from_url to prevent lookalike domain misclassification.

The provider detection at line 105 relies on ProviderType::from_url, which uses raw host.ends_with() checks. This allows lookalike domains such as myopenai.com, teamx.ai, and notgoogleapis.com to match as legitimate providers. When matched, workers skip all probing steps (health checks, gRPC, model validation) and are immediately classified as WorkerKind::External without further verification.

Update the helper in crates/protocols/src/worker.rs:214-226 to require an exact host match or a proper . boundary (e.g., host == "openai.com" || host.ends_with(".openai.com")), and add regression test cases covering boundary cases:

  • api.openai.com → match (valid subdomain)
  • myopenai.com → no match (lookalike)
  • api.x.ai → match (valid subdomain)
  • teamx.ai → no match (lookalike)
  • generativelanguage.googleapis.com → match (valid subdomain)
  • notgoogleapis.com → no match (lookalike)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/core/steps/worker/classify.rs` around lines 105 - 111,
ProviderType::from_url currently uses host.ends_with(...) which false-positives
lookalike domains; change its matching logic in crates/protocols/src/worker.rs
(the ProviderType::from_url helper) to require exact host equality or a
dot-prefixed suffix (e.g., host == "openai.com" ||
host.ends_with(".openai.com")) for each provider, and update all provider checks
(OpenAI, x.ai, googleapis/generative language) accordingly so subdomains match
but lookalikes like myopenai.com or notgoogleapis.com do not. Also add
regression tests covering the specified boundary cases (api.openai.com → match,
myopenai.com → no match, api.x.ai → match, teamx.ai → no match,
generativelanguage.googleapis.com → match, notgoogleapis.com → no match) to
validate the fix.

Add TestIGWMixedWorkerClassification that exercises the classify step
with a mix of local sglang backends and external cloud providers,
verifying race-condition-free classification.

Test setup (requires 4 GPUs + OPENAI_API_KEY + XAI_API_KEY):
- 2 HTTP sglang workers (Llama-3.1-8B-Instruct)
- 2 gRPC sglang workers (DeepSeek-R1-Distill-Qwen-7B)
- 1 OpenAI external worker (https://api.openai.com)
- 1 xAI external worker (https://api.x.ai)

Gateway starts in IGW mode first, then all workers are added
immediately without waiting for registration — this exercises the
startup race condition that previously caused misclassification.

Assertions:
- All 6 workers register successfully
- Local workers have runtime_type sglang/vllm/trtllm (not external)
- Cloud workers have runtime_type external
- /v1/models returns models from all workers (local + external)

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@github-actions github-actions Bot added the tests Test changes label Mar 20, 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: f6c1362e2a

ℹ️ 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 on lines +105 to 111
if let Some(provider) = ProviderType::from_url(&config.url) {
debug!(
"Worker {} explicitly configured as {} → Local",
config.url, config.runtime_type
"Worker {} URL matches known provider ({}) → External",
config.url, provider
);
context.data.worker_kind = Some(WorkerKind::Local);
context.data.worker_kind = Some(WorkerKind::External);
return Ok(StepResult::Success);

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 unspecified custom endpoints classifiable as External

This change makes Unspecified workers external only when ProviderType::from_url matches a hard-coded host, so any other OpenAI-compatible endpoint (custom domain, self-hosted proxy, third-party provider) now falls through to Local classification and the workflow enters DetectConnectionModeStep, which only probes /health and gRPC for local workers and can fail registration entirely. Before this commit, a reachable /v1/models response was enough to classify these endpoints as external, so this is a behavioral regression for existing callers that rely on default runtime detection.

Useful? React with 👍 / 👎.

Comment thread e2e_test/router/test_worker_api.py 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 `@e2e_test/router/test_worker_api.py`:
- Around line 438-443: The assertions use redundant case checks; simplify by
converting each model string to lower-case once and test for the lowercase
substrings. Update the assertions referencing model_ids so they use a single
case-insensitive check like "llama" in m.lower() and "deepseek" in m.lower()
(remove the redundant 'Llama'/'DeepSeek' checks) to make the intent clearer and
avoid duplicate conditions.
- Around line 411-413: Replace the hard-coded counts with derived counts
computed from the actual worker collections: compute expected_local_count from
the list/criteria that identifies local workers (e.g., count workers with HTTP
or gRPC protocol) and compute expected_total_count by adding OpenAI/xAI entries
or by taking len(workers_list_sources) instead of using 4/6; then update the
assert currently referencing workers and the message string to use these derived
values (keep the asserted object as workers and the message to reflect the
computed expected counts). Ensure you update any helper variables used in the
test (the variables that identify local workers or external workers) so the
assertion remains self-documenting and resilient to changes.
- Around line 386-405: The healthy_local count currently sums all healthy
workers from gateway.list_workers(), which includes external workers added with
disable_health_check=True; update the loop to count only local workers by
filtering workers by their URL or by comparing against a stored set/list of
local worker URLs used when launching them (use gateway.list_workers() and
filter by w.url in local_urls or by a prefix identifying local addresses), then
compute healthy_local = sum(1 for w in workers if w.status == "healthy" and
w.url in local_urls) so the loop waits for 4 healthy local workers rather than
any 4 healthy workers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa2afabd-6e62-4e6d-8623-578da294b65e

📥 Commits

Reviewing files that changed from the base of the PR and between bb5af00 and f6c1362.

📒 Files selected for processing (1)
  • e2e_test/router/test_worker_api.py

Comment thread e2e_test/router/test_worker_api.py Outdated
Comment thread e2e_test/router/test_worker_api.py Outdated
Comment thread e2e_test/router/test_worker_api.py Outdated
Add a 4-GPU CI job that runs gateway tests requiring multiple GPUs.
This enables the TestIGWMixedWorkerClassification test which needs
4 GPUs (2 HTTP + 2 gRPC sglang workers) plus external API keys
(OPENAI_API_KEY, XAI_API_KEY) to verify worker classification.

What changed:
- pr-test-rust.yml: add e2e-4gpu-gateway job using e2e-gpu-job.yml
  reusable workflow with sglang engine, 4-gpu-h100 runner, and
  test filter for gpu(4) tests in e2e_test/router
- pr-test-rust.yml: add e2e-4gpu-gateway to finish job dependencies

Note: the test will be skipped on runners without OPENAI_API_KEY and
XAI_API_KEY environment variables (pytest.skip in the test itself).

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@github-actions github-actions Bot added the ci CI/CD configuration changes label Mar 20, 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: 89ee206c05

ℹ️ 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 .github/workflows/pr-test-rust.yml 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: 1

♻️ Duplicate comments (2)
e2e_test/router/test_worker_api.py (2)

386-407: ⚠️ Potential issue | 🟠 Major

Bug: healthy_local counts all healthy workers, including external ones.

External workers added with disable_health_check=True become immediately healthy. The loop could exit early when 2 external + 2 local workers are healthy (total 4), before all 4 local workers are ready.

🐛 Proposed fix to filter by local worker URLs
+                local_urls = {w.base_url for w in all_local_workers}
+
                 # Wait for local workers to be registered (external are instant with disable_health_check)
                 deadline = time.perf_counter() + 120
                 while time.perf_counter() < deadline:
                     workers = gateway.list_workers()
-                    healthy_local = sum(1 for w in workers if w.status == "healthy")
+                    healthy_local = sum(
+                        1 for w in workers
+                        if w.status == "healthy" and w.url in local_urls
+                    )
                     if healthy_local >= 4:  # 2 HTTP + 2 gRPC
                         break
                     time.sleep(2)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e_test/router/test_worker_api.py` around lines 386 - 407, The loop
currently sets healthy_local = sum(1 for w in workers if w.status == "healthy")
which counts external workers too; change this to only count local workers by
filtering worker URLs (e.g., only include w where w.status == "healthy" and
w.url indicates a local address such as starting with "http://127.0.0.1",
"http://localhost", "grpc://127.0.0.1", etc.) when computing healthy_local and
when formatting the pytest.fail message so the timeout check requires 4 healthy
local workers (use gateway.list_workers() and the w.url filter in both the loop
condition and the failure diagnostic).

438-443: 🧹 Nitpick | 🔵 Trivial

Redundant case-insensitive checks.

"llama" in m.lower() already handles all case variations including "Llama". The or "Llama" in m part is redundant. Same applies to the DeepSeek check.

♻️ Simplified assertions
-                assert any("llama" in m.lower() or "Llama" in m for m in model_ids), (
+                assert any("llama" in m.lower() for m in model_ids), (
                     f"Expected a Llama model from HTTP workers, got: {model_ids}"
                 )
-                assert any("deepseek" in m.lower() or "DeepSeek" in m for m in model_ids), (
+                assert any("deepseek" in m.lower() for m in model_ids), (
                     f"Expected a DeepSeek model from gRPC workers, got: {model_ids}"
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e_test/router/test_worker_api.py` around lines 438 - 443, The assertions in
test_worker_api.py use redundant case checks; simplify them to use a single
case-insensitive test on model_ids by calling m.lower() once. Replace the
expressions checking for Llama and DeepSeek with any("llama" in m.lower() for m
in model_ids) and any("deepseek" in m.lower() for m in model_ids) respectively
so the variable model_ids is checked case-insensitively without the unnecessary
OR branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/pr-test-rust.yml:
- Around line 582-593: The pytest -k filter in the e2e-4gpu-gateway job is
ineffective because "gpu(4)" is not present in test node IDs; update the job's
test_filter (in the e2e-4gpu-gateway job) to either just "-k
'TestIGWMixedWorkerClassification'" or remove test_filter entirely and rely on
the existing E2E_GPU_TIER="4" + pytest_collection_modifyitems hook to select
4-GPU tests, ensuring the filter matches actual test node names rather than
marker arguments.

---

Duplicate comments:
In `@e2e_test/router/test_worker_api.py`:
- Around line 386-407: The loop currently sets healthy_local = sum(1 for w in
workers if w.status == "healthy") which counts external workers too; change this
to only count local workers by filtering worker URLs (e.g., only include w where
w.status == "healthy" and w.url indicates a local address such as starting with
"http://127.0.0.1", "http://localhost", "grpc://127.0.0.1", etc.) when computing
healthy_local and when formatting the pytest.fail message so the timeout check
requires 4 healthy local workers (use gateway.list_workers() and the w.url
filter in both the loop condition and the failure diagnostic).
- Around line 438-443: The assertions in test_worker_api.py use redundant case
checks; simplify them to use a single case-insensitive test on model_ids by
calling m.lower() once. Replace the expressions checking for Llama and DeepSeek
with any("llama" in m.lower() for m in model_ids) and any("deepseek" in
m.lower() for m in model_ids) respectively so the variable model_ids is checked
case-insensitively without the unnecessary OR branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ae1318b0-ccd0-4774-9530-e6a0c1f4cd74

📥 Commits

Reviewing files that changed from the base of the PR and between f6c1362 and 89ee206.

📒 Files selected for processing (2)
  • .github/workflows/pr-test-rust.yml
  • e2e_test/router/test_worker_api.py

Comment thread .github/workflows/pr-test-rust.yml
The -k flag uses Python expressions, not marker syntax.
gpu(4) is not valid — use the class name directly.

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.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: 42f27504b2

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

json={
"url": "https://api.openai.com",
"api_key": openai_key,
"runtime": "external",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove explicit runtime from URL-detection test setup

TestIGWMixedWorkerClassification claims to validate URL-based external classification, but this request sets "runtime": "external", which makes ClassifyWorkerTypeStep short-circuit on explicit runtime and skip URL detection entirely. As a result, this new gated test can pass even if ProviderType::from_url classification is broken, so it does not protect the regression this commit is intended to cover.

Useful? React with 👍 / 👎.

@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/core/steps/worker/classify.rs (1)

104-112: ⚠️ Potential issue | 🟠 Major

Hostname boundary matching issue persists.

The past review comment regarding ProviderType::from_url using raw ends_with() checks (allowing lookalike domains like myopenai.com to match) remains unaddressed. This allows malicious workers to bypass health/gRPC/models probing by using lookalike domains and being immediately classified as External.

The fix must be applied in crates/protocols/src/worker.rs (the ProviderType::from_url implementation) to require exact host match or proper subdomain boundary (e.g., host == "openai.com" || host.ends_with(".openai.com")), with regression tests for boundary cases.

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

In `@model_gateway/src/core/steps/worker/classify.rs` around lines 104 - 112,
ProviderType::from_url currently uses raw ends_with checks causing lookalike
hosts (e.g., myopenai.com) to be treated as provider domains and classify
workers as WorkerKind::External; update the implementation in
crates/protocols/src/worker.rs (ProviderType::from_url) to parse the URL host
and require either host == "openai.com" or host.ends_with(".openai.com") (and do
the same for other provider domains) so only exact hosts or valid subdomains
match, and add regression tests covering boundary cases like "myopenai.com",
"openai.com", "api.openai.com", and "bad.openai.com.evil" to ensure correct
behavior.
🤖 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/core/steps/worker/classify.rs`:
- Around line 104-112: ProviderType::from_url currently uses raw ends_with
checks causing lookalike hosts (e.g., myopenai.com) to be treated as provider
domains and classify workers as WorkerKind::External; update the implementation
in crates/protocols/src/worker.rs (ProviderType::from_url) to parse the URL host
and require either host == "openai.com" or host.ends_with(".openai.com") (and do
the same for other provider domains) so only exact hosts or valid subdomains
match, and add regression tests covering boundary cases like "myopenai.com",
"openai.com", "api.openai.com", and "bad.openai.com.evil" to ensure correct
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b502953b-b5dd-4c18-bd94-72f4382f7133

📥 Commits

Reviewing files that changed from the base of the PR and between 42f2750 and 6ec46ba.

📒 Files selected for processing (1)
  • model_gateway/src/core/steps/worker/classify.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: 6ec46bac45

ℹ️ 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 e2e_test/router/test_worker_api.py Outdated
@slin1237
slin1237 force-pushed the slin/improve-classify-detection branch from 6ec46ba to e3f1c86 Compare March 21, 2026 00:16

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

ℹ️ 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 e2e_test/router/test_worker_api.py Outdated
@slin1237
slin1237 force-pushed the slin/improve-classify-detection branch from e3f1c86 to 269f62d Compare March 21, 2026 00:57

@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: 269f62d410

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

// Explicit local runtime (Sglang, Vllm, Trtllm) → Local
if config.runtime_type.is_specified() {
// 3. URL matches known cloud provider → External (no probing needed)
if let Some(provider) = ProviderType::from_url(&config.url) {

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 Enforce domain-boundary checks before URL provider classification

Fresh evidence in the current tree shows ProviderType::from_url still matches hosts with ends_with(...) (crates/protocols/src/worker.rs:217-224), so this new early-return path can classify domains like notopenai.com as OpenAI and force External before /health/gRPC probes run. In practice, an unspecified local worker on a similarly suffixed hostname is routed down the external branch and can fail registration or be handled by the wrong workflow.

Useful? React with 👍 / 👎.

json={
"url": "https://api.openai.com",
"api_key": openai_key,
"runtime": "external",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove explicit runtime in URL-classification E2E scenario

Fresh evidence: the classifier still short-circuits on explicit runtime (classify.rs:90-102), and this test payload sets "runtime": "external", so the new 4-GPU gate does not actually exercise URL-based auto-classification for cloud workers. That means regressions in the URL detection path can pass this job even though the test is intended to validate that behavior.

Useful? React with 👍 / 👎.

@slin1237
slin1237 force-pushed the slin/improve-classify-detection branch from 269f62d to a49a827 Compare March 21, 2026 01:50
Add TestIGWMixedWorkerClassification that exercises the classify step
with a mix of local sglang backends and external cloud providers,
verifying race-condition-free classification.

Test setup (requires 4 GPUs + OPENAI_API_KEY + XAI_API_KEY):
- 2 HTTP sglang workers (Llama-3.1-8B-Instruct)
- 2 gRPC sglang workers (DeepSeek-R1-Distill-Qwen-7B)
- 1 OpenAI external worker (https://api.openai.com)
- 1 xAI external worker (https://api.x.ai)

Gateway starts in IGW mode first, then all workers are added
immediately without waiting for backends to be ready — this exercises
the startup race condition that previously caused misclassification.

Add wait_ready parameter to Worker.start() and start_workers() to
support spawning backend processes without blocking on health checks.
Defaults to True for backward compatibility.

Assertions:
- All 6 workers register successfully
- Local workers have runtime_type sglang/vllm/trtllm (not external)
- Cloud workers have runtime_type external
- /v1/models returns models from all workers (local + external)

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237
slin1237 force-pushed the slin/improve-classify-detection branch from a49a827 to f74502b Compare March 21, 2026 01:51

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

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

// Explicit local runtime (Sglang, Vllm, Trtllm) → Local
if config.runtime_type.is_specified() {
// 3. URL matches known cloud provider → External (no probing needed)
if let Some(provider) = ProviderType::from_url(&config.url) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize provider URLs before URL-based external detection

ProviderType::from_url(&config.url) now decides the known-provider fast path, but it only matches parseable absolute URLs. If a worker is added with an unspecified runtime and a scheme-less host (for example api.openai.com, which other worker URL helpers in this flow support by adding http://), this branch is skipped and the new logic falls through to the default-Local path instead of External. That can send a real cloud endpoint through local /health/gRPC registration and fail worker onboarding. Please normalize or reparse scheme-less URLs before this provider check.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD configuration changes model-gateway Model gateway crate changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant