fix(auxiliary): skip same-provider retry on a vision full-budget timeout - #97572
xmhuangzhijun-hue wants to merge 1 commit into
Conversation
Issue NousResearch#54465 established that a same-provider retry after a full-budget timeout costs a second whole `timeout` window before the fallback chain is reached, doubling the user-visible stall, and that compression must not pay it because it sits on a critical path. The guard added for that is spelled `task == "compression"`, so vision — which sits on the interactive path — still retries. The cost is the same and the stall is more visible: the turn holding the image cannot answer, and because turns are serialised the following user messages queue behind it. Two sequential full-budget timeouts on an unhealthy vision provider is a long stall for something the fallback chain could have served immediately. Replaces the string comparison at both retry sites (sync `call_llm` and `async_call_llm`) with `_TIMEOUT_NO_RETRY_TASKS = {"compression", "vision"}`, so the two paths cannot drift again. Behaviour is unchanged for every other task: fast blips (a streaming-close or a 5xx) still retry, and only full-budget timeouts on those two tasks skip straight to fallback. Tests: vision now falls straight through to fallback with the primary tried exactly once, and a non-critical task still gets its one same-provider retry, so the change stays scoped. Reverting the source change fails the vision test and leaves the scoping test green. Not the same as NousResearch#51513, which fixes five separate defects in the vision fallback chain (capability detection, sync/async client misuse, geo-block and RemoteProtocolError classification, and chain iteration). This is about what happens before that chain is reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Overall: Sound extension of the "skip same-provider retry on full-budget timeout" policy from compression to vision — both sit on user-visible critical paths where a second full What it does
Non-blocking notes
No blocking issues; the change is minimal, symmetric across sync/async, and well-tested. Non-blocking — please use your judgment. |
|
Confirmed against the PR tree: the production vision entry points use the exact |
|
Triage note (perf/P2 sweep, verified against Canonical for the vision-timeout pair (#97572 / #87087). Premise live on main: only Your diff no longer applies cleanly because f50b5bb added a carve-out on the sync site after your base (a no-progress timeout — dead stream, zero output within 60 s — is cheap and still retries; only stalls/hard-ceiling skip). Salvaged onto current main as #101570 with your commit cherry-picked (authorship preserved; conflict resolved keeping that carve-out) plus two follow-ups of ours: mirror the carve-out on the async site (it never had it — a stillborn async vision stream would have skipped to fallback where sync retries), and fold the three-clause decision into one Benchmark (macOS / Apple Silicon; stubbed network — primary sleeps
What actually improved: one timeout window instead of two before fallback runs — with the real vision timeout that is on the order of minutes saved per timed-out image, on the path that blocks the next user message. Tests: your two tests + |
… skip f50b5bb taught the sync retry site to keep the cheap same-provider retry when a Codex stream dies inside the 60s no-progress window (zero output), skipping straight to fallback only on a stall or hard-ceiling timeout. The async site never got that carve-out, so after widening the skip to vision (#97572) an async vision call on a stillborn stream would have jumped to fallback where the sync path retries. Both sites now apply the same rule. Adds the async twin of the vision-skip test and a no-progress-still-retries guard for the async site.
|
Merged via #101570 (rebase-merge, your commit cherry-picked with authorship preserved — merge commit On the review notes here: the literal |
… skip f50b5bb taught the sync retry site to keep the cheap same-provider retry when a Codex stream dies inside the 60s no-progress window (zero output), skipping straight to fallback only on a stall or hard-ceiling timeout. The async site never got that carve-out, so after widening the skip to vision (NousResearch#97572) an async vision call on a stillborn stream would have jumped to fallback where the sync path retries. Both sites now apply the same rule. Adds the async twin of the vision-skip test and a no-progress-still-retries guard for the async site.
What does this PR do?
Issue #54465 established that a same-provider retry after a full-budget timeout costs a second whole
timeoutwindow before the fallback chain is reached — doubling the user-visible stall — and that compression must not pay it because it sits on a critical path.The guard added for that is spelled
task == "compression", at both retry sites:So vision still retries, and vision sits on the interactive path. The cost is identical and the stall is more visible: the turn holding the image cannot answer, and because turns are serialised the following user messages queue behind it. Two sequential full-budget timeouts against an unhealthy vision provider is a long stall for something the fallback chain could have served immediately.
This replaces the string comparison at both sites (sync
call_llmandasync_call_llm) with a shared_TIMEOUT_NO_RETRY_TASKS = frozenset({"compression", "vision"}), so the two paths cannot drift apart again.Behaviour is unchanged for every other task. Fast blips (a streaming-close or a 5xx) still retry — only full-budget timeouts on those two tasks skip straight to fallback.
Found while running a deployment with a configured
auxiliary.visionprovider on a link that times out intermittently.Related Issue
Precedent and rationale: #54465
Not a duplicate of #51513 — that fixes five separate defects inside the vision fallback chain (capability detection, sync/async client misuse, geo-block and
RemoteProtocolErrorclassification, chain iteration). This is about the wasted timeout window before that chain is reached, and the two changes are independent.Type of Change
Changes Made
agent/auxiliary_client.py— added_TIMEOUT_NO_RETRY_TASKS; both retry sites now test membership instead of comparing to the literal"compression"; comments updated to state why vision qualifies. Log lines carry the task name rather than hardcoding it.tests/agent/test_auxiliary_client.py— two tests inTestTransientTransportRetry:test_vision_skips_same_provider_retry_on_timeout— primary is tried exactly once, then fallback.test_non_critical_task_still_retries_same_provider_on_timeout— an ordinary task keeps its one same-provider retry, proving the change is scoped rather than blanket.Testing
The vision test has teeth: reverting the source change fails it while leaving the scoping test green.
pytest tests/agent/test_auxiliary_client.py:The 5 pre-existing failures are unrelated to this path and reproduce on a clean checkout of
main.One note for review: vision resolves its client through
resolve_vision_provider_client()rather than_get_cached_client(), so the new test patches that instead — the retry block under test is shared by both paths.