Repository navigation
Four observations on the shape of the quota signal in auto-combo scoring #14270
Replies: 3 comments 2 replies
|
@shannonlowder this is a careful read and I checked all four against
Your ordering is the one I would use too: (2) and (3) are local to two functions and can ship with unit tests against the exact inputs above; (1) needs the window length plumbed into the candidate; (4) is a design change and belongs in the #12384 discussion rather than as a standalone fix. Please open (2) and (3) as separate issues, and (1) as a third; you have the instance data and the line references, and the issue should carry your name. Tag them |
|
Yes, please do, @shannonlowder. One PR per issue, targeting |
|
Thanks @shannonlowder, I see #15347, #15348 and #15349, and the extra input you found on #15347 ( |
Uh oh!
There was an error while loading. Please reload this page.
Four observations on the shape of the quota signal in auto-combo scoring
First: this is offered as discussion, not as a defect report. I run OmniRoute self-hosted against ~840k requests/month across ~10 providers, and I went looking for why my
upstream_429rate climbed from ~0% to 36% over three weeks. That took me through the quota scoring path, and I came away with four observations that seem related — they're all about the shape of the quota signal rather than whether it's wired up.I want to be explicit that the wiring is already done and I'm not re-reporting it: #4540/#4990 fixed exhausted providers scoring identically to healthy ones, #11411 brought genuine quota-aware routing to generic providers, #12317 declared observed reliability as a factor, and #5923 extended the quota cutoff to the 16 non-auto strategies. #12384 already sketches where auto/* is heading. These four are a layer above that.
All line references are
main@02c663cd.1. Window length never reaches the score
calculateResetWindowAffinity(combo/quotaScoring.ts:417) scores reset proximity:quotaandresetWindowAffinitythen entercalculateScoreas independent weighted terms. Because they're summed linearly, the scorer can express "45% remaining" and "resets soon", but not the thing that actually predicts exhaustion: usage measured against how much of the window has elapsed."45% used of a 5-hour window" and "45% used of a monthly window" are the same number and opposite situations. Separating them needs the window's nominal length, which never enters the scorer.
The ratio form is
percentUsed / (elapsed / windowLength)— 1.0 means "exactly on pace to finish the window", above 1.0 means it runs out before it refills. One caveat from implementing this elsewhere: elapsed has to be floored (~15% of the window) or a freshly-reset window projects its first burst into a fake emergency.Worth noting
src/lib/quota/burnRate.tsalready computes exactly this —computeBurnRateFromWindow— but it serves the quota-pool subsystem (redisQuotaStore/sqliteQuotaStore) and isn't reachable frombuildAutoCandidates. So this may be less "add a burn-rate estimator" and more "route the existing one into scoring."2. An unreadable quota scores as full
quotaRemainingPercentFromQuota(combo/comboPredicates.ts:432):A quota object that is missing, malformed, or carries no recognisable
percentUsedyieldsquotaRemaining = 100→quota: 1.0, the best attainable score on that axis. The failure mode is that a provider whose usage endpoint is having a bad day doesn't merely stay eligible — it outranks every provider that honestly reported 60% consumed.The asymmetry I'd suggest is that a telemetry failure is evidence about the telemetry, not about the provider. So it probably shouldn't do either extreme:
"Rank below any real reading, but don't move what's already there" splits those. Hard exhaustion and a failed health probe are different — those are evidence about the provider and should still move traffic.
#12317's PR body makes precisely this argument for reliability ("a corrupt reading means nothing observed, not fails every call"). This is the same principle applied to the quota axis.
3. A partial reading is a hole, not a smaller plan
Same function,
:440-447:A provider that normally reports session + weekly + monthly and this time reports only session gets scored on the session window alone. Since the aggregate is
min()over the windows that did parse, a missing window can only make the provider look better — best case wins by omission.What makes this feel like an oversight rather than a design choice is that the codebase already has the concept: #10095 added
fractionReportedtoQuotaInfoinsrc/domain/quotaCache.tsprecisely so a window upstream didn't report can't be read as a real value, andisExhaustedhonours it.comboPredicates.tsdoesn't referencefractionReportedat all. The scoring path seems to have missed the fix the exhaustion path got.Suggestion: track each provider's expected window set, and mark a reading incomplete when one is absent — incomplete being distinct from both "healthy" and "exhausted".
4. Failure kind carries routing information that failure rate discards
#12317 added
reliability = 1 - failureRateas a single blended rate. That's a clear improvement overerrorRatebeing declared and read nowhere. But the kinds fold together, and they predict differently:My instance is the argument for splitting them. Over 30 days, 74,688 of 839,817 attempts were
upstream_429— nine times the next failure class. Concentrated:opencode-zen67.8% of its attempts,gemini58.5%,openrouter45.3%, againstollama-cloudat 0.2%. Those rates persist for days, not minutes.Correlating
call_logsbycorrelation_idover four days (41,671 requests): 15.1% hit at least one 429. The fallback chain rescued 93% of them, so the reliability cost is small — but those 15% of requests consumed 47% of all upstream attempts (5.49 attempts each vs 1.12 for clean requests), and burned ~84 min/day of wall-clock in rejected round-trips.That's the shape of the cost: not failures, waste. And a blended rate can't express "this provider will reject you specifically because it's out of quota, and will keep doing so until its window resets" — which is the one failure kind whose duration is knowable in advance.
Where this might go
If any of these land, my instinct is the order is (2), (3), (1), (4) — ascending in blast radius. (2) and (3) are small and local to
comboPredicates.ts; (3) arguably just extends #10095'sfractionReportedinto the scoring path. (1) needs window length plumbed to the scorer orburnRate.tsrouted in. (4) is a design conversation and may belong under the failure-aware re-ranking already sketched in #12384 rather than as its own change.Happy to be told any of these are deliberate trade-offs I've misread — I'd rather learn the reasoning than argue for a change. And if any are worth pursuing, I'm glad to open focused issues rather than keeping it in one thread.
All reactions