diff --git a/devlog/_plan/260818_fastwire_b2_xai/evidence/010_logs_priority_lower_bound.png b/devlog/_plan/260818_fastwire_b2_xai/evidence/010_logs_priority_lower_bound.png new file mode 100644 index 0000000000..e7ac2ea007 Binary files /dev/null and b/devlog/_plan/260818_fastwire_b2_xai/evidence/010_logs_priority_lower_bound.png differ diff --git a/devlog/_plan/260818_fastwire_b2_xai/evidence/README.md b/devlog/_plan/260818_fastwire_b2_xai/evidence/README.md new file mode 100644 index 0000000000..0ae66d31f3 --- /dev/null +++ b/devlog/_plan/260818_fastwire_b2_xai/evidence/README.md @@ -0,0 +1,17 @@ +# FastWire B2 (xAI) — UI evidence + +`010_logs_priority_lower_bound.png` — Logs & Debug, three seeded `xai/grok-4.6` rows +that exercise every branch of the new pricing path: + +| Row | Situation | Cost cell | +| --- | --- | --- | +| `req-standard` | no Fast requested | `~$0.0300` | +| `req-priority` | response-confirmed priority, prompt under the long-context threshold | `~$0.0600` — exactly the documented 2x premium over the row above | +| `req-longctx-priority` | response-confirmed priority, prompt at or above 200k | `≥$0.8760` — the published long-context rate, marked a lower bound because xAI publishes no combined price | + +The `≥` prefix is the visible change: a cost that is a known floor rather than an +estimate now says so instead of rendering as `~$`. The detail drawer explains why +via the `priority_lower_bound` estimate reason. + +Captured against a local proxy with a seeded `usage.jsonl`; no live xAI request was +billed to produce it. diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 59381edd60..40ee8021f1 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -115,7 +115,7 @@ ocx logout | Fournisseur | Adaptateur | URL de base | Remarques | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | Catalogue Grok découvert en direct en priorité ; `grok-4.5` est le modèle de repli par défaut. | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth utilise la passerelle d'abonnement Grok CLI distincte. Le remplacement par clé API utilise `https://api.x.ai/v1` et peut injecter Priority Processing. Catalogue Grok découvert en direct en priorité ; `grok-4.5` est le modèle de repli par défaut. | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Modèles Claude ; liste des modèles récupérée en direct depuis `/v1/models`. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Modèles de programmation Kimi K2.7/K2.6/K2.5. | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Passerelle d'abonnement Nous Research (le même service en amont que celui utilisé par Hermes Agent). Connexion par autorisation d'appareil auprès de `portal.nousresearch.com` ; le jeton d'accès est le JWT d'inférence envoyé avec chaque requête. Le catalogue mixte de modèles payants et `:free` (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, ...) est découvert en direct pour le compte connecté. Les jetons d'actualisation sont à usage unique et renouvelés à chaque actualisation. | diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 0878773ae9..e58af8360c 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -110,7 +110,7 @@ ocx logout | Provider | Adapter | Base URL | Notes | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | Live-first Grok catalog; `grok-4.5` is the fallback default. | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth uses the separate Grok CLI subscription gateway. The API-key override uses `https://api.x.ai/v1` and may inject Priority Processing. Live-first Grok catalog; `grok-4.5` is the fallback default. | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude models; live model list fetched from `/v1/models`. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 coding models. | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research subscription gateway (same backend Hermes Agent uses). Device-grant login against `portal.nousresearch.com`; the access token is the per-request inference JWT. Mixed paid + `:free` model catalog (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, ...) discovered live from the signed-in account. Refresh tokens are single-use and rotated on every refresh. | diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 81a19e5e5b..f99bf35aec 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -105,7 +105,7 @@ ocx logout | プロバイダー | アダプター | ベース URL | 備考 | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | ライブ一覧を優先し、フォールバックのデフォルトモデルは `grok-4.5`。 | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth は独立した Grok CLI サブスクリプションゲートウェイを使用します。API キーのオーバーライドは `https://api.x.ai/v1` を使用し、Priority Processing を注入する場合があります。ライブ一覧を優先し、フォールバックのデフォルトモデルは `grok-4.5`。 | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude モデル; ライブモデル一覧は `/v1/models` から取得。 | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 コーディングモデル。 | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research サブスクリプションゲートウェイ(Hermes Agent と同じバックエンド)。`portal.nousresearch.com` へのデバイスグラントログイン; access トークンはリクエストごとの inference JWT。有料 + `:free` モデルの混在カタログ(`tencent/hy3:free`、`stepfun/step-3.7-flash:free` など)はサインイン中のアカウントからライブ探索されます。Refresh トークンは単回使用で、更新のたびにローテーションされます。 | diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 4f57dab7cc..b24e17f3fc 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -104,7 +104,7 @@ ocx logout | 프로바이더 | 어댑터 | 베이스 URL | 비고 | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | 실시간 목록을 우선 사용하며, 폴백 기본 모델은 `grok-4.5`입니다. | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth는 별도의 Grok CLI 구독 게이트웨이를 사용합니다. API 키 오버라이드는 `https://api.x.ai/v1`을 사용하며 Priority Processing을 주입할 수 있습니다. 실시간 목록을 우선 사용하며, 폴백 기본 모델은 `grok-4.5`입니다. | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 모델; 실시간 모델 목록은 `/v1/models`에서 가져옵니다. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 코딩 모델. | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research 구독 게이트웨이(Hermes Agent와 동일한 백엔드). `portal.nousresearch.com`에 대한 디바이스 그랜트 로그인; access 토큰은 요청별 inference JWT. 유료 + `:free` 모델 혼합 카탈로그(`tencent/hy3:free`, `stepfun/step-3.7-flash:free` 등)는 로그인한 계정에서 실시간으로 발견됩니다. Refresh 토큰은 단회 사용이며, 갱신할 때마다 회전됩니다. | diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 842d9a6efa..c44b628714 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -87,7 +87,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `modelMaxInputTokens?` | `Record` | Positive per-model max input limits used for catalog auto-compaction hints. | | `defaultMaxOutputTokens?` | `number` | Provider-wide `openai-chat` fallback when the client omits `max_output_tokens`. | | `modelMaxOutputTokens?` | `Record` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. | -| `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → jawcode catalog → expected-price overlay → model-level vendor fallback, and an all-zero entry falls through to the next source in that sequence. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. | +| `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an all-zero entry falls through to the next source in that sequence. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. | | `headers?` | `Record` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. | | `openRouterRouting?` | `OpenRouterProviderRouting` | Default OpenRouter `order`, `only`, and `allowFallbacks` preferences; valid only for canonical OpenRouter with `openai-chat`. | | `modelOpenRouterRouting?` | `Record` | Exact model-id overrides that replace the provider-wide OpenRouter preference. | @@ -152,6 +152,25 @@ contract; existing configurations see these migration deltas: Explicit capability `false` and Responses caller-tier forwarding retain their existing contracts. +### xAI Priority Processing + +The built-in `xai` preset advertises and injects Fast only when its effective transport uses +`authMode: "key"`. API-key mode targets `https://api.x.ai/v1` through the `openai-chat` adapter and +sends `service_tier: "priority"` through Chat Completions. `ocx login xai` +instead stores OAuth credentials for the separate Grok CLI subscription-gateway flow, so OAuth +remains unclassified: its catalog rows do not advertise Fast and the proxy does not inject a tier. + +xAI charges Priority Processing at 2× the standard token price for input, output, cached, and +reasoning tokens; cache discounts are applied before the multiplier. Cost estimates use that premium +only when xAI's response confirms `service_tier: "priority"`. A missing or unparsed response tier is +not confirmation, and an echoed `default` is a downgrade; all three stay at the standard price. + +For `grok-4.6`, the standard rate per 1M tokens is $2.00 input, $0.50 cached input, and $6.00 +output. A prompt of at least 200,000 tokens reprices the whole request at $4.00 / $1.00 / $12.00. +xAI has not published how that long-context band combines with Priority Processing. When a +long-context response confirms `priority`, the dashboard therefore shows the published long-context +cost with a `≥` marker and a lower-bound explanation; it never invents a stacked multiplier. + ### OpenRouter Fast The canonical `https://openrouter.ai/api/v1` preset advertises Fast only for these exact diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 1966d9db63..1dfe171a58 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -114,7 +114,7 @@ ocx logout | Провайдер | Адаптер | Базовый URL | Примечания | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | Каталог Grok загружается в реальном времени; фолбэк по умолчанию — `grok-4.5`. | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth использует отдельный шлюз подписки Grok CLI. Переопределение с API-ключом использует `https://api.x.ai/v1` и может добавлять Priority Processing. Каталог Grok загружается в реальном времени; фолбэк по умолчанию — `grok-4.5`. | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Модели Claude; актуальный список моделей загружается из `/v1/models`. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Модели Kimi K2.7/K2.6/K2.5 для кодинга. | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Шлюз подписки Nous Research (тот же бэкенд, что использует Hermes Agent). Вход по device grant против `portal.nousresearch.com`; access-токен — это JWT для каждого запроса к inference. Смешанный каталог платных + `:free` моделей (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, …) обнаруживается вживую по авторизованному аккаунту. Refresh-токены одноразовые и ротируются при каждом обновлении. | diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index ee153a0780..15e2ab3cf4 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -129,7 +129,7 @@ ocx logout | Sağlayıcı | Adaptör | Temel URL | Notlar | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | Canlı öncelikli Grok kataloğu; `grok-4.5` geri dönüş varsayılanıdır. | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth ayrı Grok CLI abonelik ağ geçidini kullanır. API anahtarı geçersiz kılması `https://api.x.ai/v1` kullanır ve Priority Processing ekleyebilir. Canlı öncelikli Grok kataloğu; `grok-4.5` geri dönüş varsayılanıdır. | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude modelleri; canlı model listesi `/v1/models` üzerinden getirilir. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 kodlama modelleri. | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research abonelik ağ geçidi (Hermes Agent'ın kullandığı aynı arka uç). `portal.nousresearch.com`'a karşı cihaz yetkilendirmesi girişi; erişim belirteci istek başına çıkarım JWT'sidir. Oturum açmış hesaptan canlı olarak keşfedilen karışık ücretli + `:free` model kataloğu (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, ...). Yenileme belirteçleri tek kullanımlıktır ve her yenilemede döndürülür. | diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index 4e924458ee..a73676e496 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -95,7 +95,7 @@ ocx logout | 提供商 | Adapter | 基础 URL | 备注 | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | 优先使用实时 Grok 目录;回退默认模型为 `grok-4.5`。 | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth 使用独立的 Grok CLI 订阅网关。API 密钥覆盖模式使用 `https://api.x.ai/v1`,并可能注入 Priority Processing。优先使用实时 Grok 目录;回退默认模型为 `grok-4.5`。 | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 模型;实时模型列表从 `/v1/models` 获取。 | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 编程模型。 | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research 订阅网关(与 Hermes Agent 使用同一后端)。通过设备授权登录 `portal.nousresearch.com`;access 令牌是每个请求的 inference JWT。付费 + `:free` 模型混合目录(`tencent/hy3:free`、`stepfun/step-3.7-flash:free` 等)会从已登录账户实时发现。Refresh 令牌是单次使用,每次刷新都会轮换。 | diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index ee7d709880..28298c768a 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -103,7 +103,7 @@ ocx logout | 供應商 | Adapter | Base URL | 備註 | | --- | --- | --- | --- | -| `xai` | `openai-chat` | `https://api.x.ai/v1` | 優先使用即時 Grok catalog;fallback 預設為 `grok-4.5`。 | +| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth 使用獨立的 Grok CLI 訂閱 gateway。API key 覆寫使用 `https://api.x.ai/v1`,並可能注入 Priority Processing。優先使用即時 Grok catalog;fallback 預設為 `grok-4.5`。 | | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 模型;即時模型列表從 `/v1/models` 取得。 | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 coding 模型。 | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research 訂閱 gateway(Hermes Agent 使用相同 backend)。透過 `portal.nousresearch.com` 做 device-grant 登入;access token 是每次請求使用的 inference JWT。混合付費與 `:free` 模型 catalog(`tencent/hy3:free`、`stepfun/step-3.7-flash:free` 等)會從已登入帳號即時探索。Refresh token 為單次使用,每次 refresh 都會輪換。 | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 8e64e29b97..41ae5d02e5 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -717,7 +717,7 @@ export const de: Record = { "logs.detail.estimate.cache_detail_missing": "Cache-Details fehlen; Eingabe ist als Obergrenze geschätzt.", "logs.detail.estimate.expected_price_overlay": "Ein verifizierter Expected-Listenpreis wurde verwendet.", "logs.detail.estimate.provider_cost_overlay": "Ein vom Anbieter konfiguriertes Preis-Overlay wurde verwendet.", - "logs.detail.estimate.priority_lower_bound": "Der bestätigte OpenRouter-Priority-Preis ist nicht verfügbar; die angezeigte Standardpreisschätzung ist eine bekannte Untergrenze.", + "logs.detail.estimate.priority_lower_bound": "Der bestätigte Priority-Preis ist nicht verfügbar; die angezeigte Schätzung ist eine bekannte Untergrenze.", "logs.col.error": "Fehler", "logs.col.upstreamReason": "Upstream-Grund", "logs.col.duration": "Dauer", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 262e913162..01d69f0fd1 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -750,7 +750,7 @@ export const en = { "logs.detail.estimate.cache_detail_missing": "Cache details were unavailable; input is an upper-bound estimate.", "logs.detail.estimate.expected_price_overlay": "A verified expected list price was used.", "logs.detail.estimate.provider_cost_overlay": "A provider-configured price overlay was used.", - "logs.detail.estimate.priority_lower_bound": "The confirmed OpenRouter priority price is unavailable; the displayed standard-price estimate is a known lower bound.", + "logs.detail.estimate.priority_lower_bound": "The confirmed Priority price is unavailable; the displayed estimate is a known lower bound.", "logs.col.error": "Error", "logs.col.upstreamReason": "Upstream reason", "logs.col.duration": "Duration", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 147773fd93..3765f4d95f 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -731,7 +731,7 @@ export const fr: Record = { "logs.detail.estimate.cache_detail_missing": "Les détails du cache n’étaient pas disponibles ; l’entrée est une estimation de la limite supérieure.", "logs.detail.estimate.expected_price_overlay": "Un tarif catalogue attendu et vérifié a été utilisé.", "logs.detail.estimate.provider_cost_overlay": "Un remplacement de tarif configuré pour le fournisseur a été utilisé.", - "logs.detail.estimate.priority_lower_bound": "Le tarif Priority OpenRouter confirmé n’est pas disponible ; l’estimation au tarif standard affichée est une borne inférieure connue.", + "logs.detail.estimate.priority_lower_bound": "Le tarif Priority confirmé n’est pas disponible ; l’estimation affichée est une borne inférieure connue.", "logs.col.error": "Erreur", "logs.col.upstreamReason": "Motif en amont", "logs.col.duration": "Durée", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 11a85127ea..63e20efd3b 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -693,7 +693,7 @@ export const ja: Record = { "logs.detail.estimate.cache_detail_missing": "キャッシュの詳細が利用できませんでした; 入力は上限の推定です。", "logs.detail.estimate.expected_price_overlay": "検証済みの予想定価が使用されました。", "logs.detail.estimate.provider_cost_overlay": "プロバイダー設定の価格オーバーレイが使用されました。", - "logs.detail.estimate.priority_lower_bound": "確認済みの OpenRouter Priority 価格は取得できないため、表示される標準価格の見積もりは既知の下限です。", + "logs.detail.estimate.priority_lower_bound": "確認済みの Priority 価格を利用できないため、表示される見積もりは既知の下限です。", "logs.col.error": "エラー", "logs.col.upstreamReason": "上流の理由", "logs.col.duration": "所要時間", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 5bbc14ae7d..b13cd141ff 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -736,7 +736,7 @@ export const ko: Record = { "logs.detail.estimate.cache_detail_missing": "캐시 상세가 없어 입력 전액을 상한으로 추정했습니다.", "logs.detail.estimate.expected_price_overlay": "검증된 expected 정가를 사용했습니다.", "logs.detail.estimate.provider_cost_overlay": "프로바이더 구성 가격 오버레이를 사용했습니다.", - "logs.detail.estimate.priority_lower_bound": "확인된 OpenRouter Priority 가격을 사용할 수 없어 표시된 표준 가격 추정치는 알려진 하한입니다.", + "logs.detail.estimate.priority_lower_bound": "확인된 Priority 가격을 사용할 수 없어 표시된 추정치는 알려진 하한입니다.", "logs.col.error": "오류", "logs.col.upstreamReason": "업스트림 원인", "logs.col.duration": "소요 시간", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 396ccf3ed0..05cf793779 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -734,7 +734,7 @@ export const ru: Record = { "logs.detail.estimate.cache_detail_missing": "Детализация кэша недоступна; входные токены оценены по верхней границе.", "logs.detail.estimate.expected_price_overlay": "Использована подтверждённая ожидаемая цена из прайс-листа.", "logs.detail.estimate.provider_cost_overlay": "Использован ценовой оверлей провайдера.", - "logs.detail.estimate.priority_lower_bound": "Подтверждённая цена OpenRouter Priority недоступна; показанная оценка по стандартной цене является известной нижней границей.", + "logs.detail.estimate.priority_lower_bound": "Подтверждённая цена Priority недоступна; показанная оценка является известной нижней границей.", "logs.col.error": "Ошибка", "logs.col.upstreamReason": "Причина от провайдера", "logs.col.duration": "Длительность", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index f460bbeb36..c98421183e 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -741,7 +741,7 @@ export const tr: Record = { "logs.detail.estimate.cache_detail_missing": "Önbellek detayları eksik.", "logs.detail.estimate.expected_price_overlay": "Doğrulanmış liste fiyatı kullanıldı.", "logs.detail.estimate.provider_cost_overlay": "Kullanıcı tarafından yapılandırılan bir sağlayıcı fiyat katmanı kullanıldı.", - "logs.detail.estimate.priority_lower_bound": "Doğrulanan OpenRouter Priority fiyatı kullanılamıyor; gösterilen standart fiyat tahmini bilinen bir alt sınırdır.", + "logs.detail.estimate.priority_lower_bound": "Doğrulanan Priority fiyatı kullanılamıyor; gösterilen tahmin bilinen bir alt sınırdır.", "logs.col.error": "Hata", "logs.col.upstreamReason": "Yukarı akış nedeni", "logs.col.duration": "Süre", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 942e21e61f..d35a96bd34 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1818,7 +1818,7 @@ export const zhTW: Record = { "logs.detail.attempt.recovery.emptyCompletion": "空白完成重試", "logs.detail.attempt.recovery.unknown": "未知的復原原因", "logs.detail.estimate.provider_cost_overlay": "已使用供應商設定的價格覆蓋。", - "logs.detail.estimate.priority_lower_bound": "無法取得已確認的 OpenRouter Priority 價格;目前顯示的標準價格估算是已知下限。", + "logs.detail.estimate.priority_lower_bound": "無法取得已確認的 Priority 價格;目前顯示的估算是已知下限。", "pws.cockpitImportDescription": "從此裝置匯入 Cockpit Tools Antigravity JSON 匯出檔。不會顯示檔案內容。", "pws.cockpitImportFileLabel": "Cockpit Tools Antigravity JSON 匯出檔", "pws.cockpitImportChooseFile": "選擇 JSON 檔案", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index b9cd4a3573..dc749ac530 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -729,7 +729,7 @@ export const zh: Record = { "logs.detail.estimate.cache_detail_missing": "缺少缓存明细;输入费用按上限估算。", "logs.detail.estimate.expected_price_overlay": "使用了已验证的 Expected 标价。", "logs.detail.estimate.provider_cost_overlay": "使用了用户配置的提供方价格覆盖。", - "logs.detail.estimate.priority_lower_bound": "暂无已确认的 OpenRouter Priority 价格;当前显示的标准价估算是已知下界。", + "logs.detail.estimate.priority_lower_bound": "暂无已确认的 Priority 价格;当前显示的估算是已知下界。", "logs.col.error": "错误", "logs.col.upstreamReason": "上游原因", "logs.col.duration": "耗时", diff --git a/gui/tests/logs-cost-lower-bound.test.ts b/gui/tests/logs-cost-lower-bound.test.ts new file mode 100644 index 0000000000..60759219fa --- /dev/null +++ b/gui/tests/logs-cost-lower-bound.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from "bun:test"; +import { DICTS } from "../src/i18n/catalogs"; +import { interpolate, type Locale, type TFn } from "../src/i18n/shared"; +import { + formatEstimatedUsd, + formatEstimatedUsdValue, + summarizeEstimatedCosts, +} from "../src/pages/logs-cost-format"; + +function translator(locale: Locale): TFn { + return (key, vars) => interpolate(DICTS[locale][key], vars); +} + +test("ordinary dashboard costs retain the estimate marker", () => { + expect(formatEstimatedUsdValue(0.77, translator("en"), "en-US", false)).toBe("~$0.7700"); +}); + +test("priority long-context lower bounds render with a greater-than-or-equal marker", () => { + expect(formatEstimatedUsdValue(0.77, translator("en"), "en-US", true)).toBe("≥$0.7700"); +}); + +test("USD placement and separators follow a non-English locale", () => { + expect(formatEstimatedUsdValue(0.77, translator("de"), "de-DE", false)).toBe("ca. 0,7700\u00a0$"); + expect(formatEstimatedUsd({ kind: "unavailable" }, translator("de"), "de-DE")).toBe("nicht verfügbar"); +}); + +describe("conversation cost lower-bound aggregation", () => { + const priced = (total: number, lowerBound: boolean) => ({ + usageStatus: "reported", + displayMetrics: { + cost: { + kind: "value" as const, + estimate: { cost: { total }, priorityLowerBound: lowerBound }, + }, + }, + }); + + test("marks a total only when every included priced estimate is a lower bound", () => { + expect(summarizeEstimatedCosts([priced(0.77, true), priced(1.23, true)])).toMatchObject({ + estimatedCostUsd: 2, + priorityLowerBound: true, + }); + expect(summarizeEstimatedCosts([priced(0.77, true), priced(1.23, false)])).toMatchObject({ + estimatedCostUsd: 2, + priorityLowerBound: false, + }); + }); + + test("preserves unpriced and unsupported exclusions without minting a lower bound", () => { + expect(summarizeEstimatedCosts([ + { usageStatus: "reported", displayMetrics: { cost: { kind: "unavailable" } } }, + { usageStatus: "unsupported" }, + ])).toEqual({ + estimatedCostUsd: 0, + priorityLowerBound: false, + unpricedRequests: 1, + unmeteredRequests: 1, + }); + }); +}); diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index 3bf5daa086..0648b64d17 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -147,7 +147,7 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel) entry.service_tiers = [{ id: "priority", name: "Fast", - description: "1.5x speed, increased usage", + description: model.fastTierDescription ?? "1.5x speed, increased usage", }]; entry.additional_speed_tiers = ["fast"]; } diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 0d1b2c2aaa..a2a1c86c7c 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -125,6 +125,8 @@ export interface CatalogModel { supportsVerbosity?: boolean; /** Whether this exact routed model has a verified OpenAI-compatible service tier. */ supportsServiceTier?: boolean; + /** Optional provider-specific copy for the advertised Fast tier. */ + fastTierDescription?: string; supportsReasoningSummaries?: boolean; /** * Codex tool calling mode for this routed model. diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 9c3384b4e3..565f6057a4 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -34,7 +34,8 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive"; import { captureFastPolicyAuthority, - serviceTierSupportForModel, + fastPolicyForModel, + serviceTierSupportFromPolicy, } from "../../providers/service-tier"; import type { FastPolicyAuthority } from "../../providers/fastwire"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry"; @@ -647,8 +648,13 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, const reasoningEfforts = configuredReasoningEfforts(prov, model.id); const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort; const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id); - const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name); - const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model; + const fastPolicy = fastPolicyForModel(prov, model.id, name); + const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy); + const { + supportsServiceTier: _staleServiceTier, + fastTierDescription: _staleFastTierDescription, + ...modelWithoutServiceTier + } = model; // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。 const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow @@ -671,6 +677,9 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), + ...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined + ? { fastTierDescription: fastPolicy.fastTierDescription } + : {}), ...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}), // Default-on for openai-chat providers (explicit false opts out); other adapters // advertise only on explicit opt-in. @@ -1845,8 +1854,11 @@ async function gatherRoutedModelsUncached( ? nativeDefaultReasoningEffort(cm.modelId) : undefined; const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId); - const supportsServiceTier = effectiveProvider - ? serviceTierSupportForModel(effectiveProvider, cm.modelId, cm.provider) + const fastPolicy = effectiveProvider + ? fastPolicyForModel(effectiveProvider, cm.modelId, cm.provider) + : undefined; + const supportsServiceTier = fastPolicy + ? serviceTierSupportFromPolicy(fastPolicy) : undefined; const base: CatalogModel = { id: cm.modelId, @@ -1883,6 +1895,9 @@ async function gatherRoutedModelsUncached( ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}), ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), + ...(supportsServiceTier === true && fastPolicy?.fastTierDescription !== undefined + ? { fastTierDescription: fastPolicy.fastTierDescription } + : {}), ...(cm.codexToolMode !== undefined ? { codexToolMode: cm.codexToolMode } : effectiveProvider?.codexToolMode !== undefined diff --git a/src/providers/fastwire.ts b/src/providers/fastwire.ts index 63cd642532..9098bd9df9 100644 --- a/src/providers/fastwire.ts +++ b/src/providers/fastwire.ts @@ -33,6 +33,7 @@ export interface FastPolicyAuthority { readonly providerAdapter: string; readonly providerAuthMode?: ProviderAuthKind; readonly fastWireDeclaration: FastWire | null | undefined; + readonly fastTierDescription?: string; readonly modelWireOverrideAllowed: boolean; readonly authTransport: FastPolicyAuthTransport; readonly capability: { @@ -55,6 +56,7 @@ export interface ResolvedFastPolicy { | "pin-unavailable"; readonly adapter: string; readonly fastWire: FastWire | null; + readonly fastTierDescription?: string; readonly forwardCallerTier: boolean; } @@ -222,7 +224,16 @@ export function resolveFastPolicy( else if (capability === undefined) eligibility = "unclassified"; else eligibility = "eligible"; - return { capability, eligibility, adapter, fastWire, forwardCallerTier }; + return { + capability, + eligibility, + adapter, + fastWire, + ...(authority.fastTierDescription !== undefined + ? { fastTierDescription: authority.fastTierDescription } + : {}), + forwardCallerTier, + }; } export function canonicalFastTierMarker(callerTier: string | undefined): "priority" | undefined { diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 4e61bb1e68..32a65a7af5 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -227,6 +227,18 @@ export interface ProviderRegistryEntry { supportsOpenAiWebSearchToolFields?: boolean; /** Registry default for exact model service-tier capability; explicit config keys win. */ modelSupportsServiceTier?: Record; + /** + * Registry-only service-tier defaults for an OAuth preset's explicit API-key transport. + * Applied only when `allowKeyAuthOverride` is true and the captured effective auth transport + * is key-based. Explicit provider config still wins field-by-field, including `false`. + */ + keyAuthServiceTier?: { + supportsServiceTier?: boolean; + modelSupportsServiceTier?: Record; + chatServiceTier?: boolean; + }; + /** Provider-specific copy for the Codex catalog's Fast tier. */ + fastTierDescription?: string; /** * Registry-only destination guard for `modelSupportsServiceTier`. This scopes vendor evidence * without changing provider ownership, routing, authentication, or config validation. @@ -1023,6 +1035,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ baseUrl: "https://api.x.ai/v1", authKind: "oauth", allowKeyAuthOverride: true, + // Priority Processing is documented for xAI's public API-key Chat Completions and + // Responses endpoints. OAuth is a separate Grok CLI subscription gateway and remains + // unclassified; do not turn this into a provider-wide supportsServiceTier declaration. + keyAuthServiceTier: { + supportsServiceTier: true, + chatServiceTier: true, + }, + fastTierDescription: "Priority processing, 2x token price", featured: true, oauthId: "xai", jawcodeBundle: "xai", diff --git a/src/providers/service-tier.ts b/src/providers/service-tier.ts index 2a09530d23..de71cb3f9e 100644 --- a/src/providers/service-tier.ts +++ b/src/providers/service-tier.ts @@ -70,11 +70,22 @@ function buildFastPolicyAuthority( capabilityProvider: ServiceTierCapabilityProvider = provider, ): FastPolicyAuthority { const registry = registryTransportMatch ? getProviderRegistryEntry(providerName) : undefined; + const authTransport = resolveProviderAuthTransport( + provider.adapter, + provider.authMode ?? registry?.authKind ?? "key", + provider.apiKeyTransport, + ); + const keyAuthDefaults = registry?.allowKeyAuthOverride === true + && (authTransport === "authorization_bearer" || authTransport === "x_api_key") + ? registry.keyAuthServiceTier + : undefined; const registryModelCapabilities = registry && registryModelServiceTierCapabilityApplies(registry, capabilityProvider) ? registry.modelSupportsServiceTier : undefined; - const providerCapability = capabilityProvider.supportsServiceTier ?? registry?.supportsServiceTier; + const providerCapability = capabilityProvider.supportsServiceTier + ?? keyAuthDefaults?.supportsServiceTier + ?? registry?.supportsServiceTier; const authority: FastPolicyAuthority = Object.freeze({ providerAdapter: provider.adapter, providerAuthMode: provider.authMode ?? registry?.authKind ?? "key", @@ -82,19 +93,23 @@ function buildFastPolicyAuthority( provider.fastWire !== undefined ? provider.fastWire : registry?.fastWire, { freeze: true }, ), + ...(registry?.fastTierDescription !== undefined + ? { fastTierDescription: registry.fastTierDescription } + : {}), modelWireOverrideAllowed: !isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig), - authTransport: resolveProviderAuthTransport( - provider.adapter, - provider.authMode ?? registry?.authKind ?? "key", - provider.apiKeyTransport, - ), + authTransport, capability: Object.freeze({ ...(providerCapability !== undefined ? { provider: providerCapability } : {}), models: Object.freeze({ ...(registryModelCapabilities ?? {}), + ...(keyAuthDefaults?.modelSupportsServiceTier ?? {}), ...(capabilityProvider.modelSupportsServiceTier ?? {}), }), - ...(provider.chatServiceTier !== undefined ? { chatServiceTier: provider.chatServiceTier } : {}), + ...(provider.chatServiceTier !== undefined + ? { chatServiceTier: provider.chatServiceTier } + : keyAuthDefaults?.chatServiceTier !== undefined + ? { chatServiceTier: keyAuthDefaults.chatServiceTier } + : {}), }), modelAdapters: Object.freeze({ ...(provider.modelAdapters ?? {}) }), hardPins: captureWireAdapterHardPins(providerName), diff --git a/src/usage/cost.ts b/src/usage/cost.ts index 608dea31ac..d507668a84 100644 --- a/src/usage/cost.ts +++ b/src/usage/cost.ts @@ -22,7 +22,8 @@ import { activeConfiguredProviders, activeUserCostOverlays, userCostOverlayVersi import { EXPECTED_PRICE_OVERLAYS, findExpectedPriceOverlay, - resolvePriorityMultiplier, + findVerifiedPriceOverride, + findPriorityPricingRule, findContextTier, isLongContext, type Cost4, @@ -81,12 +82,12 @@ export interface AttemptCostEstimate { price: MatchedPrice; cost: CostBreakdown; estimated: boolean; - /** Applied OpenAI priority-tier multiplier (undefined or 1 = standard). */ + /** Applied provider priority-tier multiplier (undefined or 1 = standard). */ priorityMultiplier?: number; - /** Standard-price estimate is a known floor for a confirmed, unpriced priority endpoint. */ - priorityLowerBound?: boolean; /** Set when the published long-context rate was applied (#908). */ contextTier?: ContextTierName; + /** The numeric estimate is a known floor because the exact Priority price is unavailable. */ + priorityLowerBound?: boolean; } export interface CostEstimate { @@ -95,12 +96,12 @@ export interface CostEstimate { estimated: boolean; attempts?: AttemptCostEstimate[]; price?: MatchedPrice; - /** Applied OpenAI priority-tier multiplier (undefined or 1 = standard). */ + /** Applied provider priority-tier multiplier (undefined or 1 = standard). */ priorityMultiplier?: number; - /** Standard-price estimate is a known floor for a confirmed, unpriced priority endpoint. */ - priorityLowerBound?: boolean; /** Set when any priced attempt used the published long-context rate (#908). */ contextTier?: ContextTierName; + /** The aggregate is a known floor because every priced attempt is a lower bound. */ + priorityLowerBound?: boolean; } function finiteNonNegative(value: number): boolean { @@ -236,7 +237,7 @@ function resolveMatchedPriceInner( /** * Exact provider/model price lookup: user-configured `modelCosts` first, then - * the jawcode provider bundle, then the expected-price overlay, then the + * an exact official correction, the jawcode provider bundle, the expected-price overlay, then the * model-level vendor fallback. All-zero rows fall through ("not billable"). */ function resolveMatchedPriceExact( @@ -249,6 +250,20 @@ function resolveMatchedPriceExact( // operator's explicit price is authoritative for the ~$ estimate. const userOverlay = userOverlayMatch(provider, modelId, userOverlays); if (userOverlay) return userOverlay; + const verifiedOverride = overlays === EXPECTED_PRICE_OVERLAYS + ? findVerifiedPriceOverride(provider, modelId) + : undefined; + if (verifiedOverride && validCost4(verifiedOverride.cost4) && hasNonZeroCost(verifiedOverride.cost4)) { + return { + provider, + modelId, + cost4: verifiedOverride.cost4, + source: "expected", + sourceRef: verifiedOverride.source, + verifiedAt: verifiedOverride.verifiedAt, + status: "verified", + }; + } const metadataProvider = resolveMetadataProvider(provider); const bundled = metadataProvider ? getModelMetadata(metadataProvider, modelId) @@ -320,13 +335,6 @@ function isEstimated(usage: OcxUsage, usageStatus: UsageStatus, priceStatus: Exp return usage.estimated === true || usageStatus === "estimated" || priceStatus === "verified-derived"; } -/** - * OpenAI provider ids eligible for service_tier "priority" price multipliers. - * Only canonical OpenAI forward providers use the priority tier; routed providers - * (OpenRouter, Cursor, etc.) may share model slugs but have independent pricing. - */ -const OPENAI_TIER_PROVIDER_IDS = new Set(["openai", "openai-apikey"]); - /** * Resolve the effective service tier from persisted log fields. * Priority: responseServiceTier (server-confirmed) > requestedServiceTier @@ -405,9 +413,9 @@ function isConfirmedFast(tier?: ServiceTierInput): boolean { * normalized billable input — normalization subtracts cache read/write, so a * cache-heavy long prompt would fall below the boundary and under-bill. * - * Skipped entirely for a response-confirmed Fast request: OpenAI does not serve - * long context in Fast mode, so the two are mutually exclusive regimes rather - * than composable multipliers. + * A provider's declaration decides how a response-confirmed priority tier relates to this band. + * OpenAI declares the bands exclusive. xAI publishes neither a combined rate nor an exclusion, + * so its long-context rate remains the known lower bound instead of inventing a stacked multiplier. */ function applyContextTier( cost4: Cost4, @@ -415,25 +423,27 @@ function applyContextTier( modelId: string, rawInputTokens: number | undefined, tier?: ServiceTierInput, -): [Cost4, ContextTierName | undefined] { - if (rawInputTokens === undefined) return [cost4, undefined]; - if (isConfirmedFast(tier)) return [cost4, undefined]; +): [Cost4, ContextTierName | undefined, boolean] { + if (rawInputTokens === undefined) return [cost4, undefined, false]; const rule = findContextTier(baseProviderLabel(provider), modelId); - if (!rule || !isLongContext(rule, rawInputTokens)) return [cost4, undefined]; + if (!rule || !isLongContext(rule, rawInputTokens)) return [cost4, undefined, false]; + const confirmedFast = isConfirmedFast(tier); + if (confirmedFast && rule.confirmedPriorityRelation === "exclusive") { + return [cost4, undefined, false]; + } return [{ input: cost4.input * rule.multiplier.input, output: cost4.output * rule.multiplier.output, cacheRead: cost4.cacheRead * rule.multiplier.cacheRead, cacheWrite: cost4.cacheWrite * rule.multiplier.cacheWrite, - }, "long"]; + }, "long", confirmedFast && rule.confirmedPriorityRelation === "lower-bound"]; } /** - * Apply the OpenAI priority-tier multiplier to a Cost4 when applicable. + * Apply a declared provider/model priority-tier multiplier to a Cost4 when applicable. * Returns [effectiveCost4, multiplier]. Multiplier is 1 (no-op) when: * - serviceTier is not "priority" - * - provider is not a canonical OpenAI forward provider - * - model is not in PRIORITY_MULTIPLIERS + * - no exact provider/model rule exists */ function applyPriorityMultiplier( cost4: Cost4, @@ -443,8 +453,9 @@ function applyPriorityMultiplier( ): [Cost4, number] { if (tierScalar(serviceTier) !== "priority") return [cost4, 1]; const base = baseProviderLabel(provider); - if (!OPENAI_TIER_PROVIDER_IDS.has(base)) return [cost4, 1]; - const multiplier = resolvePriorityMultiplier(modelId); + const rule = findPriorityPricingRule(base, modelId); + if (rule?.requiresResponseConfirmation && !isConfirmedFast(serviceTier)) return [cost4, 1]; + const multiplier = rule?.multiplier ?? 1; if (multiplier === 1) return [cost4, 1]; return [{ input: cost4.input * multiplier, @@ -495,16 +506,17 @@ export function estimateAttemptCost( const attemptServiceTier = attempt.tierOutcome ? serviceTierContextFromOutcome(attempt.tierOutcome) : serviceTier; - const [tieredCost4, contextTier] = applyContextTier( + const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier( price.cost4, attempt.provider, attempt.model, attempt.usage.inputTokens, attemptServiceTier, ); - // Exclusive both ways: if the long rate applied, the request was NOT served as - // Fast (Fast does not support long context), so the Fast multiplier must not - // also apply — otherwise a downgraded request bills at both rates. + // A published long-context row owns the numeric estimate. OpenAI declares that band + // exclusive with Fast; xAI's confirmed combination is deliberately left unmultiplied + // and marked as a lower bound because no combined price has been published. const [effectiveCost4, multiplier] = contextTier ? [tieredCost4, 1] as const : applyPriorityMultiplier(tieredCost4, attempt.provider, attempt.model, attemptServiceTier); - const priorityLowerBound = isOpenRouterPriorityLowerBound(attempt.provider, attempt.tierOutcome); + const priorityLowerBound = contextPriorityLowerBound + || isOpenRouterPriorityLowerBound(attempt.provider, attempt.tierOutcome); return { ordinal: attempt.ordinal, provider: attempt.provider, @@ -514,8 +526,8 @@ export function estimateAttemptCost( cost: calculateCost(tokens, effectiveCost4), estimated: isEstimated(attempt.usage, attempt.usageStatus, price.status), ...(multiplier !== 1 ? { priorityMultiplier: multiplier } : {}), - ...(priorityLowerBound ? { priorityLowerBound: true } : {}), ...(contextTier ? { contextTier } : {}), + ...(priorityLowerBound ? { priorityLowerBound: true } : {}), }; } @@ -557,8 +569,10 @@ export function estimateComboCost( ...(estimates.some(est => est.priorityMultiplier && est.priorityMultiplier !== 1) ? { priorityMultiplier: estimates.find(est => est.priorityMultiplier)?.priorityMultiplier } : {}), - ...(estimates.some(est => est.priorityLowerBound) ? { priorityLowerBound: true } : {}), ...(estimates.some(est => est.contextTier) ? { contextTier: "long" as const } : {}), + ...(estimates.every(est => est.priorityLowerBound === true) + ? { priorityLowerBound: true as const } + : {}), }; } @@ -579,13 +593,13 @@ export function estimateRequestCost( if (!tokens) return null; const price = resolveMatchedPrice(input.provider, input.model, overlays, userOverlays); if (!price) return null; - const [tieredCost4, contextTier] = applyContextTier( + const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier( price.cost4, input.provider, input.model, input.usage.inputTokens, input.serviceTier, ); const [effectiveCost4, multiplier] = contextTier ? [tieredCost4, 1] as const : applyPriorityMultiplier(tieredCost4, input.provider, input.model, input.serviceTier); - const priorityLowerBound = isOpenRouterPriorityLowerBound( + const priorityLowerBound = contextPriorityLowerBound || isOpenRouterPriorityLowerBound( input.provider, typeof input.serviceTier === "object" ? input.serviceTier.tierOutcome : undefined, ); @@ -595,8 +609,8 @@ export function estimateRequestCost( cost: calculateCost(tokens, effectiveCost4), estimated: isEstimated(input.usage, input.usageStatus, price.status), ...(multiplier !== 1 ? { priorityMultiplier: multiplier } : {}), - ...(priorityLowerBound ? { priorityLowerBound: true } : {}), ...(contextTier ? { contextTier } : {}), + ...(priorityLowerBound ? { priorityLowerBound: true } : {}), }; } diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index 64a42e87e7..af94b8b262 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -181,6 +181,30 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ { provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" }, ]; +/** + * Exact official corrections for stale nonzero catalog rows. These are intentionally separate + * from fallback overlays: they win over the bundled row only for the declared provider/model and + * therefore cannot reprice routed resellers that reuse the same model slug. + */ +export const VERIFIED_PRICE_OVERRIDES: readonly ExpectedPriceOverlay[] = [ + { + provider: "xai", + modelId: "grok-4.6", + cost4: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 }, + source: "https://docs.x.ai/developers/pricing", + verifiedAt: "2026-08-18", + status: "verified", + }, +]; + +export function findVerifiedPriceOverride( + provider: string, + modelId: string, + overrides: readonly ExpectedPriceOverlay[] = VERIFIED_PRICE_OVERRIDES, +): ExpectedPriceOverlay | undefined { + return overrides.find(row => row.provider === provider && row.modelId === modelId); +} + /** * Exact-key overlay lookup. Returns verified first, then verified-derived. * NEVER returns "unverified" rows — fail-closed is enforced in code, not just docs. @@ -196,12 +220,7 @@ export function findExpectedPriceOverlay( ?? exact.find(row => row.status === "verified-derived"); } -/** - * OpenAI Fast mode (`service_tier=priority`) price multipliers by model slug. - * Source: https://openai.com/api-fast-mode/ (2026-07-31). - * Fast pricing applies uniformly to all token types (input, output, cache). - * Models not listed here fall back to 1× (no multiplier). - */ +/** OpenAI Fast price multipliers retained as a compatibility export. */ export const PRIORITY_MULTIPLIERS: Readonly> = { "gpt-5.6-sol": 2, // Post-price-cut Fast tables (https://openai.com/api-fast-mode/, 2026-08-05): @@ -219,6 +238,52 @@ export function resolvePriorityMultiplier(modelId: string): number { return PRIORITY_MULTIPLIERS[modelId] ?? 1; } +export interface PriorityPricingRule { + provider: string; + modelId: string; + multiplier: number; + /** Apply the premium only after the upstream response confirms this tier. */ + requiresResponseConfirmation?: true; + source: string; + verifiedAt: string; +} + +const OPENAI_FAST_PRICING = "https://openai.com/api-fast-mode/"; +const XAI_PRIORITY_PRICING = "https://docs.x.ai/developers/advanced-api-usage/priority-processing"; + +/** + * Exact provider/model priority premiums. Routed resellers never inherit a vendor rule merely + * because they reuse its model slug. Multipliers apply uniformly after cache discounts. + */ +export const PRIORITY_PRICING_RULES: readonly PriorityPricingRule[] = [ + ...["openai", "openai-apikey"].flatMap(provider => + Object.entries(PRIORITY_MULTIPLIERS).map(([modelId, multiplier]): PriorityPricingRule => ({ + provider, + modelId, + multiplier, + source: OPENAI_FAST_PRICING, + verifiedAt: "2026-08-05", + })), + ), + ...["grok-4.5", "grok-4.6"].map((modelId): PriorityPricingRule => ({ + provider: "xai", + modelId, + multiplier: 2, + requiresResponseConfirmation: true, + source: XAI_PRIORITY_PRICING, + verifiedAt: "2026-08-18", + })), +]; + +/** Exact provider/model priority-pricing lookup. */ +export function findPriorityPricingRule( + provider: string, + modelId: string, + rules: readonly PriorityPricingRule[] = PRIORITY_PRICING_RULES, +): PriorityPricingRule | undefined { + return rules.find(rule => rule.provider === provider && rule.modelId === modelId); +} + /** * Long-context pricing tiers (#908). Several vendors reprice the ENTIRE request * once the prompt crosses a published input-token threshold, so a flat Cost4 @@ -244,6 +309,8 @@ export interface ContextTier { inclusive: boolean; /** Per-field factor from the short rate to the published long rate. */ multiplier: Cost4; + /** Published relationship between confirmed priority and long-context bands. */ + confirmedPriorityRelation?: "exclusive" | "lower-bound"; source: string; verifiedAt: string; } @@ -277,6 +344,7 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [ thresholdInputTokens: 272_000, inclusive: false, multiplier: OPENAI_LONG_CONTEXT, + confirmedPriorityRelation: "exclusive", source: OPENAI_PRICING_DOC, verifiedAt: "2026-08-03", })), @@ -287,19 +355,21 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [ thresholdInputTokens: 200_000, inclusive: true, multiplier: UNIFORM_DOUBLE, + confirmedPriorityRelation: "lower-bound", source: "https://docs.x.ai/developers/pricing", verifiedAt: "2026-08-03", }, { - // 260813: grok-4.6 long-context tier mirrored from grok-4.5; the official pricing row - // was not yet published when the model page went up, so treat as provisional. + // xAI publishes the whole-request >=200k band for grok-4.6. Its combination with + // Priority Processing is not published, so confirmed priority uses this row as a lower bound. provider: "xai", modelId: "grok-4.6", thresholdInputTokens: 200_000, inclusive: true, multiplier: UNIFORM_DOUBLE, + confirmedPriorityRelation: "lower-bound", source: "https://docs.x.ai/developers/pricing", - verifiedAt: "2026-08-13", + verifiedAt: "2026-08-18", }, { // daybreak-blue-latest aliases gpt-5.6-sol, which publishes the full long-context row diff --git a/tests/fastwire-policy.test.ts b/tests/fastwire-policy.test.ts index 412d0524a4..a29debcb34 100644 --- a/tests/fastwire-policy.test.ts +++ b/tests/fastwire-policy.test.ts @@ -288,6 +288,7 @@ describe("resolveFastPolicy matrix", () => { settledCallerTier: undefined, }, { + // B2: key-auth Chat Completions is a documented Priority Processing transport. name: "xAI API-key default", providerName: "xai", modelIds: ["grok-4.6", "grok-4.5"], @@ -297,7 +298,7 @@ describe("resolveFastPolicy matrix", () => { authMode: "key" as const, }, adapter: "openai-chat", - forwardCallerTier: false, + forwardCallerTier: true, callerTier: undefined, settledCallerTier: undefined, }, diff --git a/tests/management-api-logs-metrics.test.ts b/tests/management-api-logs-metrics.test.ts index f7077a8951..0255005e2f 100644 --- a/tests/management-api-logs-metrics.test.ts +++ b/tests/management-api-logs-metrics.test.ts @@ -95,6 +95,31 @@ describe("GET /api/logs display metrics", () => { expect(dto!.displayMetrics.cost.estimateReasons).toContain("cache_detail_missing"); }); + test("confirmed xAI priority plus long context is exposed as a cost lower bound", async () => { + addRequestLog(baseEntry({ + provider: "xai", + model: "grok-4.6", + usage: { + inputTokens: 200_000, + outputTokens: 10_000, + cacheReadInputTokens: 50_000, + }, + tierOutcome: { + canonical: "priority", + wireKind: "service-tier", + wireValue: "priority", + fastOutcome: "applied", + confirmation: "confirmed", + responseServiceTier: "priority", + }, + })); + const [dto] = await readLogs(); + expect(dto!.displayMetrics.cost.kind).toBe("value"); + expect(dto!.displayMetrics.cost.estimate.priorityLowerBound).toBe(true); + expect(dto!.displayMetrics.cost.estimate.cost.total).toBeCloseTo(0.77, 9); + expect(dto!.displayMetrics.cost.estimateReasons).toContain("priority_lower_bound"); + }); + test("unmatched price is unavailable instead of zero", async () => { addRequestLog(baseEntry({ provider: "no-such-provider", diff --git a/tests/service-tier-capability.test.ts b/tests/service-tier-capability.test.ts index 49c051b8dc..fd1a418602 100644 --- a/tests/service-tier-capability.test.ts +++ b/tests/service-tier-capability.test.ts @@ -7,15 +7,22 @@ * ever receiving an injection (PR #860 family). */ import { afterEach, describe, expect, test } from "bun:test"; -import { applyProviderConfigHints } from "../src/codex/catalog"; +import { applyProviderConfigHints, buildCatalogEntries, gatherRoutedModels } from "../src/codex/catalog"; import { applyCatalogModelMetadata } from "../src/codex/catalog/effort"; import type { RawEntry } from "../src/codex/catalog/parsing"; import { providerConfigSeed, enrichProviderFromRegistry } from "../src/providers/derive"; import { getProviderRegistryEntry } from "../src/providers/registry"; +import { decideTier } from "../src/providers/fastwire"; import type { RequestLogContext } from "../src/server/request-log"; import { applyServiceTierGate, handleResponses } from "../src/server/responses/core"; -import { canForwardServiceTierForModel, serviceTierSupportForModel, supportsServiceTierForModel } from "../src/providers/service-tier"; -import { serviceTierAdapterForModel } from "../src/providers/service-tier"; +import { + canForwardServiceTierForModel, + fastPolicyForModel, + serviceTierAdapterForModel, + serviceTierSupportForModel, + serviceTierSupportFromPolicy, + supportsServiceTierForModel, +} from "../src/providers/service-tier"; import { candidateCapabilityEvidence } from "../src/routing/capability"; import { resolveProductionBehaviorValues } from "../src/routing/compatibility/behavior"; import type { OcxConfig, OcxProviderConfig } from "../src/types"; @@ -74,6 +81,92 @@ describe("registry capability reaches saved configs without overriding them", () }); }); +describe("xAI Fast capability follows the captured authentication transport", () => { + function xaiProvider( + authMode: "key" | "oauth", + overrides: Partial = {}, + ): OcxProviderConfig { + return { + ...providerConfigSeed(getProviderRegistryEntry("xai")!), + authMode, + apiKey: authMode === "key" ? "xai-test-key" : "oauth-test-token", + liveModels: false, + models: ["grok-4.6"], + ...overrides, + }; + } + + async function catalogEntry(provider: OcxProviderConfig) { + const models = await gatherRoutedModels({ + providers: { xai: provider }, + } as unknown as OcxConfig); + return buildCatalogEntries(null, [], models) + .find(entry => entry.slug === "xai/grok-4.6"); + } + + test("registry declares a key-auth overlay without classifying OAuth", () => { + const entry = getProviderRegistryEntry("xai")!; + expect(entry.keyAuthServiceTier).toEqual({ + supportsServiceTier: true, + chatServiceTier: true, + }); + expect(entry.supportsServiceTier).toBeUndefined(); + expect(entry.chatServiceTier).toBeUndefined(); + + const keyPolicy = fastPolicyForModel(xaiProvider("key"), "grok-4.6", "xai"); + expect(keyPolicy).toMatchObject({ + capability: true, + eligibility: "eligible", + forwardCallerTier: true, + fastTierDescription: "Priority processing, 2x token price", + }); + + const oauthPolicy = fastPolicyForModel(xaiProvider("oauth"), "grok-4.6", "xai"); + expect(oauthPolicy.capability).toBeUndefined(); + expect(oauthPolicy.eligibility).toBe("unclassified"); + expect(oauthPolicy.forwardCallerTier).toBe(false); + }); + + test("catalog and runtime publish the same key/OAuth conclusion", async () => { + const keyProvider = xaiProvider("key"); + const keyPolicy = fastPolicyForModel(keyProvider, "grok-4.6", "xai"); + const keyCatalog = await catalogEntry(keyProvider); + expect(serviceTierSupportFromPolicy(keyPolicy)).toBe(true); + expect(keyCatalog?.service_tiers).toEqual([{ + id: "priority", + name: "Fast", + description: "Priority processing, 2x token price", + }]); + expect(keyCatalog?.additional_speed_tiers).toEqual(["fast"]); + expect(decideTier(keyPolicy, true, undefined)).toEqual({ kind: "set", value: "priority" }); + + const oauthProvider = xaiProvider("oauth"); + const oauthPolicy = fastPolicyForModel(oauthProvider, "grok-4.6", "xai"); + const oauthCatalog = await catalogEntry(oauthProvider); + expect(serviceTierSupportFromPolicy(oauthPolicy)).toBe(false); + expect(oauthCatalog).not.toHaveProperty("service_tiers"); + expect(oauthCatalog).not.toHaveProperty("additional_speed_tiers"); + expect(decideTier(oauthPolicy, true, undefined)).toEqual({ kind: "drop" }); + }); + + test("explicit supportsServiceTier=false wins in policy and catalog for both transports", async () => { + for (const authMode of ["key", "oauth"] as const) { + const provider = xaiProvider(authMode, { supportsServiceTier: false }); + const policy = fastPolicyForModel( + provider, + "grok-4.6", + "xai", + ); + expect(policy.capability).toBe(false); + expect(policy.eligibility).toBe("capability-unsupported"); + expect(decideTier(policy, true, undefined)).toEqual({ kind: "drop" }); + const catalog = await catalogEntry(provider); + expect(catalog).not.toHaveProperty("service_tiers"); + expect(catalog).not.toHaveProperty("additional_speed_tiers"); + } + }); +}); + describe("service-tier capability is exact-model and provider-scoped", () => { test("an exact model entry overrides the provider fallback in both directions", () => { const provider: OcxProviderConfig = { @@ -264,6 +357,16 @@ describe("the gate fires on the live handleResponses path", () => { ({ ...providerConfigSeed(getProviderRegistryEntry("deepseek")!), apiKey: "sk-test" }); const openAiKeyProvider = (): OcxProviderConfig => ({ ...providerConfigSeed(getProviderRegistryEntry("openai-apikey")!), apiKey: "sk-test" }); + const xaiKeyProvider = (): OcxProviderConfig => ({ + ...providerConfigSeed(getProviderRegistryEntry("xai")!), + authMode: "key", + apiKey: "xai-test-key", + }); + const xaiOAuthProvider = (): OcxProviderConfig => ({ + ...providerConfigSeed(getProviderRegistryEntry("xai")!), + authMode: "oauth", + apiKey: "xai-oauth-test-token", + }); const openRouterProvider = (overrides: Partial = {}): OcxProviderConfig => { const provider: OcxProviderConfig = { ...providerConfigSeed(getProviderRegistryEntry("openrouter")!), @@ -321,6 +424,23 @@ describe("the gate fires on the live handleResponses path", () => { expect(body.service_tier).toBe("flex"); }); + test("xAI API-key runtime injects priority while OAuth does not", async () => { + const keyBody = await drive("xai", xaiKeyProvider(), "grok-4.6", {}, true); + expect(keyBody.service_tier).toBe("priority"); + const oauthBody = await drive("xai", xaiOAuthProvider(), "grok-4.6", {}, true); + expect(oauthBody).not.toHaveProperty("service_tier"); + for (const provider of [xaiKeyProvider(), xaiOAuthProvider()]) { + const optedOut = await drive( + "xai", + { ...provider, supportsServiceTier: false }, + "grok-4.6", + {}, + true, + ); + expect(optedOut).not.toHaveProperty("service_tier"); + } + }); + test("an unclassified custom Responses provider keeps caller values; only explicit false strips", async () => { const custom = (): OcxProviderConfig => ({ adapter: "openai-responses", baseUrl: "https://gateway.example.com/v1", apiKey: "sk-test" }); const preserved = await drive("custom-gw", custom(), "some-model", { service_tier: "priority" }); diff --git a/tests/usage-cost.test.ts b/tests/usage-cost.test.ts index 11fa3d77fc..362868f4f2 100644 --- a/tests/usage-cost.test.ts +++ b/tests/usage-cost.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { createAdapterTierMetadata } from "../src/providers/fastwire"; import { calculateCost, estimateAttemptCost, @@ -12,8 +13,10 @@ import { import { EXPECTED_PRICE_OVERLAYS, PRIORITY_MULTIPLIERS, + PRIORITY_PRICING_RULES, CONTEXT_TIERS, findExpectedPriceOverlay, + findPriorityPricingRule, resolvePriorityMultiplier, type ExpectedPriceOverlay, } from "../src/usage/expected-prices"; @@ -564,6 +567,169 @@ describe("priority (Fast) service tier multiplier", () => { }); }); +describe("xAI Priority Processing pricing", () => { + const usage = { + inputTokens: 100_000, + outputTokens: 10_000, + cacheReadInputTokens: 20_000, + }; + + function outcome(responseServiceTier?: string) { + const tracker = createAdapterTierMetadata( + { + capability: true, + eligibility: "eligible", + fastWire: { + kind: "service-tier", + canonicalToWire: { priority: "priority" }, + foreignCallerTiers: "verbatim", + }, + demandDecision: "force-fast", + }, + { kind: "set", value: "priority" }, + "service-tier", + "priority", + )!; + if (responseServiceTier !== undefined) tracker.observeResponseServiceTier(responseServiceTier); + return tracker.outcome; + } + + function estimate(tierOutcome: ReturnType, requestUsage = usage) { + return estimateAttemptCost({ + ordinal: 1, + provider: "xai", + model: "grok-4.6", + usageStatus: "reported", + usage: requestUsage, + tierOutcome, + })!; + } + + test("xAI rules declare exact 2x premiums with official provenance", () => { + const xaiRules = PRIORITY_PRICING_RULES.filter(rule => rule.provider === "xai"); + expect(xaiRules.map(rule => rule.modelId)).toEqual(["grok-4.5", "grok-4.6"]); + expect(xaiRules.every(rule => rule.multiplier === 2)).toBe(true); + expect(xaiRules.every(rule => rule.requiresResponseConfirmation === true)).toBe(true); + expect(xaiRules.every(rule => rule.source === "https://docs.x.ai/developers/advanced-api-usage/priority-processing")).toBe(true); + expect(findPriorityPricingRule("xai", "grok-4.6")?.multiplier).toBe(2); + expect(findPriorityPricingRule("openrouter", "grok-4.6")).toBeUndefined(); + expect(resolveMatchedPrice("openrouter", "grok-4.6")?.cost4).toEqual({ + input: 2, + output: 6, + cacheRead: 0.3, + cacheWrite: 0, + }); + expect(resolveMatchedPrice("cursor", "grok-4.6")?.cost4).toEqual({ + input: 2, + output: 6, + cacheRead: 0.3, + cacheWrite: 0, + }); + }); + + test("grok-4.6 standard and confirmed priority prices include the official cache rate", () => { + expect(resolveMatchedPrice("xai", "grok-4.6")?.cost4).toEqual({ + input: 2, + output: 6, + cacheRead: 0.5, + cacheWrite: 0, + }); + const confirmedOutcome = outcome("priority"); + const confirmed = estimate(confirmedOutcome); + expect(confirmedOutcome).toMatchObject({ + canonical: "priority", + fastOutcome: "applied", + confirmation: "confirmed", + }); + expect(confirmed.cost.total).toBeCloseTo(0.46, 9); + expect(confirmed.cost.cacheRead).toBeCloseTo(0.02, 9); + expect(confirmed.priorityMultiplier).toBe(2); + }); + + test("an assumed priority outcome stays at the standard price", () => { + const assumedOutcome = outcome(); + const assumed = estimate(assumedOutcome); + expect(assumedOutcome).toMatchObject({ + canonical: "priority", + fastOutcome: "applied", + confirmation: "assumed", + }); + expect(assumed.cost.total).toBeCloseTo(0.23, 9); + expect(assumed.priorityMultiplier).toBeUndefined(); + }); + + test("missing provenance and a requested tier do not prove the xAI premium", () => { + for (const serviceTier of [ + "priority", + { requestedServiceTier: "priority" }, + { configuredServiceTier: "priority" }, + ] as const) { + const unconfirmed = estimateRequestCost({ + provider: "xai", + model: "grok-4.6", + usageStatus: "reported", + usage, + serviceTier, + })!; + expect(unconfirmed.cost.total).toBeCloseTo(0.23, 9); + expect(unconfirmed.priorityMultiplier).toBeUndefined(); + } + }); + + test("an echoed default records a downgrade and bills the standard price", () => { + const downgradedOutcome = outcome("default"); + const downgraded = estimate(downgradedOutcome); + expect(downgradedOutcome).toMatchObject({ + fastOutcome: "downgraded", + fastDowngradeReason: "response-declined", + confirmation: "downgraded", + responseServiceTier: "default", + }); + expect(downgradedOutcome).not.toHaveProperty("canonical"); + expect(downgraded.cost.total).toBeCloseTo(0.23, 9); + expect(downgraded.priorityMultiplier).toBeUndefined(); + }); + + test("confirmed priority at 200k uses the long-context price as a marked lower bound", () => { + const long = estimate(outcome("priority"), { + inputTokens: 200_000, + outputTokens: 10_000, + cacheReadInputTokens: 50_000, + }); + expect(long.contextTier).toBe("long"); + expect(long.priorityMultiplier).toBeUndefined(); + expect(long.priorityLowerBound).toBe(true); + expect(long.cost).toMatchObject({ + input: 0.6, + cacheRead: 0.05, + output: 0.12, + }); + expect(long.cost.total).toBeCloseTo(0.77, 9); + }); + + test("a combo is a lower bound only when every priced attempt is a lower bound", () => { + const confirmed = outcome("priority"); + const lowerBoundAttempt = { + ordinal: 1, + provider: "xai", + model: "grok-4.6", + usageStatus: "reported" as const, + usage: { inputTokens: 200_000, outputTokens: 10_000 }, + tierOutcome: confirmed, + }; + const ordinaryAttempt = { + ordinal: 2, + provider: "xai", + model: "grok-4.6", + usageStatus: "reported" as const, + usage, + }; + + expect(estimateComboCost([lowerBoundAttempt, { ...lowerBoundAttempt, ordinal: 2 }])?.priorityLowerBound).toBe(true); + expect(estimateComboCost([lowerBoundAttempt, ordinaryAttempt])?.priorityLowerBound).toBeUndefined(); + }); +}); + describe("long-context pricing tiers (#908)", () => { const SOL: ExpectedPriceOverlay[] = [ { provider: "openai", modelId: "gpt-5.6-sol", cost4: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, source: "test", verifiedAt: "2026-08-03", status: "verified" },