Skip to content

fix: the non-stream deadline, conversation-sticky variants and credential faults - #124

Merged
CMGS merged 3 commits into
mainfrom
fix/upstream-contract-followups
Sep 23, 2026
Merged

CMGS merged 3 commits into
mainfrom
fix/upstream-contract-followups

Conversation

@CMGS

@CMGS CMGS commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #123. The three design items #123's review round left open, each re-verified against the source and fixed with a failing-first test and a live before/after.

Commit Finding Defect Live before Live after
a non-streaming upstream deadline grows with the request's output cap B1 the per-account timeout (default 60s) bounded a non-streaming attempt whole; with max_tokens defaulting to 16384 on models that think by default (#123), a long answer outran it: timeout, failover, a second timeout, both accounts cooled, where the old 1024 cap had truncated in time an Anthropic account with timeout_seconds: 20, claude-opus-5-5 non-streaming, no max_tokens, a 1200-word essay: 408 model_timeout_exception after 20 s 200 after 37 s, finish_reason: stop, 3764 completion tokens (the 16384 cap budgets 461 s)
a conversation without a user id stays on one variant B2 a thinking replay from a client with no user id was pinned to the requested model while its first turn had been split like any request, so a canary rollout switched models mid tool loop (the residue #123 declared) claude-opus-5 canaried to claude-opus-5-5, no user id, a tool turn: turn 1 served by 5-5 with a thinking block, the replay served by claude-opus-5 both turns served by claude-opus-5-5
a credential or billing refusal faults the account and fails over B4 a vendor 401/402/403 answers the account's own state, yet neither engine arm counted it: no cooling, no failover; a dead account kept its share of traffic and answered 424 to every call two Anthropic accounts, the preferred one with an invalid key, failure_threshold: 1: two requests, both 424 (vendor 401), the good account never tried request 1 200 through the failover to the good account; request 2 200 on the good account directly, the refused one cooled

Design notes:

  • The deadline is max(timeout_seconds, max_tokens at 128k tokens/hour), capped at ten minutes: the rate and ceiling the Anthropic SDK budgets for non-streaming calls. timeout_seconds stays the floor, so an operator's 120s still holds; streams keep their header deadline and idle cap. The cap rides the upstream request from the Anthropic wire and OpenAI chat; the Bedrock paths keep the plain timeout.
  • The variant key is the user id when present, else the conversation's first user turn. A signed replay must carry that prefix verbatim (the signature binds to it), so every turn lands where the first one did; a reasoning continuation with neither key (a native Responses input) still pins to the requested model.
  • 401–403 join 5xx as account faults on both engine arms and in the handler's fallback test; 429 stays a throttle (no cooling), and an in-band insufficient_quota that arrives as HTTP 429 is not distinguished.

Verification

  • Failing-first: each of the three tests (non_stream_deadline_grows_with_the_output_cap, a_conversation_without_a_user_id_sticks_to_one_variant with thinking_conversations_stay_on_one_variant_and_off_quota_fallbacks, a_credential_refusal_fails_over_and_cools_the_account over 401/402/403) fails with its fix reverted and passes with it.
  • Every commit verified on its own: each of the 3 commits, exported fresh (git archive | tar -m), passes cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings and cargo test --workspace; the test count rises 676 → 679.
  • Linux: a rust:1 linux/arm64 container: rustc 1.98.0, fmt ✓, clippy ✓, 679 passed, 0 failed, 4 ignored (the env-gated Postgres/Redis suites were not run; this PR touches no store).
  • Live before/after (in the table): B1 against the real Anthropic API with a 20 s account timeout; B2 against claude-opus-5 canaried to claude-opus-5-5; B4 with an invalid key on the preferred account. The full live matrix was run on fix: close the flow-read defects (#112–#121) with live before/after #123's binary; this PR changes no wire body, only the deadline, the split key and the fault gate.
  • Hot path: one max/multiply per non-streaming upstream call (the deadline), one find over the messages on the variant path only when no user id is present (the first message is normally the first user turn), and one status compare on the error path. An ABBA bench against fix: close the flow-read defects (#112–#121) with live before/after #123 (in-process mock upstream, c=64, 6 rounds of 10 s) read chat→Claude −0.3%, buffered messages +0.1%, Converse stream +0.4% and Responses stream +0.5% requests/s, p50 within ±0.02 ms; an A/A control on the same host read +0.2% / −0.0% / −2.9% / +0.5%, so every change is within noise. The bench binary predates two layout-only fixups (a helper moved above a doc comment, a blank line, a doc sentence).
  • Hygiene ledger: style, judge, comments and loc lenses at 0 due over 102 files and 12 flows after the change.

Size

Production code grows by 37 lines net (+48 −11) for the three items, tests by 106 and docs by 6. Comment lines in *.rs are 4 added and 2 removed: a one-line doc on the new output_cap field (its 0 sentinel), a one-line WHY on the 128k-tokens/hour constant (its source), and the VariantSelect doc reworded for the new key. Whole-repo non-blank production Rust goes from 26115 (#123) to 26148 lines, and comment density from 8.75% to 8.76%. Per commit (prod net): the deadline +18 (the field on the upstream request, its eight constructors, the two engines setting it, the deadline function), the conversation key +12, the account-fault gate +4; the remaining 3 lines of the range are comment rewording counted as production lines.

…t cap

The per-account timeout (default 60s) bounded a non-streaming attempt
whole. With max_tokens defaulting to 16384 on models that think by
default, a long answer outran it: the request timed out, failed over,
timed out again and cooled both accounts, where the old 1024 cap had
truncated in time. The deadline is now at least the account's timeout
and grows with the request's max_tokens at 128k tokens per hour, at most
ten minutes: the rate and ceiling the Anthropic SDK budgets for
non-streaming calls. The cap rides the upstream request from the
Anthropic wire and OpenAI chat; streams keep their header deadline and
idle cap.
A thinking replay from a client with no user id was pinned to the
requested model while its first turn had been split like any request,
so a canary rollout switched models mid tool loop. The split is now
keyed by the conversation's first user turn when no user id is present:
a signed replay must carry that prefix verbatim, so every turn lands
where the first one did. A reasoning continuation with neither key (a
native Responses input) still pins.
A vendor 401, 402 or 403 answers the account's own state, not the
request, yet neither engine arm counted it: the first attempt neither
cooled the account nor failed over, and the retry arm only cooled it by
accident until the 5xx gate. A dead account kept its share of traffic
and answered 424 to every call. Both arms now treat 401-403 as an
account fault, the same as a 5xx: cool it, try the next account, and
walk the fallback chain when none is left.
@CMGS
CMGS force-pushed the fix/upstream-contract-followups branch from ed42632 to 600c6c6 Compare September 23, 2026 12:04
@CMGS
CMGS changed the base branch from fix/closure-followups to main September 23, 2026 12:04
@CMGS
CMGS changed the base branch from main to fix/closure-followups September 23, 2026 12:24
@CMGS
CMGS changed the base branch from fix/closure-followups to main September 23, 2026 12:24
@CMGS CMGS closed this Sep 23, 2026
@CMGS CMGS reopened this Sep 23, 2026
@CMGS
CMGS merged commit f16be65 into main Sep 23, 2026
2 checks passed
@CMGS
CMGS deleted the fix/upstream-contract-followups branch September 23, 2026 13: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.

1 participant