Skip to content

fix(passthrough): virtual selectors advance past ambiguous timeouts - #1176

Merged
seonghobae merged 1 commit into
mainfrom
fix/virtual-selector-ambiguous-timeout-failover
Sep 17, 2026
Merged

seonghobae merged 1 commit into
mainfrom
fix/virtual-selector-ambiguous-timeout-failover

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Defect

Noema review (org CI) calls POST /v1/chat/completions with the virtual model
orchestrator/free. This takes the passthrough path in
TaskOrchestrator.proxy_completion (contextual_orchestrator/orchestrator.py).
When the first candidate raises a TimeoutError (90s read timeout), the
ambiguous-transport-failure branch recorded the failure and immediately raised
a classified 502 provider_connection_error without trying the remaining
ready candidates
— even though the free-pool preflight had three more ready
candidates.

Sidecar log evidence (run 34754423834 attempt 2, PR #1166): one
provider_attempt_failed ... TimeoutError on
nvidia_nim_deepseek_ai_deepseek_v4_pro_0813circuit_failure
request_failed status=502 code=provider_connection_error; ready candidates
nvidia_nim_sub_deepseek_ai_deepseek_v4_pro_0813,
nvidia_nim_meta_llama_3_2_11b_vision_instruct,
nvidia_nim_sub_meta_llama_3_2_11b_vision_instruct were never called.

This behavior was introduced for #1045 (_is_ambiguous_passthrough_transport_failure)
to stop replaying a request whose outcome is unknown (a timeout may follow
provider acceptance). That rationale is valid only for an explicit
concrete model request — the caller pinned one specific provider and nothing
else is safe to substitute. It does not hold for a virtual selector
(no model / orchestrator/auto / orchestrator/free): the caller delegated
candidate selection to the gateway, and _orchestrated_provider_completion's
own docstring already documents that virtual selectors advance across
retryable transport failures (502/429/timeout). The passthrough candidate
loop contradicted that.

Change

In TaskOrchestrator.proxy_completion's passthrough candidate loop
(contextual_orchestrator/orchestrator.py):

  • Compute a virtual_model flag from the same
    requested_model in {None, GATEWAY_DEFAULT_MODEL, AUTO_MODEL, FREE_MODEL}
    check the method already uses earlier to decide whether a request even
    reaches this multi-candidate loop (an explicit concrete model resolves to
    one agent and returns through an earlier single-shot branch — it never
    reaches this loop at all).
  • On an ambiguous transport failure, always record the failure
    (_record_failure + group observe_failure — the breaker learns it
    either way). When virtual_model is true, continue to the next ranked
    candidate instead of raising, preserving last_failure so that if every
    candidate fails, the final error is still the same classified 502 provider_connection_error shape as before.
  • Explicit concrete-model requests are unaffected: they never reach this
    loop, so their fail-closed, single-call behavior is unchanged.
  • Updated the docstrings of _is_ambiguous_passthrough_transport_failure and
    the branch comment to state the explicit-vs-virtual distinction and why.
  • Updated docs/doctoring/provider-diverse-discovery-routing.md, which
    documented the old blanket "ambiguous timeout always fails closed" rule for
    this exact passthrough pool.

Honesty / usage note: the passthrough path records no per-attempt usage rows
today (usage_source/measurement_status bookkeeping lives only in the
orchestrated/conduct workflow path). No new usage-tracking subsystem was
added; the skipped-over candidate's unknown outcome is captured only via the
existing breaker/group-router failure observation, never as a fabricated
zero-cost usage row.

No new numeric caps were added — the loop still iterates the existing ranked,
provider-diverse candidate list with no additional bound on candidates or
time.

Tests

tests/test_passthrough_provider_failover.py:

  • test_ambiguous_timeout_on_explicit_model_is_not_replayed — explicit
    concrete model still fails closed after exactly one call (splits the old
    test_ambiguous_timeout_is_not_replayed, which actually exercised the
    implicit-default virtual selector, not an explicit model).
  • test_ambiguous_timeout_on_virtual_selector_advances_to_next_candidate
    (parametrized: default model, AUTO_MODEL) — first candidate times out,
    second succeeds; response comes from the second; both calls recorded;
    first candidate is a breaker observation.
  • test_ambiguous_timeout_on_free_model_advances_to_next_free_candidate
    same, using FREE_MODEL with admitted cost:free candidates (AGENTS.md
    requires FREE_MODEL coverage, not only AUTO_MODEL).
  • test_ambiguous_timeout_on_virtual_selector_exhausts_to_classified_502
    every candidate times out → classified 502 provider_connection_error,
    every candidate called once, all recorded in the breaker.
  • Updated two pre-existing tests
    (test_ambiguous_transport_failure_is_classified_and_recorded,
    test_ambiguous_transport_failure_is_observed_by_the_group_router) that
    also unknowingly exercised a virtual selector ("contextual-orchestrator"
    / default) and asserted the old fail-closed behavior; they now assert the
    failover.

Verification

  • python -m pytest tests/test_passthrough_provider_failover.py tests/test_provider_reliability.py tests/test_provider_error_taxonomy.py tests/test_api_contract.py -q → 135 passed.
  • python -m interrogate -v contextual_orchestrator/orchestrator.py → 100% (119/119).
  • python -m pytest tests -q (full suite) → see PR thread / report for the summary line; one pre-existing, unrelated failure
    (tests/test_privacy_policy_analysis.py::test_pinned_mcp_client_renders_and_closes_camoufox_tab,
    an mcp.Client attribute mismatch against this environment's installed mcp
    package) reproduces identically on a clean origin/main checkout and is
    unrelated to this change.

Docs / changelog

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 44 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b34de4ff-3a50-4131-a9b6-c326f351ffd5

📥 Commits

Reviewing files that changed from the base of the PR and between 69d92d6 and e687905.

📒 Files selected for processing (5)
  • CHANGELOG.d/virtual-selector-timeout-failover.md
  • contextual_orchestrator/orchestrator.py
  • docs/doctoring/provider-diverse-discovery-routing.md
  • docs/product-technical-gap-baseline.md
  • tests/test_passthrough_provider_failover.py

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

❤️ Share

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

@opencode-agent opencode-agent 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.

Pull request overview

OpenCode reviewed the current-head product diff. Coverage is a separate gate.

Changed files

  • CHANGELOG.d/virtual-selector-timeout-failover.md — repository behavior
  • contextual_orchestrator/orchestrator.py — Python module behavior
  • docs/doctoring/provider-diverse-discovery-routing.md — operator or user guidance
  • docs/product-technical-gap-baseline.md — operator or user guidance
  • tests/test_passthrough_provider_failover.py — regression suite

Changed behavior

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Repository file: virtual-selector-timeout-failover.md"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Repository file: virtual-selector-timeout-failover.md"]
  R1 --> V1["required checks"]
  Evidence --> S2["Python: orchestrator.py"]
  S2 --> I2["Python module behavior"]
  I2 --> R2["Review risk: Python: orchestrator.py"]
  R2 --> V2["pytest plus coverage"]
  Evidence --> S3["Docs: provider-diverse-discovery-routing.md (2 files)"]
  S3 --> I3["operator or user guidance"]
  I3 --> R3["Review risk: Docs: provider-diverse-discovery-routing.md (2 files)"]
  R3 --> V3["docs review"]
  Evidence --> S4["Test: test_passthrough_provider_failover.py"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test: test_passthrough_provider_failover.py"]
  R4 --> V4["targeted test run"]
Loading

Findings

No source-backed product finding is synthesized from the coverage gate. A coverage miss belongs in the status comment.

  • Head SHA: c36d4a5db901a3b4449d136dbeaf7d8e95cee2b3
  • Workflow run: 34758401923
  • Workflow attempt: 1
  • Coverage gate: failure

Review outcome

Coverage is a gate, not the review. This body reviews the changed product files.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Repository file: virtual-selector-timeout-failover.md"]
  S1 --> I1["repository behavior"]
  I1 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V1["required checks"]
  Evidence --> S2["Python: orchestrator.py"]
  S2 --> I2["Python module behavior"]
  I2 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V2["pytest plus coverage"]
  Evidence --> S3["Docs: provider-diverse-discovery-routing.md (2 files)"]
  S3 --> I3["operator or user guidance"]
  I3 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V3["docs review"]
  Evidence --> S4["Test: test_passthrough_provider_failover.py"]
  S4 --> I4["regression suite"]
  I4 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V4["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

Coverage evidence did not pass, so approval is blocked. The formal pull-request review is the source-backed diff review, not this status comment.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Reconciled this branch with origin/main's PR #1053. #1053's rationale (replaying an accepted request can double-bill a priced provider) applies to every selector except orchestrator/free: its candidates are admitted solely on explicit zero-cost evidence, so a replay there can never double-bill.

The candidate loop now compares requested_model directly against FREE_MODEL in the ambiguous-transport-failure branch instead of the broader virtual-selector set. Explicit concrete models and priced virtual selectors (None/GATEWAY_DEFAULT_MODEL/AUTO_MODEL) keep #1053's non-retryable 502 provider_outcome_unknown fail-closed shape unchanged; only orchestrator/free advances past an ambiguous timeout to the next ranked free candidate, and exhausting every free candidate still surfaces the same non-retryable provider_outcome_unknown for the last one tried (never classify_provider_failure's retryable classification).

Tests: kept #1053's parametrized test_ambiguous_timeout_is_not_replayed verbatim, adjusted the explicit-model test to #1053's error shape, deleted the None/AUTO_MODEL advance test (now contradicts main), and added FREE_MODEL-advance and all-free-candidates-exhausted coverage. Focused suite: 202 passed (1 known pre-existing local failure unrelated to this change — test_sdk_passthrough_unknown_outcome_never_replays pins openai SDK 2.54.0, this machine has 2.44.0). interrogate on orchestrator.py: 100%.

Merge commit: a9cb8c6.

🤖 Addressed by Claude Code

@seonghobae

Copy link
Copy Markdown
Contributor Author

CI triage for head c36d4a5d: the four failing checks are the opencode verdict placeholders — COVERAGE_BLOCKED from the org coverage image build (ContextualWisdomLab/.github#2157; repair PR .github#2123 restacked, awaiting org review). The formal opencode review (5190771650) lists no findings; the other comments are Codex/CodeRabbit quota notices. No inline threads. Local: 202 passed on the failover/reliability/timeout-policy suites at this head.

🤖 Addressed by Claude Code

seonghobae added a commit that referenced this pull request Sep 17, 2026
… onto main

Virtual selectors advance past a recorded ambiguous transport failure while
explicit models keep fail-closed provider_outcome_unknown; rebase #1176 onto
current rate-limit admission and taxonomy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@seonghobae
seonghobae force-pushed the fix/virtual-selector-ambiguous-timeout-failover branch from a9cb8c6 to a423456 Compare September 17, 2026 14:45
… onto main

Virtual selectors advance past a recorded ambiguous transport failure while
explicit models keep fail-closed provider_outcome_unknown; rebase #1176 onto
current rate-limit admission and taxonomy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@seonghobae
seonghobae force-pushed the fix/virtual-selector-ambiguous-timeout-failover branch from a423456 to e687905 Compare September 17, 2026 14:49
@seonghobae
seonghobae merged commit ec971c0 into main Sep 17, 2026
16 of 21 checks passed
@seonghobae
seonghobae deleted the fix/virtual-selector-ambiguous-timeout-failover branch September 17, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant