Repository navigation
fix: cool an account down per served model, not for every model it serves - #174
Merged
Merged
Conversation
…rves A model-level upstream refusal (OpenRouter's peak-load refusals for one model) tripped the account's cooldown, so a single-account pool answered no healthy upstream for every other model too. The failure streak and cooldown are now keyed by account and served model; /internal/accounts lists the cooling models.
…-model cooldown test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The failure streak and cooldown were keyed by account name only. A refusal that only concerns one model therefore benched the account for every model it serves. OpenRouter's peak-load refusal for one model ("exceeds the maximum usage size allowed during peak load") is an example, and it often arrives as an error event inside a 200 stream.
On a single-account pool, that means every caller gets
no healthy upstream account serves model type ...for 30 s after three such refusals, including callers of unrelated models. In a 2h15m window, one model's refusals tripped the only Responses account 36 times and produced 211no healthy upstreamrefusals for callers of other models.Change
gw_state::health_key(account, model). The health store records failures, successes and cooldowns per account and served model (after variant and fallback resolution).AccountPool::select_healthytakes the served model and skips only the accounts cooling down for it.account_cooldownalerts. The alert subject and theaccount_healthdecision now nameaccount/model./internal/accounts. It reportshealth: coolingwhile any configured model the account serves is cooling, and lists those models in a newcooling_modelsfield. The control plane reads onlyhealth, so it is unaffected.architecture.md,providers.md,configuration.md,multi-instance.mdandapi.mdare updated.Why per-model keys rather than a last-account exemption
Never cooling the last healthy account would keep sending the failing model to an upstream that refuses it. It would also still bench the account for every model on multi-account pools. Keying by served model isolates the refusal where it happens, and it needs no special case for pool size.
The cost: an account-wide fault (a revoked key, an unreachable host) now has to trip each model's streak on its own,
failure_thresholdfailures per model. Each of those failures still fails over to another account within its request, so the delay is bounded.Hot-path cost
Stringper candidate account, plus theVecholding them.record_success.Tests
a_model_cooling_down_leaves_its_only_account_serving_other_models. A single-account pool with two models,failure_threshold: 1, and an upstream that refuses one model. The test runs twice: once with a plain 503, and once with the refusal as an error event inside a 200 stream, reached through a variant so the served name differs from the requested one. The refused model cools down while the other is still served by the same account; the cooling model is then refused withno healthy upstream. With account-only keys, the test fails at the check that the account is still available for the other model.a_cooldown_benches_the_account_for_one_model_only.select_healthypicks the account for one model and skips it for the cooling one.account_cooldown_and_recovery. It now also assertscooling_models.Gates:
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warningsandcargo test --workspaceare clean: 775 passed, 4 ignored.GW_TEST_REDIS_URL/GW_TEST_PG_URLsuites were not run.RedisHealthcode is unchanged; only the key it receives changes.Deploy note
Health keys change shape, so a cooldown active at deploy time is forgotten. Old Redis keys expire on their own TTL.