fix(modelinfo): decode the LiteLLM window map per entry, not as one typed map - #41
Conversation
…yped map
The LiteLLM fetch has never resolved a single context window in production, so
every fraction-based Trigger threshold (min_request_frac, min_output_frac,
huge_output_frac) has silently no-opped since it landed.
The cause is a whole-map typed decode. The upstream document carries a
`sample_spec` documentation entry whose numeric fields hold prose, and a handful
of models spell an integer window as a float. encoding/json aborts the entire map
on the first type error, so `fetch` parsed 2,988 entries and then returned
(nil, err), discarding all of them:
whole-map decode: entries=2988 err=json: cannot unmarshal string into Go
struct field .max_input_tokens of type int
Decode per entry instead: unmarshal into map[string]json.RawMessage, then decode
each value and skip the ones that fail. `sample_spec` is skipped by name so the
document's own schema notes never count as a failure, and the window fields are
read as float64 so a float-spelled integer still resolves.
Also stop degrading silently. An empty map was indistinguishable from "no model
has a window" at every call site, because every lookup fails open — which is
exactly how this hid for the life of the package. An empty result now warns that
fraction triggers will not fire, and skipped entries are logged with examples.
Against a checked-in snapshot of the real document: 0 keys before, 3,664 after,
with non-zero windows for aws/claude-sonnet-5 and aws/claude-haiku-4-5.
Closes #39
Assisted-By: Claude Opus 5
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
14db6dc to
1455e08
Compare
Verified end-to-end against the live documentI reproduced the original bug and then confirmed this fix resolves it, going past the fixture-based tests to the real network path. Before (on After (this branch), resolving from the live upstream document:
All five tests pass, including One thing that cost me time and is worth writing downMy first three attempts all showed That is correct behaviour for a hot-path lookup (never block a request on an upstream fetch), but it means a short-lived process may never see a resolved window at all — the fetch outlives the request that triggered it, and if the process exits first the work is wasted. Not a blocker for the proxy, which is long-lived. Worth a sentence in the doc comment so the next person doesn't spend the time I did concluding the fetch is broken. On the splitCorrect call to separate this from #38. Three files, one concern, independently bisectable — and it changes compaction behaviour via fraction triggers, which has no business riding along in a dashboard PR. Confirmed the diff is exactly Merging. |
Closes #39
internal/modelinfo's LiteLLM fetch has never resolved a single context window inproduction, so every fraction-based
Triggerthreshold (min_request_frac,min_output_frac,huge_output_frac) has silently no-opped since it landed.Root cause
fetchdecoded the whole document into one typedmap[string]struct{…}. The upstreammap is community-maintained and not schema-clean: it carries a
sample_specdocumentationentry whose numeric fields hold prose (
"deprecation_date": "date when the model becomes deprecated in the format YYYY-MM-DD"), and 7 models spell an integer window as a float.encoding/jsonaborts the entire map on the first type error, so it parsed everything andthen threw it away:
fetchreturned(nil, err),l.byKeystayednil, andWindowreturned0, falseforevery model forever.
Fix
Decode per entry:
map[string]json.RawMessage, then unmarshal each value andcontinuepast the ones that fail.
sample_specis skipped by name so the document's own schema notesnever even count as a failure. Window fields are read as
float64and truncated, so afloat-spelled integer resolves.
And stop degrading silently — which is how this hid for the life of the package. An empty
map was indistinguishable from "no model has a window" at every call site, because every
lookup deliberately fails open. Now an empty result warns that fraction triggers will
not fire, and skipped entries are logged with examples.
Evidence
Against a checked-in gzipped snapshot of the real upstream document (2,988 entries,
internal/modelinfo/testdata/litellm_prices.json.gz):aws/claude-sonnet-5aws/claude-haiku-4-5main)window=0 ok=falsewindow=0 ok=falsewindow=1000000 ok=truewindow=200000 ok=trueBlast radius
This changes compaction behaviour, which is why it is its own PR rather than riding
along in #38:
components/trigger.goresolves fraction thresholds againstCtx.CtxWindow,which is always
0today, so fractions are ignored and only absolute floors apply. Afterthis, a fraction-configured deployment starts compacting differently on the first request.
Practical risk today is low — no shipped preset uses a fraction (
presetConfigsuseabsolute
min_tokens/min_request_tokens), anddeploy/harbor/swebench.py:100recordsthat the benchmarked
codesmartconfig deliberately switched to absolutes. So the publishedSWE-bench and Terminal-Bench numbers do not depend on this path. But that was luck, and it
deserves to be bisectable on its own.
Tests
TestLiteLLMSkipsMalformedEntries— minimal fixture with asample_spec-shapedprose entry plus a float-spelled window; asserts a malformed sibling does not poison
the map.
TestLiteLLMDecodesTheRealDocument— the checked-in real snapshot; asserts >2,900keys decode and a non-zero window for
aws/claude-sonnet-5andaws/claude-haiku-4-5.This test fails on
mainwith 0 keys.go build -tags cg_skeleton ./...,go test -race ./...(20/20),make lint,gofmt -lall clean.