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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user