You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Model facts — context window, tokenizer, pricing, cache economics — live in five places that drift:
Each Python provider hardcodes its own table + prefix logic + inconsistent fallback: providers/google.py raises ValueError for unknown models (get_token_counter, get_context_limit), while providers/openai.py / providers/anthropic.py degrade gracefully (_load_custom_model_config + family inference + default). Same job, three behaviours.
crates/headroom-proxy/src/compression/model_limits.rs reads the vendored LiteLLM catalog — but only extracts max_input_tokens.
crates/headroom-proxy/data/model_prices_and_context_window.json — the rich catalog (~2,700 models incl. gpt-5.2, gemini-3-pro-preview, with input_cost_per_token, cache_read_input_token_cost, cache_creation_input_token_cost) — is under-exploited.
The installed litellm package: Python providers use litellm.get_model_info as a soft lookup — a second copy of the same catalog, version-dependent, that can drift from the vendored snapshot.
compression_policy.{py,rs} hardcodes cache economics (CACHE_WRITE_MULTIPLIER=1.25, CACHE_READ_MULTIPLIER=0.10) for every provider, ignoring the catalog's per-model cache costs.
Cost of the fragmentation:
New models break unevenly. gemini-3-* raises in the Google provider, so the in-process LangChain wrapper (integrations/langchain/chat_model.py, which calls get_context_limit) falls back to no optimization — while the same model degrades cleanly on OpenAI. Silent and provider-specific.
One catalog, read the same way everywhere; providers and policy become thin consumers.
Source of truth: the vendored catalog (model_prices_and_context_window.json), already in-repo, already refreshed by scripts/refresh_model_limits.sh. Keep vendoring (no runtime/build network — the existing model_limits.rs rationale stands).
One accessor per language. Extend model_limits.rs to expose all relevant fields (context, pricing, cache read/creation), not just max_input_tokens. Add the Python equivalent reading the same data.
Providers become thin and uniform. get_context_limit / get_token_counter / estimate_cost resolve via one chain for all three: operator override (models.json / HEADROOM_MODEL_LIMITS) → catalog → family inference → graceful default. Never raise. The per-provider hardcoded dicts shrink to a small set of known-stable anchors for offline robustness.
Policy consumes the catalog. CompressionPolicy derives (w, r) per-model from the catalog's cache costs, defaulting to today's constants when absent (so existing anchors are unchanged).
Operator override + freshness, unchanged in spirit. models.json (generalized to every provider) for local additions; a scheduled CI job re-vendors the snapshot and opens a PR.
Concrete shape
Fields the layer reads from the catalog (already present, per model): max_input_tokens, input_cost_per_token, output_cost_per_token, cache_read_input_token_cost, cache_creation_input_token_cost, supports_prompt_caching. Everything the providers and the policy need is already there — nothing new to author per model.
Operator override (~/.headroom/config/models.json), generalized to every provider — the same shape the OpenAI/Anthropic loaders already accept, now honored by Google too:
Resolution order, identical for every provider: operator override → vendored catalog → family inference → graceful default (never raise).
Migration — incremental, each piece shippable on its own
fix(providers): Google adopts the graceful-fallback + models.json loader already in OpenAI / Anthropic. Pure bug fix — fixes the gemini-3 raise. Independent.
refactor(providers): extract the shared catalog accessor; route all three Python providers through it; collapse the duplicated dicts to anchors. Behaviour-preserving.
Catalog stays in headroom-proxy; it injects (w, r) into the core policy. Core must not carry a 1.4MB JSON or depend on proxy, so the proxy (which already owns the snapshot via include_str!) computes the per-model economics and passes them through CompressionPolicy::with_cache_economics(…). If you'd rather the catalog live in core, that's your call — it changes the wiring.
One physical catalog, read by both languages; retire the litellm.model_cost lookup. Ship the vendored JSON as Python package data and read it in Python too, so Rust and Python can't drift. Bonus: drops Python's soft dependency on whatever litellm version is installed. Cost: ~1.4MB in the wheel — acceptable for a deploy artifact.
Shrink the per-provider dicts to a small anchor set; stop growing them per-model. Keep a handful of known-stable models per provider as an offline last-resort default and a test lock; route the primary lookup through the catalog. New models then need zero per-provider edits — the whole point.
I'm happy to drive this as the migration PRs above, starting with the Google bug fix (independent) and the per-model economics on top of #905. Does the shape work for you?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Problem
Model facts — context window, tokenizer, pricing, cache economics — live in five places that drift:
providers/google.pyraisesValueErrorfor unknown models (get_token_counter,get_context_limit), whileproviders/openai.py/providers/anthropic.pydegrade gracefully (_load_custom_model_config+ family inference + default). Same job, three behaviours.crates/headroom-proxy/src/compression/model_limits.rsreads the vendored LiteLLM catalog — but only extractsmax_input_tokens.crates/headroom-proxy/data/model_prices_and_context_window.json— the rich catalog (~2,700 models incl.gpt-5.2,gemini-3-pro-preview, withinput_cost_per_token,cache_read_input_token_cost,cache_creation_input_token_cost) — is under-exploited.litellmpackage: Python providers uselitellm.get_model_infoas a soft lookup — a second copy of the same catalog, version-dependent, that can drift from the vendored snapshot.compression_policy.{py,rs}hardcodes cache economics (CACHE_WRITE_MULTIPLIER=1.25,CACHE_READ_MULTIPLIER=0.10) for every provider, ignoring the catalog's per-model cache costs.Cost of the fragmentation:
gemini-3-*raises in the Google provider, so the in-process LangChain wrapper (integrations/langchain/chat_model.py, which callsget_context_limit) falls back to no optimization — while the same model degrades cleanly on OpenAI. Silent and provider-specific.Proposal
One catalog, read the same way everywhere; providers and policy become thin consumers.
model_prices_and_context_window.json), already in-repo, already refreshed byscripts/refresh_model_limits.sh. Keep vendoring (no runtime/build network — the existingmodel_limits.rsrationale stands).model_limits.rsto expose all relevant fields (context, pricing, cache read/creation), not justmax_input_tokens. Add the Python equivalent reading the same data.get_context_limit/get_token_counter/estimate_costresolve via one chain for all three: operator override (models.json/HEADROOM_MODEL_LIMITS) → catalog → family inference → graceful default. Never raise. The per-provider hardcoded dicts shrink to a small set of known-stable anchors for offline robustness.CompressionPolicyderives(w, r)per-model from the catalog's cache costs, defaulting to today's constants when absent (so existing anchors are unchanged).models.json(generalized to every provider) for local additions; a scheduled CI job re-vendors the snapshot and opens a PR.Concrete shape
Fields the layer reads from the catalog (already present, per model):
max_input_tokens,input_cost_per_token,output_cost_per_token,cache_read_input_token_cost,cache_creation_input_token_cost,supports_prompt_caching. Everything the providers and the policy need is already there — nothing new to author per model.Operator override (
~/.headroom/config/models.json), generalized to every provider — the same shape the OpenAI/Anthropic loaders already accept, now honored by Google too:{ "google": { "context_limits": { "gemini-9-experimental": 1048576 }, "pricing": { "gemini-9-experimental": [2.5, 15.0] } }, "openai": { "context_limits": { "gpt-6": 400000 } }, "fallback": { "on_unknown_model": "infer_then_default" } }Resolution order, identical for every provider: operator override → vendored catalog → family inference → graceful default (never raise).
Migration — incremental, each piece shippable on its own
fix(providers):Google adopts the graceful-fallback +models.jsonloader already in OpenAI / Anthropic. Pure bug fix — fixes thegemini-3raise. Independent.refactor(providers):extract the shared catalog accessor; route all three Python providers through it; collapse the duplicated dicts to anchors. Behaviour-preserving.feat(policy):policy derives per-model(w, r)from the catalog (Rust + Python), default = today's constants. Builds on feat(policy): consume net-cost mutation gate in ContentRouter (#856 P2) #905; provable viaHEADROOM_NET_COST_POLICY=1.ci:scheduled re-vendor workflow (freshness).CompressionPolicyhand-mirror via PyO3 — the module docstring already flags this as desirable.Each step is small, reversible, and leaves the tree green; the end state is one coherent layer.
Non-goals
/v1/modelsdiscovery; this is the attribute substrate underneath it.Relationship to current work
/v1/modelsanswers "which models exist"; this answers "what are this model's compression-relevant attributes". This is the static fallback layer [FEATURE] Dynamic model registry for headroom wrap — auto-discover available models #77 lists as component 3.(w, r)per-model instead of Claude-calibrated.Design decisions
headroom-proxy; it injects(w, r)into the core policy. Core must not carry a 1.4MB JSON or depend on proxy, so the proxy (which already owns the snapshot viainclude_str!) computes the per-model economics and passes them throughCompressionPolicy::with_cache_economics(…). If you'd rather the catalog live in core, that's your call — it changes the wiring.litellm.model_costlookup. Ship the vendored JSON as Python package data and read it in Python too, so Rust and Python can't drift. Bonus: drops Python's soft dependency on whateverlitellmversion is installed. Cost: ~1.4MB in the wheel — acceptable for a deploy artifact.I'm happy to drive this as the migration PRs above, starting with the Google bug fix (independent) and the per-model economics on top of #905. Does the shape work for you?
All reactions