From e91a458d8a01e4518d558af2e84c28c57cdf8daa Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:03:09 -0700 Subject: [PATCH] fix(pricing): price direct OpenAI/xAI/Anthropic/Google models missing from the snapshot A direct first-party route whose model is absent from the bundled official-docs snapshot fell through to the provider's own /models listing, which carries no prices on api.openai.com or api.x.ai, and recorded the turn as cost_status=unknown / $0 despite real billed usage (gpt-5-nano on OpenAI, grok-4.3 on xAI in the live provider canary). Widen the trusted models.dev fallback from Xiaomi to every pay-per-token first-party API (openai, xai, anthropic, google, deepseek, xiaomi), keyed by billing-route provider -> the vendor's API domain. The price applies only on HTTPS:443 to that domain (or with no base URL, i.e. the provider default), so relays, downgraded origins, custom endpoints and subscription routes (xai-oauth, openai-codex) keep their own policy. Drops the blanket try/except: get_model_info(allow_network=False) reads only the memory/disk cache and does not raise. --- agent/usage_pricing.py | 45 ++++++++++++++++------------------ website/docs/user-guide/cli.md | 2 +- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 23d953cf4e..58aeff3fea 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -15,8 +15,16 @@ logger = logging.getLogger(__name__) _ZERO = Decimal("0") _ONE_MILLION = Decimal("1000000") _NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1" -_MODELS_DEV_NO_BASE_PROVIDERS = frozenset({"xiaomi"}) -_MODELS_DEV_DIRECT_HOSTS = frozenset({("xiaomi", "xiaomimimo.com")}) +# Pay-per-token first-party APIs whose models.dev rate card is the vendor's own +# list price, keyed by billing-route provider -> API domain. A model missing from +# the snapshot below is priced from models.dev only on HTTPS:443 to that domain +# (or with no base URL, i.e. the provider default): a proxy, relay or custom +# endpoint serving the same model id may bill differently, and subscription +# routes (openai-codex, xai-oauth) keep their own policy. +_MODELS_DEV_DIRECT_HOSTS = { + "openai": "openai.com", "xai": "x.ai", "anthropic": "anthropic.com", "google": "googleapis.com", + "deepseek": "deepseek.com", "xiaomi": "xiaomimimo.com", +} # Below $0.01, render at 4 dp so cheap-model costs never display as $0.00. # Sub-cent cost threshold: below $0.01, render at 4 decimal places so the display is non-zero (e.g. $0.0046 @@ -490,27 +498,17 @@ def _pricing_entry_from_metadata( def _models_dev_pricing_entry(route: BillingRoute) -> Optional[PricingEntry]: - """Return models.dev pricing only for direct provider routes.""" - if not route.provider or not route.model: + """models.dev list price for a direct first-party route (see ``_MODELS_DEV_DIRECT_HOSTS``).""" + domain = _MODELS_DEV_DIRECT_HOSTS.get(route.provider) + if not domain or not route.model: return None - try: - from agent.models_dev import get_model_info - - origin = base_url_origin(route.base_url) - if (not route.base_url and route.provider not in _MODELS_DEV_NO_BASE_PROVIDERS) or ( - route.base_url and not ( - origin[0] == "https" - and origin[2] == 443 - and any( - route.provider == provider and (origin[1] == host or origin[1].endswith("." + host)) - for provider, host in _MODELS_DEV_DIRECT_HOSTS - ) - ) - ): + if route.base_url: + scheme, host, port = base_url_origin(route.base_url) + if (scheme, port) != ("https", 443) or not (host == domain or host.endswith("." + domain)): return None - model_info = get_model_info(route.provider, route.model) - except Exception: - return None + from agent.models_dev import get_model_info + + model_info = get_model_info(route.provider, route.model) if model_info is None or not model_info.has_cost_data(): return None return PricingEntry( @@ -518,12 +516,11 @@ def _models_dev_pricing_entry(route: BillingRoute) -> Optional[PricingEntry]: output_cost_per_million=_to_decimal(model_info.cost_output), cache_read_cost_per_million=_to_decimal(model_info.cost_cache_read), cache_write_cost_per_million=_to_decimal(model_info.cost_cache_write), - source="provider_models_api", - source_url="https://models.dev", - pricing_version="models.dev", + source="provider_models_api", source_url="https://models.dev", pricing_version="models.dev", fetched_at=_UTC_NOW(), ) + def get_pricing_entry( model_name: str, provider: Optional[str] = None, base_url: Optional[str] = None, api_key: Optional[str] = None, diff --git a/website/docs/user-guide/cli.md b/website/docs/user-guide/cli.md index cec56deba3..dcc6e756c5 100644 --- a/website/docs/user-guide/cli.md +++ b/website/docs/user-guide/cli.md @@ -159,7 +159,7 @@ A persistent status bar sits above the input area, updating in real time: | Model name | Current model (truncated if longer than 26 chars) | | Token count | Context tokens used / max context window; `~` marks an estimate | | Context bar | Visual fill indicator with color-coded thresholds | -| Cost | Estimated session cost (or `n/a` for unknown/zero-priced models) | +| Cost | Estimated session cost (or `n/a` for unknown/zero-priced models). Rates come from Hermes' bundled official price table, then the provider's `/models` listing; on a direct first-party API (OpenAI, xAI, Anthropic, Google, DeepSeek, Xiaomi) a model missing from both is priced at the vendor's list price from models.dev. Proxies, relays and custom endpoints serving the same model id stay `n/a` rather than inherit that price. | | 🗜️ N | **Context compression count** — how many times the running session has been auto-compressed. Appears once the first compression fires. | | ▶ N | **Active background tasks** — how many `/bg` prompts are still running in the current session. Appears whenever at least one task is in flight. | | Duration | Elapsed session time |