openrouter: price models from the live rate card, not only the bundled map - #10
Open
jwbron wants to merge 1 commit into
Open
openrouter: price models from the live rate card, not only the bundled map#10jwbron wants to merge 1 commit into
jwbron wants to merge 1 commit into
Conversation
…d map PR #8 fixed half of a two-headed problem: `model_prices_and_context_window.json` does not carry current OpenRouter slugs, so `supports_reasoning` answered False for a current model and its reasoning knob was dropped in silence. The same absence has a second consequence nobody had chased. `_get_model_info_helper` raises "This model isn't mapped yet" for those slugs, so `response_cost` is never computed and every cost consumer downstream reports nothing. For a proxy routing current slugs that is not an edge case; it is every call. OpenRouter publishes the rate card in the same `GET /api/v1/models` response `capabilities.py` already fetches, so this reads it off the existing cache and hands it to the model-info lookup at the raise site, after every map lookup has already failed. A slug the bundled map DOES carry keeps the bundled answer: the live card can add a model but never reprice one. Two deliberate limits, both about not trading a known unknown for a confident wrong number: * Cost fields only. `supports_reasoning: true` arriving through this door would make `get_supported_openai_params` admit `thinking`, a different wire shape from OpenRouter's `reasoning` that would be forwarded verbatim to a provider that does not accept it. Parameter admission stays where PR #8 put it. Context lengths are omitted for the same reason: the entry changes what a call costs, nothing else. * A rate card tiered by prompt length is declined outright. OpenRouter expresses a long-context surcharge as `pricing.overrides` keyed by an arbitrary `min_prompt_tokens` — qwen3-max publishes 32000 and 128000. This map has slots for three fixed thresholds and drops the rest, so a faithful translation does not exist in general, and registering the base tier would under-report by 2-2.5x on exactly the long prompts such a model is chosen for. Silently, under a field an operator would use to compare models. Those stay unpriced and log the reason once, naming the tiers. 49 of 367 models on the roster are tiered; the rest get a real number. Also folds in the hardening the twin of this file has accumulated in jwbron/egg since PR #8 (config/litellm/openrouter_capabilities.py). The two are meant to stay line-for-line comparable and had drifted: * Fetch failures were logged at `debug`. verbose_logger defaults to INFO, so a permanently unreachable endpoint reverted behaviour to the bundled map in total silence — the same silent-fallback problem the drop_params warning in PR #7 exists to remove. Now the first failure warns and the rest are debug, re-armed by a successful fetch so a startup blip cannot mute a real outage. * Bad values for the env knobs were swallowed. `TTL=5s` or `TIMEOUT=0` took the default with no signal, and `_env_flag` read every unrecognized value as enable — so `FETCH=disabled` inverted the operator's instruction rather than falling back. Both now warn once per distinct (name, value). * `_get_cache` held the lock unconditionally, so a request arriving during a refresh queued behind a network call. httpx timeouts are per-phase, not total, so that latency lands on a live request once per TTL. A thread that finds the lock held now serves the stale cache; only the very first fetch blocks. * All three warn-once latches record the dedup key only after the emit did not raise. `_log` swallows its own failures, so recording first meant a logger that was not yet in place on the first call suppressed the warning permanently. * `TTL=0` is accepted as the natural spelling of "never cache", and a 200 whose `data` list parses to nothing is now reported rather than being indistinguishable from an ordinary empty answer. Tests: the `live_payload` fixture now stubs the HTTP transport rather than `_fetch`, so the parsing — where the pricing translation lives — is under test instead of mocked past. Adds coverage for the translation, the tiered decline, provider scoping, the disable switches, unusable rate cards, and the `get_model_info` / `cost_per_token` path end to end. `TestDropParamsVisibility` in tests/test_litellm/test_utils.py gains the same fetch-disabling fixture the openrouter conftest already uses. Those tests were reaching the live model list and thus doing real HTTP with an outcome dependent on what OpenRouter published that morning; a tiered model's pricing warning is what made it visible. Operator knob: LITELLM_OPENROUTER_PRICING=0 disables the pricing half alone. LITELLM_OPENROUTER_CAPABILITY_FETCH=0 still governs both, since one fetch serves both.
6 tasks
| # line that was never emitted would demote every later outage to debug | ||
| # without anyone having seen the first one. | ||
| if _log(level, "openrouter capabilities: " + message, *args): | ||
| _WARNED_FETCH_FAILURE = True |
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.
Sequel to #8, same file, same fetch.
#8 fixed half of a two-headed problem:
model_prices_and_context_window.jsondoes not carry current OpenRouter slugs, sosupports_reasoningansweredFalsefor a current model and its reasoning knob was dropped in silence. The same absence has a second consequence nobody had chased —_get_model_info_helperraises "This model isn't mapped yet" for those slugs, soresponse_costis never computed and every cost consumer downstream reports nothing.Found in jwbron/egg#3691, where it meant 1252 of 1252 sampled calls logged a null cost and the project had no dollar figure at all for its LLM spend.
What this does
Reads the
pricingblock out of the sameGET /api/v1/modelsresponsecapabilities.pyalready caches, and hands it to the model-info lookup at the raise site — after every map lookup has already failed. A slug the bundled map does carry keeps the bundled answer: the live card can add a model but never reprice one.Two deliberate limits, both about not trading a known unknown for a confident wrong number:
supports_reasoning: truearriving through this door would makeget_supported_openai_paramsadmitthinking— a different wire shape from OpenRouter'sreasoning, forwarded verbatim to a provider that does not accept it. Parameter admission stays exactly where openrouter: read supported params from OpenRouter instead of the bundled model-cost map #8 put it. Context lengths are omitted for the same reason: this entry changes what a call costs and nothing else.pricing.overrideskeyed by an arbitrarymin_prompt_tokens— qwen3-max publishes 32000 and 128000. This map has slots for three fixed thresholds and drops the rest, so a faithful translation does not exist in general, and registering the base tier would under-report by 2-2.5x on exactly the long prompts such a model gets chosen for. Silently, under a field an operator would use to compare models. Those stay unpriced and log the reason once, naming the tiers. 49 of 367 models on the roster are tiered; the other 87% get a real number.Drift repair
The twin of this file in jwbron/egg (
config/litellm/openrouter_capabilities.py) has been hardened since #8 landed. #8's own commit message flagged the drift risk; this folds the divergence back:debug.verbose_loggerdefaults to INFO, so a permanently unreachable endpoint reverted behaviour to the bundled map in total silence — the same silent-fallback problem Log when drop_params discards caller-specified params #7'sdrop_paramswarning exists to remove, one file over. First failure now warns, the rest are debug, re-armed by a successful fetch so a startup blip cannot mute a real outage hours later.TTL=5sorTIMEOUT=0took the default with no signal. Worse,_env_flagread every unrecognized value as enable — soFETCH=disabledinverted the operator's instruction rather than falling back to the default. Both now warn once per distinct(name, value)._get_cacheheld the lock unconditionally, so a request arriving during a refresh queued behind a network call. httpx timeouts are per-phase, not total, so a pathological connection puts that latency on a live request once per TTL. A thread that finds the lock held now serves the stale cache; only the very first fetch blocks._logswallows its own failures, so recording first meant a logger not yet in place on the first call suppressed the warning permanently.TTL=0is accepted as the natural spelling of "never cache", and a 200 whosedatalist parses to nothing is reported rather than being indistinguishable from an ordinary empty answer.Tests
live_payloadnow stubs the HTTP transport instead of_fetch, so the parsing — where the pricing translation lives — is under test rather than mocked past. New coverage: the translation itself (including theinput_cache_write→cache_creation_input_token_costmapping, which if swapped prices cache writes at the read rate), the tiered decline, provider scoping, both disable switches, unusable rate cards, and theget_model_info/cost_per_tokenpath end to end against hand-computed arithmetic.TestDropParamsVisibilityintests/test_litellm/test_utils.pygains the same fetch-disabling fixture the openrouterconftest.pyalready uses. Those tests were reaching the live model list and doing real HTTP, with an outcome dependent on what OpenRouter published that morning; a tiered model's pricing warning is what made it visible.Verification
tests/test_litellm/llms/openrouter/+test_utils.py+litellm_core_utils/: 1677 passed, 8 failed — the 8 are vertex/gemini and health-check failures that reproduce identically onmainat 86a53b6. No regressions; the one test my change did break is the network-dependent one described above, fixed here.ruff checkandruff formatclean on the files this touches.Note for whoever syncs this fork next
This does not need the
costhalf of the streamed-usage fix — upstream landed that in v1.94.0 (8a49423, port of BerriAI#16162) and this fork already carries it by merge. What upstream still does not carry iscost_details, so the BYOK path (cost: 0with the real number undercost_details.upstream_inference_cost) records the zero and stops. That is a separate, small PR and probably belongs to BerriAI rather than here.