Skip to content

fix(routing): classify primary provider transport failures explicitly - #922

Merged
seonghobae merged 7 commits into
mainfrom
claude/noema-opencode-strix-orchestration-sexqzc
Aug 31, 2026
Merged

fix(routing): classify primary provider transport failures explicitly#922
seonghobae merged 7 commits into
mainfrom
claude/noema-opencode-strix-orchestration-sexqzc

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Investigates and hardens the orchestrator/free/orchestrator/auto request-time reliability gap behind ContextualWisdomLab/.github PR #1433 (the noema-review sidecar's gateway preflight returning an opaque 502 instead of the required review passing).

Root cause, confirmed and refined from the original hypothesis. The hypothesis in the task ("the gateway never retries the next-ranked candidate on a request-time failure") turned out to be mostly already false: TaskOrchestrator._invoke already builds a full ranked candidate list per request (_failover_candidates) and already advances past a 413 and past most 5xx/network failures. But that advancing behavior depended on an implicit, fragile coincidence: the primary chat call's classified ProviderUpstreamError was routed through classify_tool_failure — a classifier designed for tool-execution failures, where invalid_arguments/permission_denied/policy_blocked correctly fail_closed. A plain provider transport failure only avoided that fate because its message text usually didn't contain a tool-fallback keyword. A 500/502/429 whose body happened to also say something like "invalid arguments" (naming an unrelated field, not a tool) would have been misclassified into fail_closed and stopped free/auto failover on a request that never touched a tool — exactly the kind of thing that would surface as an intermittent opaque 502 in CI, matching the evidence.

What changed (contextual-orchestrator):

  1. contextual_orchestrator/tool_fallback.py: new classify_provider_transport_failure(retryable: bool). It classifies a plain, side-effect-free provider/model transport failure directly from the provider taxonomy's own already-computed retryable flag — never from message text — and never returns fail_closed: retryable (429/500/502/503/504/408/network) gets one bounded same-agent retry then sequential failover; non-retryable (401/403/404/422/...) fails over immediately. 413 is unaffected (already handled earlier, unconditionally, by _is_request_too_large_error).
  2. contextual_orchestrator/orchestrator.py: _invoke now uses that classifier for ProviderUpstreamError instead of classify_tool_failure. classify_tool_failure itself, the 413 failover, and the free-tier/priced-tier candidate boundary (orchestrator/free never advances into a priced agent; exhausting every candidate still fails closed with the last classified provider error) are all unchanged.
  3. contextual_orchestrator/model_discovery.py: while reviewing discovery, found that nvidia_nim/nvidia_nim_sub's entire orchestrator/free coverage depends on one shared, unauthenticated, third-party Models.dev fetch (ADR 0041, merged onto main today) that had exactly one attempt against an endpoint already documented in this same file as intermittently rejecting urllib's default user agent. _fetch_models_dev_metadata now retries that one fetch with a small bounded budget before degrading to the existing "no evidence, not free" behavior — a lone transient blip in a service this gateway doesn't control no longer has to erase that coverage for a whole discovery run.
  4. Docs: amended docs/adr/0001-tool-execution-fallback-policy.md and docs/planning/adrs/0041-generalize-models-dev-cost-classification.md (both explicitly stated intents this now fulfills more robustly), plus a pointer in docs/architecture.md, and a CHANGELOG.md entry.

Explicitly out of scope / flagged, not fixed here: a second, later symptom the coordinator observed mid-task — "sidecar exited before healthz" from a ReviewPreflightError — was traced to ContextualWisdomLab/.github's own contextual_orchestrator_review_launcher.py (_preflight_review_agents, a sequential single-shot probe loop over the composed catalog with max_retries=0), which lives entirely in the .github repo. Verified in-scope here: discover_all_models already isolates one provider's discovery failure from the others (untouched, already correct), and the launcher's own zero-eligible-route path already fails closed with a distinct, typed message. No .github files were touched, per the task's instructions.

Not needed / considered and rejected: proactively excluding a free model with a small context/request-size limit from ranking (task point 4's first option) — the existing unconditional 413→failover (its second option) already covers this, so adding size-aware ranking would be new, untested scope for no behavioral gain right now.

Follow-up: adversarial re-review findings (addressed)

A separate, dedicated adversarial root-cause pass identified two more _invoke exception carve-outs shaped like the bug this PR fixes, plus one discovery-quality issue. All three were investigated on this branch; here's the disposition of each:

  1. ProviderResponseError (HTTP 200 with empty/reasoning-only content, surfaced as invalid_structured_output/502) — same immediate-raise-bypasses-failover shape as the ProviderUpstreamError bug this PR fixes. Verified already resolved, independently, by main's PR fix(api): accept the advertised gateway-default model on chat surfaces #868 (which this branch is rebased onto, commit 913bb986): _invoke now advances to the next candidate whenever allowed_agent_ids is not None — true for every orchestrator/free/orchestrator/auto call — and only fails closed once every candidate in the pool has structurally failed (bounded_provider_response_failures == len(candidates)). No code change needed.
  2. ToolFallbackStoppedError (raised by the provider's own explicit terminal tool_execution_stopped signal, or by classify_tool_failure resolving FAIL_CLOSED) — confirmed still an unconditional raise in _invoke with zero failover, on both the original pinned commit and current main. Investigated whether to port the same "advance while candidates remain, fail closed on exhaustion" pattern here, and deliberately did not: every path that raises this exception resolves to ambiguous_outcome, permission_denied, policy_blocked, or invalid_arguments. ADR 0001 states as an explicit invariant that permission/policy failures must never fall through to another agent, and that non-idempotent timeout/transport uncertainty must never replay automatically; for an ambiguous server-side outcome specifically, asking a different agent doesn't resolve the ambiguity about whether the first one's action already took effect — the risk is about the task's state, not the specific agent. Converting this to failover needs an explicit product decision about which failure kinds that's actually safe for (if any); it is not a mechanical port of the fix in this PR. Added an inline comment at the raise (new commit) explaining this is a deliberate boundary, not an oversight, since it's now the second time this exact line has been flagged as suspicious. Left unimplemented pending product sign-off.
  3. nvidia_nim/nvidia_nim_sub admitting retired/404 model IDs into the free catalog — plausible contributor to a thin catalog (reproduction reportedly saw 4/4 free candidates rejected, including 404s on retired models). A live /v1/models-style cross-check before admitting a discovered row (mirroring a pattern used elsewhere in .github's tooling) would help, but it's a new discovery-time network dependency with its own failure-mode design questions (fail-open vs. fail-closed on the cross-check's own failure, added latency/rate-limit exposure per discovery run, whether it generalizes past NIM) — not a small addition, and risks shrinking the catalog further if the cross-check itself is unreliable. Flagging for a separate, dedicated PR rather than folding into this one.

One correction carried over from the adversarial pass for the record: the request_failed status=413 code=request_too_large log line in the original CI evidence is confirmed unrelated — it's the sidecar shell script's own deliberate self-test artifact, not a real failure. No code here was built around that line as evidence.

Test plan

  • contextual_orchestrator/tool_fallback.py: new unit tests for classify_provider_transport_failure (never fail-closed, boolean type-check).
  • tests/test_provider_reliability.py: orchestrator/free advances through a 3-member free pool on 502/503 without ever calling a higher-priority priced agent in the pool; exhausting every free candidate fails closed with the last classified error and never calls the priced agent; a 500 whose body incidentally says "invalid arguments"/mentions tool_choice still fails over (regression guard for the exact fragility this PR removes); orchestrator/auto's existing failover behavior is unchanged.
  • tests/test_model_discovery.py: the shared Models.dev fetch retries a transient failure and recovers full free-tier evidence; exhausting the retry budget still degrades to is_free=False (not a crash), matching the ADR 0041 cost-safety argument.
  • Full local suite: python -m pytest tests -q2776 passed, 1 skipped, 1 deselected (the 1 deselected is the pre-existing, already-documented fast-mlsirm sandbox-unreachable-dependency gap noted in this repo's own gap baseline; unrelated to this change), re-verified clean after the fix(api): accept the advertised gateway-default model on chat surfaces #868 rebase merge. python -m pytest tests/fuzz -q → 14 passed. python tests/test_self_check.py / test_paper_contracts.py / test_conventions.py → all pass. Targeted subset re-run after the follow-up (comment-only) commit → 235 passed.
  • interrogate on the touched modules → 100%.
  • pip-audit --requirement requirements.lock --strict → no known vulnerabilities (no dependency changes in this PR).
  • Trivy filesystem scan could not be run in this sandbox (no installable binary reachable without attaching an unrelated public repo); no new dependencies were introduced, so the org-central Security workflow is expected to pass unaffected — please confirm on CI.
  • Required org-central checks (CodeQL, OpenCode review, etc.) — pending on this PR.

Rebased onto the fix/gateway-default-chat-model (#868) merge that landed on main mid-task; that PR independently touched the same _invoke exception-handling block (a new bounded ProviderResponseError failover for virtual pools — see follow-up section above) and the same model_discovery.py region (SSL/certifi retry, configured-gateway discovery, OpenRouter ZDR/privacy metadata) — merged cleanly, both behaviors preserved, full suite green afterward.

Once this lands on main (merged, not just opened), the vendored ORCHESTRATOR_PIN_SHA in ContextualWisdomLab/.github's scripts/ci/contextual_orchestrator_review_sidecar.sh needs a follow-up bump in a separate PR there — intentionally not done in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Summary by CodeRabbit

  • 새로운 기능

    • 일시적인 제공자 오류(429, 5xx, 네트워크 오류) 발생 시 동일 모델 재시도 후 적합한 후보로 자동 전환합니다.
    • 재시도할 수 없는 오류는 즉시 다음 후보로 전환합니다.
    • 무료 모델 후보가 모두 실패해도 유료 모델로 자동 승격되지 않습니다.
    • 모델 카탈로그 조회 시 일시적 오류를 최대 3회 재시도합니다.
  • 버그 수정

    • 오류 메시지 내용에 따라 제공자 오류가 잘못 차단되는 문제를 수정했습니다.
  • 문서

    • 오류 재시도 및 대체 모델 전환 정책을 관련 문서에 반영했습니다.

claude added 2 commits August 30, 2026 10:36
TaskOrchestrator._invoke's route/Conduct primary chat call is a bounded,
side-effect-free model completion request, not a tool invocation, so it
can never produce the ambiguous-outcome risk classify_tool_failure's
fail-closed rows exist to guard against. It was still classified through
that tool-execution-oriented, message-text heuristic: a generic
ProviderUpstreamError (5xx/429/network) usually fell through to the
`unknown` catch-all and correctly kept failing over, but only
incidentally -- an upstream error body that happened to also say
something like "invalid arguments" (naming an unrelated field, not a
tool) could be misclassified into invalid_arguments/permission/policy and
stop orchestrator/free and orchestrator/auto request-time failover on a
request that never touched a tool.

tool_fallback.classify_provider_transport_failure(retryable: bool) now
classifies this specific call directly from the provider taxonomy's own
already-computed retryable flag, never from message text, and never
returns fail_closed: retryable failures get one bounded same-agent retry
then sequential failover; non-retryable failures fail over immediately.
classify_tool_failure itself, the existing 413 request-too-large
failover, and the free-tier/priced-tier candidate-set boundary in
_failover_candidates (orchestrator/free never advances into a priced
agent; exhausting every candidate still fails closed with the last
classified provider error) are all unchanged.

Also hardens the shared Models.dev fetch discover_all_models depends on
for nvidia_nim/nvidia_nim_sub free-tier classification (ADR 0041): it had
exactly one attempt against an unauthenticated third-party endpoint
already observed to reject urllib's default user agent, so one transient
blip could erase every dependent provider's orchestrator/free coverage
for a whole discovery run. _fetch_models_dev_metadata now retries with a
small bounded budget before degrading to the existing "no evidence, not
free" fail-closed behavior.

Motivated by the orchestrator/free review-sidecar reliability gap in
ContextualWisdomLab/.github PR #1433 (majority of noema-review runs
failing with an opaque 502 from the gateway preflight).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
…-strix-orchestration-sexqzc

# Conflicts:
#	CHANGELOG.md
#	contextual_orchestrator/model_discovery.py
#	contextual_orchestrator/orchestrator.py
#	tests/test_model_discovery.py
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 47 minutes.

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: Pro Plus

Run ID: 21830c6f-049b-4420-973e-ad9e8ccc9606

📥 Commits

Reviewing files that changed from the base of the PR and between 5d60b92 and 3fea0af.

📒 Files selected for processing (2)
  • docs/architecture.md
  • docs/planning/adrs/0041-generalize-models-dev-cost-classification.md
📝 Walkthrough

Walkthrough

Provider 전송 오류 분류를 retryable 플래그 기반으로 변경했습니다. Free 및 auto 경로의 failover 테스트를 추가했습니다. Models.dev 조회에 제한된 재시도를 추가했습니다. 관련 ADR, 아키텍처 문서, 변경 로그를 갱신했습니다.

Changes

Provider 장애 조치 분류

Layer / File(s) Summary
Provider 전송 오류 분류 계약
contextual_orchestrator/tool_fallback.py, tests/test_tool_execution_fallback.py
classify_provider_transport_failureretryable 값에 따라 재시도 또는 failover 결정을 반환합니다. 이 함수는 FAIL_CLOSED를 반환하지 않으며, 잘못된 타입을 거부합니다.
Orchestrator 재시도 및 failover 통합
contextual_orchestrator/orchestrator.py, tests/test_provider_reliability.py, docs/adr/0001-tool-execution-fallback-policy.md, docs/architecture.md, CHANGELOG.md
TaskOrchestrator._invokeProviderUpstreamError를 새 분류기로 처리합니다. Free 경로는 priced agent로 이동하지 않으며, auto 경로는 선언된 모델 그룹 안에서 failover합니다. 관련 정책 문서와 변경 로그를 갱신했습니다.

Models.dev 메타데이터 재시도

Layer / File(s) Summary
Models.dev 조회 재시도 통합
contextual_orchestrator/model_discovery.py, tests/test_model_discovery.py, docs/planning/adrs/0041-generalize-models-dev-cost-classification.md
Models.dev 조회가 최대 세 번 재시도합니다. 모든 시도가 실패하면 None으로 처리합니다. 일시적 실패 복구와 재시도 예산 소진을 테스트합니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: 🔵 Low · up to 5d60b

This change improves provider failover and metadata discovery resilience without promoting free routes to paid providers or expanding credential exposure. It is mergeable with explicit owner awareness for bounded duplicate provider calls during compound failures, less predictable mixed-error reporting, and two minor documentation corrections.

Sequence Diagram(s)

sequenceDiagram
  participant TaskOrchestrator
  participant ModelClient
  participant classify_provider_transport_failure
  participant AgentPool
  TaskOrchestrator->>ModelClient: route/Conduct chat 호출
  ModelClient-->>TaskOrchestrator: ProviderUpstreamError
  TaskOrchestrator->>classify_provider_transport_failure: retryable 전달
  classify_provider_transport_failure-->>TaskOrchestrator: 재시도 또는 failover 결정
  TaskOrchestrator->>AgentPool: 다음 eligible agent 선택
Loading
sequenceDiagram
  participant discover_all_models
  participant _fetch_models_dev_metadata
  participant ModelsDev
  discover_all_models->>_fetch_models_dev_metadata: 카탈로그 조회 요청
  _fetch_models_dev_metadata->>ModelsDev: HTTP fetch
  ModelsDev-->>_fetch_models_dev_metadata: 데이터 또는 일시적 오류
  _fetch_models_dev_metadata->>ModelsDev: 실패 시 최대 재시도
  _fetch_models_dev_metadata-->>discover_all_models: 메타데이터 또는 None
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 5 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 provider transport failure를 명시적으로 분류하도록 routing을 수정한 PR의 핵심 변경을 정확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 5 files. (5 skipped: 4 unsupported, 1 too large.)

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/noema-opencode-strix-orchestration-sexqzc

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.

Follow-up to a dedicated adversarial root-cause review of PR #922: it
correctly flagged that ToolFallbackStoppedError bypasses candidate
failover entirely via an immediate re-raise in _invoke, the same shape
as the ProviderUpstreamError misclassification this PR already fixed.

Verified this is a distinct case, not a leftover instance of the fixed
bug, and deliberately did not change its behavior: every path that
raises it resolves to ambiguous_outcome, permission_denied,
policy_blocked, or invalid_arguments (via classify_tool_failure's
FAIL_CLOSED branches, or the provider's own explicit terminal
tool-execution-state signal). ADR 0001 states as an explicit invariant
that permission/policy failures must never fall through to another
agent, and that non-idempotent timeout/transport uncertainty must never
replay automatically -- for an ambiguous server-side outcome
specifically, a different agent asking again does not make the
uncertainty go away, so converting this to failover would need an
explicit product decision about which failure kinds that is actually
safe for, not a mechanical port of the ProviderUpstreamError fix. Left
unimplemented pending that decision; documented inline so it reads as a
deliberate boundary rather than an oversight next time it's audited.

(Also verified in the same follow-up: ProviderResponseError's identical
carve-out was already fixed independently on main's PR #868, which this
branch is already rebased onto -- allowed_agent_ids is not None for
every orchestrator/free and orchestrator/auto call, so it already
advances to the next candidate and only fails closed after every
candidate in the pool has structurally failed. No code change needed
there.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
seonghobae pushed a commit to ContextualWisdomLab/.github that referenced this pull request Aug 30, 2026
Second, .github-local half of the 2026-08-30 request-time-failover
investigation (contextual-orchestrator's own routing-level fix is tracked
separately as ContextualWisdomLab/contextual-orchestrator#922, draft, not
yet merged: classify_provider_transport_failure corrects a provider
transport failure being misclassified through a tool-execution-failure
classifier). That fix cannot help here: contextual_orchestrator_review_launcher.py's
_preflight_review_agents calls client.proxy_send_once directly per candidate
and never reaches TaskOrchestrator's own routing/failover at all. Its client
is configured with max_retries=0 and proxy_send_once is a single-shot
transport by design, so before this change a single transient blip (a 503,
a timeout) permanently rejected an otherwise-healthy route with zero retry
budget of this loop's own -- in the worst case where every discovered
candidate hit the same transient blip in one run, the whole sidecar would
exit before healthz regardless of how good the gateway's own failover is.

_is_retryable_preflight_error classifies a caught exception by type and, for
an HTTP failure, status code alone (408/429/500/502/503/504, or a
connection-level failure with no status) -- never from response text, and
with no dependency on the vendored contextual_orchestrator package (which
this file's own coverage-omit note documents as unavailable to this
repository's test suite; importing it at call time here would silently pass
only in environments where it happens to be installed and fail in real CI).
_preflight_review_agents now gives each candidate up to
REVIEW_PREFLIGHT_ATTEMPTS_PER_ROUTE (2) attempts: one bounded retry on a
retryable failure, none on a non-retryable one, preserving the exact same
report/row shape.

Bears directly on the exact-head review's acceptance criteria 3 and 4 for
Strix's orchestrator/free access: a single flaky preflight attempt can no
longer take the whole review pipeline down before Strix ever runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
@seonghobae
seonghobae marked this pull request as ready for review August 30, 2026 12:04

@devin-ai-integration devin-ai-integration 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.

Devin Review found 4 potential issues.

Devin Review

Comment on lines 6495 to +6505
if isinstance(exc, ProviderUpstreamError):
last_upstream_error = exc
if isinstance(exc, ProviderResponseError):
# The primary chat call is a bounded, side-effect-free
# model request, not a tool invocation: classify from
# the provider's own already-computed retryability
# instead of classify_tool_failure's message-text
# heuristics, so free/auto virtual-model failover can
# never be accidentally downgraded to fail-closed by
# incidental wording in an upstream error body (e.g. a
# 400 that happens to mention "invalid arguments").
decision = classify_provider_transport_failure(exc.retryable)

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.

📝 Info: Layered retries remain bounded

ModelClient.chat exhausts transport retries before _invoke applies its orchestration budget. The multiplication is finite and preserves the existing two-layer contract.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +155 to +159
return _decision(
ToolFailureKind.TRANSPORT_ERROR,
ToolFallbackAction.FAILOVER_AGENT,
circuit_failure=True,
)

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.

📝 Info: Permanent failures stay pool-bound

Non-retryable provider errors advance candidates, but _failover_candidates retains free-tier and model-group boundaries. Exhaustion returns the final typed provider error.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 6495 to +6505
if isinstance(exc, ProviderUpstreamError):
last_upstream_error = exc
if isinstance(exc, ProviderResponseError):
# The primary chat call is a bounded, side-effect-free
# model request, not a tool invocation: classify from
# the provider's own already-computed retryability
# instead of classify_tool_failure's message-text
# heuristics, so free/auto virtual-model failover can
# never be accidentally downgraded to fail-closed by
# incidental wording in an upstream error body (e.g. a
# 400 that happens to mention "invalid arguments").
decision = classify_provider_transport_failure(exc.retryable)

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.

📝 Info: Size failures bypass classification

The earlier _is_request_too_large_error guard intercepts 413 failures. They retain penalty-free failover and the aggregate exhaustion error.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +314 to +320
for attempt in range(_MODELS_DEV_FETCH_ATTEMPTS):
try:
return _fetch_json(_MODELS_DEV_URL, timeout=timeout)
except (urllib.error.URLError, TimeoutError, ValueError, OSError):
if attempt < _MODELS_DEV_FETCH_ATTEMPTS - 1:
time.sleep(_MODELS_DEV_FETCH_RETRY_DELAY_SECONDS)
return None

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.

📝 Info: Catalog retries preserve cost safety

Only parsed metadata reaches the exact model join. Exhaustion returns None, leaving cost unknown rather than classifying any model as free.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

claude added 2 commits August 30, 2026 18:29
Adds the regression test ADR-0020 (ContextualWisdomLab/.github) calls out
as still missing: a live incident reported against .github#1437 showed a
required Strix run's three bounded outer attempts against
orchestrator/free all landing on the same agent and failing with a
provider HTTP 400 invalid_request_error (retryable: false), with the
gateway ultimately declaring the whole free pool exhausted.

This PR's own classify_provider_transport_failure change already routes
a non-retryable ProviderUpstreamError to FAILOVER_AGENT instead of
same-agent retry, but no existing test proved that end-to-end for a 4xx
specifically (test_free_model_advances_through_the_free_pool_on_retryable_5xx
only covers retryable 502/503). The new test reproduces the exact
incident shape: a 400 on free_route_a advances immediately (no
same-agent retry, unlike the 5xx case) to free_route_b.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

Added the regression test docs/adr/0020-strix-orchestrator-free-pool.md (in ContextualWisdomLab/.github) calls out as still missing (commit 5d60b92): test_free_model_advances_through_the_free_pool_on_non_retryable_4xx.

A live incident reported against .github#1437 showed a required Strix run's three bounded outer attempts against orchestrator/free all landing on the same agent and failing with a provider HTTP 400 invalid_request_error (retryable: false), with the gateway ultimately declaring the whole free pool exhausted (STRIX_PROVIDER_UNAVAILABLE). classify_provider_transport_failure (this PR's own fix) already routes a non-retryable ProviderUpstreamError to FAILOVER_AGENT rather than same-agent retry, but nothing proved that end-to-end for a 4xx specifically — the existing test_free_model_advances_through_the_free_pool_on_retryable_5xx only covers retryable 502/503.

The new test reproduces the exact incident shape and passes: a 400 on free_route_a advances immediately (no same-agent retry, unlike the 5xx case) to free_route_b. This closes the specific evidence gap ADR-0020 tracks as AC3 ("a request-time non-retryable failure did not advance to a different admitted route"), for the decision-classification half of that gap. (The other half — why the incident's live pool apparently only had one eligible candidate for that request shape despite nominal family diversity ≥2 — is a separate, request-time candidate-set question that this test doesn't address; flagging it for whoever picks up .github#1437's own diversity-gate hardening, since it's evidence about that gate's assumptions, not about this PR's classification fix.)

Full local suite: python -m pytest tests -q → 2777 passed, 1 skipped, 1 pre-existing unrelated failure (fast_mlsirm native module not installed in this sandbox — confirmed identical with/without this change).


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Standing down on the required opencode-review check failure on 5d60b929 ("No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head") — not this PR's diff, and not a flake worth re-running.

This is the same pre-existing, org-wide opencode-review/Strix outage I've been tracking and documenting today across ContextualWisdomLab/.github#1456, #1459, and #1437: a broad sample of OpenCode Review Dispatch runs across many unrelated PRs and repositories org-wide shows the same near-universal failure pattern over the last several hours, and the underlying cause traces to Strix's own request-time free-pool failover gap that this very PR is fixing (see the comment I just added above, and the fuller trail on .github#1437). Re-running would not help — this is systemic, not local to this commit. Keeping this PR watched.


Generated by Claude Code

coderabbitai[bot]

This comment was marked as resolved.

Two CodeRabbit nits on PR #922:

- docs/architecture.md's _invoke description listed 5xx/429/network/413
  as the failures that advance to the next candidate but omitted
  non-retryable 4xx (401/403/404), which
  classify_provider_transport_failure(False) also routes to
  FAILOVER_AGENT -- an operator reading only this list could wrongly
  conclude those errors aren't failed over. Added them and noted the
  list is illustrative, not exhaustive.
- The ADR-0041 amendment said _fetch_models_dev_metadata "retries...
  3 times", which reads as 3 retries (4 total attempts). The code makes
  at most 3 total attempts (the initial attempt plus 2 retries). Fixed
  the wording; no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

The strix check also failed on 3fea0afc, separately from opencode-review above: STRIX_PROVIDER_UNAVAILABLE: contextual-orchestrator/orchestrator/free exhausted; the gateway owns provider discovery and failover. Same root cause as everything else I've documented today — this is direct, textbook confirmation that the classification fix in this PR, while correct and necessary, doesn't by itself fully close the incident: the underlying free pool's actual per-request route diversity is still often 1 in practice (see my reply above and the fuller trail on .github#1437), so exhausting that single effectively-available route still fails closed here exactly as designed. No further fix from me on this PR for that remaining half — it's the same open request-time candidate-set gap, not a regression from this change. Not re-running; it wouldn't help.


Generated by Claude Code

@seonghobae
seonghobae merged commit 1dd131a into main Aug 31, 2026
32 of 34 checks passed
@seonghobae
seonghobae deleted the claude/noema-opencode-strix-orchestration-sexqzc branch August 31, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants