@bitkyc08/opencodex 2.42.0 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -270,6 +270,12 @@ export interface OcxProviderConfig {
270
270
  * compaction blobs. Absent or false degrades foreign blobs to an opaque note.
271
271
  */
272
272
  decodesNativeCompactionBlobs?: boolean;
273
+ /**
274
+ * Trust this direct key-auth Responses provider to consume or relay opaque encrypted
275
+ * V2 agent tasks. OpenCodex does not decrypt, translate, or recover an eligible task.
276
+ * Absent or false keeps the existing recovery/fail-closed behavior.
277
+ */
278
+ allowEncryptedV2AgentTasks?: boolean;
273
279
  /**
274
280
  * Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918,
275
281
  * link-local, or unique-local upstreams. Metadata endpoints remain blocked.
@@ -339,6 +345,13 @@ export interface OcxProviderConfig {
339
345
  * full set so the user can pick). See devlog issue_052_provider-model-allowlist.
340
346
  */
341
347
  selectedModels?: string[];
348
+ /** Registration-owned state. Absent means legacy or OAuth-exempt, not uninitialized. */
349
+ initialModelSelection?: {
350
+ version: 1;
351
+ registrationId: string;
352
+ status: "pending" | "ready" | "all-off";
353
+ modelCount?: number;
354
+ };
342
355
  /**
343
356
  * Per-provider retention allowlist for authoritative live discovery. When non-empty, any
344
357
  * model id in this list is preserved in the routed catalog even if the live `/models`
@@ -424,11 +437,13 @@ export interface OcxProviderConfig {
424
437
  */
425
438
  authMode?: "key" | "forward" | "oauth" | "local";
426
439
  /**
427
- * Per-provider override for generic OAuth multi-account 429 failover (#2568).
440
+ * Per-provider override for the generic OAuth PROACTIVE account preference (#2568, #695).
428
441
  *
429
- * Rotation is presence-driven by default — 2+ logged-in accounts activate it — so this exists
430
- * for the operator who accepts rotation on one provider and refuses it on another. An explicit
431
- * boolean here beats the global `oauthAccountFailover` and beats presence.
442
+ * Reactive 429 rotation is presence-driven and cannot be refused here — 2+ logged-in accounts
443
+ * activate it, and a 429 with an idle second account is a defect rather than a preference.
444
+ * What an explicit `false` still refuses is the pre-dispatch preference that steers a HEALTHY
445
+ * request toward the account with more known headroom. It beats the global
446
+ * `oauthAccountFailover` in either direction; reactive 429 rotation remains presence-driven.
432
447
  */
433
448
  oauthAccountFailover?: {
434
449
  enabled?: boolean;
@@ -506,6 +521,8 @@ export interface OcxProviderConfig {
506
521
  * from the web-search sidecar's `search.xSearch` options and never widens caller tool selectors.
507
522
  */
508
523
  xaiResponsesXSearch?: boolean;
524
+ /** One-time Grok subscription wire upgrade; later explicit Chat choices remain authoritative. */
525
+ xaiResponsesDefaultVersion?: number;
509
526
  /**
510
527
  * Whether the Responses upstream accepts native custom tools and custom_tool_call items.
511
528
  * Set false only for a provider whose native contract rejects them; absence preserves
@@ -518,9 +535,21 @@ export interface OcxProviderConfig {
518
535
  * SSE/JSON; raw inspection state remains authoritative.
519
536
  */
520
537
  responsesSnapshotRepair?: boolean;
521
- /** Provider-wide mapping from Codex effort labels to upstream `reasoning_effort` values. */
538
+ /**
539
+ * Provider-wide mapping from Codex effort labels to upstream `reasoning_effort` values.
540
+ * Map a label to the reserved value `"__omit__"` to send no reasoning field at all for that
541
+ * effort, so the upstream model's own default applies. The sentinel is
542
+ * `REASONING_EFFORT_OMIT_SENTINEL` in `src/reasoning-effort.ts`; it suppresses
543
+ * `reasoning_effort` on an OpenAI-compatible wire and Ollama's native `think` field on the
544
+ * Ollama native adapter (#2356).
545
+ */
522
546
  reasoningEffortMap?: Record<string, string>;
523
- /** Model-specific mapping from Codex effort labels to upstream `reasoning_effort` values. */
547
+ /**
548
+ * Model-specific mapping from Codex effort labels to upstream `reasoning_effort` values.
549
+ * Map a label to the reserved value `"__omit__"` to send no reasoning field at all for that
550
+ * effort, so the upstream model's own default applies. Same sentinel as
551
+ * `reasoningEffortMap`, resolved per model first.
552
+ */
524
553
  modelReasoningEffortMap?: Record<string, Record<string, string>>;
525
554
  /** OpenAI-compatible gateway reasoning wire shape. Default sends `reasoning_effort`. */
526
555
  reasoningWireFormat?: "gateway-object";
@@ -696,7 +725,7 @@ export interface OcxProviderConfig {
696
725
  * headless and cannot control a screen itself; provide commands here only when running on a host
697
726
  * that can. With no executor, these tools honestly report "not supported".
698
727
  */
699
- desktopExecutor?: import("../adapters/cursor/native-exec-desktop").DesktopExecutorConfig;
728
+ desktopExecutor?: import("../adapters/cursor/desktop-executor-contract").DesktopExecutorConfig;
700
729
  /**
701
730
  * Cursor adapter only: unsafe opt-in escape hatch for Cursor server-driven built-in local
702
731
  * read/write/delete/ls/grep/shell/fetch execution. Prefer `nativeLocalExec: "on"` for new
@@ -31,6 +31,17 @@ export function namespacedToolName(namespace: string | undefined, name: string):
31
31
  return namespace ? `${namespace}__${name}` : name;
32
32
  }
33
33
 
34
+ /**
35
+ * Dotted alias of a namespaced tool's wire name. Some routed providers (observed: muse-spark
36
+ * via opencode-go) echo a namespaced tool call as "<namespace>.<name>" instead of the flattened
37
+ * "<namespace>__<name>" form. It names the same tool identity+�u���T never a new grant"��y��y� so the
38
+ * undeclared-tool guard and the tool bridge maps accept it wherever the wire name is accepted
39
+ * (mirroring the second entry of `toolChoiceAliases`). See #3402.
40
+ */
41
+ export function dottedToolName(namespace: string | undefined, name: string): string {
42
+ return namespace ? `${namespace}.${name}` : name;
43
+ }
44
+
34
45
  /**
35
46
  * Codex unified-exec name normalization.
36
47
  *
@@ -73,9 +84,23 @@ export function normalizeDeclaredToolName(
73
84
  : name;
74
85
  }
75
86
 
87
+ /**
88
+ * True when a declared catalog is the genuine Codex code-mode shape.
89
+ *
90
+ * `exec` is a name, not a guarantee. A catalog that lists `exec` NEXT TO a bare
91
+ * `exec_command` or `shell_command` is the flat-bridge shape: there `exec` may be an
92
+ * ordinary caller-defined tool, and nested `tools.*` helpers are not what it runs.
93
+ * `normalizeDeclaredToolName` already refuses to reinterpret helper names in that shape,
94
+ * and anything inferring code mode from the bare name owes the same check.
95
+ */
96
+ export function declaresCodeModeExec(declared: ReadonlySet<string> | undefined): boolean {
97
+ if (!declared || !declared.has(CODE_MODE_EXEC_TOOL_NAME)) return false;
98
+ return !(LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).some(legacy => declared.has(legacy));
99
+ }
100
+
76
101
  export function toolChoiceAliases(tool: Pick<OcxTool, "namespace" | "name">): string[] {
77
102
  const wireName = namespacedToolName(tool.namespace, tool.name);
78
- return tool.namespace ? [wireName, `${tool.namespace}.${tool.name}`] : [wireName];
103
+ return tool.namespace ? [wireName, dottedToolName(tool.namespace, tool.name)] : [wireName];
79
104
  }
80
105
 
81
106
  function sameToolIdentity(
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  export type { OcxTool, OcxToolChoice } from "./types/tools";
5
5
  export {
6
6
  CODE_MODE_EXEC_TOOL_NAME,
7
+ dottedToolName,
7
8
  namespacedToolName,
8
9
  normalizeDeclaredToolName,
9
10
  toolChoiceAliases,
@@ -14,6 +15,7 @@ export {
14
15
  modelInList,
15
16
  isAllowedToolChoice,
16
17
  toolChoiceToolPredicate,
18
+ declaresCodeModeExec,
17
19
  } from "./types/tools";
18
20
 
19
21
  export type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire";
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { isatty } from "node:tty";
4
5
  import { createInterface } from "node:readline/promises";
5
6
  import { atomicWriteFile, getConfigDir } from "../config";
6
7
  import { hasStarPromptRun } from "../cli/star-prompt";
@@ -122,8 +123,13 @@ export function isSourceBuildVersion(v: string): boolean {
122
123
  }
123
124
 
124
125
  /** The interactive/TTY + install-method gate shared with the star prompt. */
125
- function interactiveGuardOk(): boolean {
126
- return !(process.env.OCX_SERVICE || !process.stdin.isTTY || !process.stdout.isTTY);
126
+ export function interactiveGuardOk(): boolean {
127
+ try {
128
+ return !(process.env.OCX_SERVICE || !isatty(0) || !isatty(1));
129
+ } catch {
130
+ /* best-effort */
131
+ return false;
132
+ }
127
133
  }
128
134
 
129
135
  /**
package/src/usage/cost.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  resolveMetadataProvider,
16
16
  } from "../generated/model-metadata";
17
17
  import type { AttemptTierOutcome, OcxUsage } from "../types";
18
+ import { canonicalFastTierMarker } from "../providers/fastwire";
18
19
  import { baseProviderLabel, canonicalUsageProviderLabel } from "../providers/label";
19
20
  import type { PersistedUsageAttempt, UsageStatus } from "./log";
20
21
  import { canonicalAntigravityUsageModel } from "../providers/antigravity-models";
@@ -34,12 +35,18 @@ import {
34
35
  /** Published long-context pricing band (#908). */
35
36
  export type ContextTierName = "long";
36
37
 
38
+ export interface PriceResolutionOptions {
39
+ /** False for unresolved slash selectors; exact provider/user rates still apply. */
40
+ allowModelLevelFallback?: boolean;
41
+ }
42
+
43
+ type AttemptCostInput = Pick<PersistedUsageAttempt, "ordinal" | "provider" | "model" | "usage" | "usageStatus" | "tierOutcome"> & PriceResolutionOptions;
44
+
37
45
  /**
38
46
  * Service-tier provenance. `effectiveServiceTier()` collapses these with `??`,
39
- * but long-context exclusivity needs to know WHICH source supplied "priority":
40
- * OpenAI does not serve long context in Fast mode, so a >272k request that was
41
- * merely TAGGED priority was necessarily downgraded and must still be priced at
42
- * the long rate. Only a response-confirmed Fast tier suppresses the context tier.
47
+ * but provider-specific long-context rules need to know WHICH source supplied Fast.
48
+ * Current API rows stack Fast and long rates; legacy exclusive rows and unknown
49
+ * vendor combinations still require response-confirmed provenance.
43
50
  */
44
51
  export interface ServiceTierContext {
45
52
  responseServiceTier?: string;
@@ -178,6 +185,7 @@ export function resolveMatchedPrice(
178
185
  modelId: string,
179
186
  overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
180
187
  userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
188
+ options: PriceResolutionOptions = {},
181
189
  ): MatchedPrice | null {
182
190
  // User-configured overlays are keyed by the EXACT configured provider name.
183
191
  // A provider that literally exists in config.providers keeps its own pricing
@@ -201,14 +209,14 @@ export function resolveMatchedPrice(
201
209
  // user overlays get a NEW array identity + version bump on every config refresh,
202
210
  // so memoized rows never go stale.
203
211
  if (overlays === EXPECTED_PRICE_OVERLAYS && userOverlays === activeUserCostOverlays()) {
204
- const cacheKey = `${userCostOverlayVersion()} ${provider} ${modelId}`;
212
+ const cacheKey = `${userCostOverlayVersion()} ${options.allowModelLevelFallback !== false} ${provider} ${modelId}`;
205
213
  if (!priceMemo.has(cacheKey)) {
206
214
  if (priceMemo.size >= 512) priceMemo.clear();
207
- priceMemo.set(cacheKey, resolveMatchedPriceInner(provider, modelId, overlays, userOverlays));
215
+ priceMemo.set(cacheKey, resolveMatchedPriceInner(provider, modelId, overlays, userOverlays, options));
208
216
  }
209
217
  return priceMemo.get(cacheKey)!;
210
218
  }
211
- return resolveMatchedPriceInner(provider, modelId, overlays, userOverlays);
219
+ return resolveMatchedPriceInner(provider, modelId, overlays, userOverlays, options);
212
220
  }
213
221
 
214
222
  const priceMemo = new Map<string, MatchedPrice | null>();
@@ -223,14 +231,15 @@ function resolveMatchedPriceInner(
223
231
  modelId: string,
224
232
  overlays: readonly ExpectedPriceOverlay[],
225
233
  userOverlays: readonly ExpectedPriceOverlay[],
234
+ options: PriceResolutionOptions,
226
235
  ): MatchedPrice | null {
227
- const direct = resolveMatchedPriceExact(provider, modelId, overlays, userOverlays);
236
+ const direct = resolveMatchedPriceExact(provider, modelId, overlays, userOverlays, options);
228
237
  if (direct) return direct;
229
238
  // Antigravity historical/wire ids often lack an exact overlay; fall back to the
230
239
  // picker/call base model so collapsed usage rows still get a price.
231
240
  if (provider === "google-antigravity" || provider.startsWith("google-antigravity")) {
232
241
  const base = canonicalAntigravityUsageModel(modelId);
233
- if (base !== modelId) return resolveMatchedPriceExact(provider, base, overlays, userOverlays);
242
+ if (base !== modelId) return resolveMatchedPriceExact(provider, base, overlays, userOverlays, options);
234
243
  }
235
244
  return null;
236
245
  }
@@ -245,6 +254,7 @@ function resolveMatchedPriceExact(
245
254
  modelId: string,
246
255
  overlays: readonly ExpectedPriceOverlay[],
247
256
  userOverlays: readonly ExpectedPriceOverlay[],
257
+ options: PriceResolutionOptions,
248
258
  ): MatchedPrice | null {
249
259
  // User-configured provider overlay wins over every compiled catalog: the
250
260
  // operator's explicit price is authoritative for the ~$ estimate.
@@ -253,7 +263,7 @@ function resolveMatchedPriceExact(
253
263
  const verifiedOverride = overlays === EXPECTED_PRICE_OVERLAYS
254
264
  ? findVerifiedPriceOverride(provider, modelId)
255
265
  : undefined;
256
- if (verifiedOverride && validCost4(verifiedOverride.cost4) && hasNonZeroCost(verifiedOverride.cost4)) {
266
+ if (verifiedOverride && verifiedOverride.status !== "unverified" && validCost4(verifiedOverride.cost4) && hasNonZeroCost(verifiedOverride.cost4)) {
257
267
  return {
258
268
  provider,
259
269
  modelId,
@@ -261,7 +271,7 @@ function resolveMatchedPriceExact(
261
271
  source: "expected",
262
272
  sourceRef: verifiedOverride.source,
263
273
  verifiedAt: verifiedOverride.verifiedAt,
264
- status: "verified",
274
+ status: verifiedOverride.status,
265
275
  };
266
276
  }
267
277
  const metadataProvider = resolveMetadataProvider(provider);
@@ -280,7 +290,7 @@ function resolveMatchedPriceExact(
280
290
  }
281
291
  const overlay = findExpectedPriceOverlay(provider, modelId, overlays);
282
292
  if (!overlay || !validCost4(overlay.cost4) || !hasNonZeroCost(overlay.cost4)) {
283
- return resolveModelLevelPrice(provider, modelId);
293
+ return options.allowModelLevelFallback === false ? null : resolveModelLevelPrice(provider, modelId);
284
294
  }
285
295
  if (overlay.status === "unverified") return null;
286
296
  return {
@@ -433,7 +443,7 @@ function tierScalar(tier?: ServiceTierInput): string | undefined {
433
443
 
434
444
  /** True only when the UPSTREAM RESPONSE confirmed the Fast tier (see ServiceTierContext). */
435
445
  function isConfirmedFast(tier?: ServiceTierInput): boolean {
436
- return typeof tier === "object" && tier.responseServiceTier === "priority";
446
+ return typeof tier === "object" && canonicalFastTierMarker(tier.responseServiceTier) === "priority";
437
447
  }
438
448
 
439
449
  /**
@@ -445,7 +455,7 @@ function isConfirmedFast(tier?: ServiceTierInput): boolean {
445
455
  * cache-heavy long prompt would fall below the boundary and under-bill.
446
456
  *
447
457
  * A provider's declaration decides how a response-confirmed priority tier relates to this band.
448
- * OpenAI declares the bands exclusive. xAI publishes neither a combined rate nor an exclusion,
458
+ * API rows can stack the bands. xAI publishes neither a combined rate nor an exclusion,
449
459
  * so its long-context rate remains the known lower bound instead of inventing a stacked multiplier.
450
460
  */
451
461
  function applyContextTier(
@@ -481,9 +491,11 @@ function applyPriorityMultiplier(
481
491
  provider: string,
482
492
  modelId: string,
483
493
  serviceTier?: ServiceTierInput,
494
+ contextTier?: ContextTierName,
484
495
  ): [Cost4, number] {
485
- if (tierScalar(serviceTier) !== "priority") return [cost4, 1];
496
+ if (canonicalFastTierMarker(tierScalar(serviceTier)) !== "priority") return [cost4, 1];
486
497
  const base = baseProviderLabel(provider);
498
+ if (contextTier && findContextTier(base, modelId)?.confirmedPriorityRelation !== "stack") return [cost4, 1];
487
499
  const rule = findPriorityPricingRule(base, modelId);
488
500
  if (rule?.requiresResponseConfirmation && !isConfirmedFast(serviceTier)) return [cost4, 1];
489
501
  const multiplier = rule?.multiplier ?? 1;
@@ -524,7 +536,7 @@ function isOpenRouterPriorityLowerBound(
524
536
  * missing so combos can fail closed.
525
537
  */
526
538
  export function estimateAttemptCost(
527
- attempt: Pick<PersistedUsageAttempt, "ordinal" | "provider" | "model" | "usage" | "usageStatus" | "tierOutcome">,
539
+ attempt: AttemptCostInput,
528
540
  overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
529
541
  serviceTier?: ServiceTierInput,
530
542
  userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
@@ -532,7 +544,7 @@ export function estimateAttemptCost(
532
544
  if (!attempt.usage) return null;
533
545
  const tokens = normalizeCostTokens(attempt.usage);
534
546
  if (!tokens) return null;
535
- const price = resolveMatchedPrice(attempt.provider, attempt.model, overlays, userOverlays);
547
+ const price = resolveMatchedPrice(attempt.provider, attempt.model, overlays, userOverlays, attempt);
536
548
  if (!price) return null;
537
549
  const attemptServiceTier = attempt.tierOutcome
538
550
  ? serviceTierContextFromOutcome(attempt.tierOutcome)
@@ -540,12 +552,9 @@ export function estimateAttemptCost(
540
552
  const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier(
541
553
  price.cost4, attempt.provider, attempt.model, attempt.usage.inputTokens, attemptServiceTier,
542
554
  );
543
- // A published long-context row owns the numeric estimate. OpenAI declares that band
544
- // exclusive with Fast; xAI's confirmed combination is deliberately left unmultiplied
545
- // and marked as a lower bound because no combined price has been published.
546
- const [effectiveCost4, multiplier] = contextTier
547
- ? [tieredCost4, 1] as const
548
- : applyPriorityMultiplier(tieredCost4, attempt.provider, attempt.model, attemptServiceTier);
555
+ const [effectiveCost4, multiplier] = applyPriorityMultiplier(
556
+ tieredCost4, attempt.provider, attempt.model, attemptServiceTier, contextTier,
557
+ );
549
558
  const priorityLowerBound = contextPriorityLowerBound
550
559
  || isOpenRouterPriorityLowerBound(attempt.provider, attempt.tierOutcome);
551
560
  return {
@@ -567,7 +576,7 @@ export function estimateAttemptCost(
567
576
  * attempt is unpriced or unnormalizable, return null rather than a partial sum.
568
577
  */
569
578
  export function estimateComboCost(
570
- attempts: readonly Pick<PersistedUsageAttempt, "ordinal" | "provider" | "model" | "usage" | "usageStatus" | "tierOutcome">[],
579
+ attempts: readonly AttemptCostInput[],
571
580
  overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
572
581
  serviceTier?: ServiceTierInput,
573
582
  userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
@@ -615,6 +624,7 @@ export function estimateRequestCost(
615
624
  usage?: OcxUsage;
616
625
  usageStatus: UsageStatus;
617
626
  serviceTier?: ServiceTierInput;
627
+ allowModelLevelFallback?: boolean;
618
628
  },
619
629
  overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
620
630
  userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
@@ -622,14 +632,14 @@ export function estimateRequestCost(
622
632
  if (!input.usage) return null;
623
633
  const tokens = normalizeCostTokens(input.usage);
624
634
  if (!tokens) return null;
625
- const price = resolveMatchedPrice(input.provider, input.model, overlays, userOverlays);
635
+ const price = resolveMatchedPrice(input.provider, input.model, overlays, userOverlays, input);
626
636
  if (!price) return null;
627
637
  const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier(
628
638
  price.cost4, input.provider, input.model, input.usage.inputTokens, input.serviceTier,
629
639
  );
630
- const [effectiveCost4, multiplier] = contextTier
631
- ? [tieredCost4, 1] as const
632
- : applyPriorityMultiplier(tieredCost4, input.provider, input.model, input.serviceTier);
640
+ const [effectiveCost4, multiplier] = applyPriorityMultiplier(
641
+ tieredCost4, input.provider, input.model, input.serviceTier, contextTier,
642
+ );
633
643
  const priorityLowerBound = contextPriorityLowerBound || isOpenRouterPriorityLowerBound(
634
644
  input.provider,
635
645
  typeof input.serviceTier === "object" ? input.serviceTier.tierOutcome : undefined,
@@ -42,6 +42,8 @@ export interface ExpectedPriceOverlay {
42
42
 
43
43
  const GEMINI_31_PRO: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 };
44
44
  const GPT56_SOL: Cost4 = { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 };
45
+ const GPT6_ASTRA: Cost4 = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
46
+ const ASTRA_API_PRICING = "https://developers.openai.com/api/docs/models/gpt-6-astra";
45
47
  /**
46
48
  * Daybreak aliases. `daybreak-*-latest` never appears in the pricing table itself — only its
47
49
  * current snapshot does — so these tuples are the snapshot's published rates and carry
@@ -112,9 +114,14 @@ const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cac
112
114
  const QWEN38_MAX_PRICING = "https://qwen.ai/blog?id=qwen3.8 (Qwen release announcement; no Model Studio billing row yet; cache rates unpublished -> 0)";
113
115
 
114
116
  export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
115
- // claude-fable-5-1 has no jawcode row yet, so both Anthropic surfaces need their own
116
- // overlay (the overlay lookup is keyed by the configured provider id; only the jawcode
117
- // bundle collapses anthropic-apikey onto anthropic).
117
+ { provider: "openai-apikey", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: ASTRA_API_PRICING, verifiedAt: "2026-09-05", status: "verified" },
118
+ // Display estimates use API prices for both login and API-key routes, including cache writes.
119
+ { provider: "openai", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: `API-reference comparison estimate: ${ASTRA_API_PRICING}`, verifiedAt: "2026-09-05", status: "verified-derived" },
120
+ // claude-fable-5-1 now HAS a generated jawcode row, so the two Anthropic surfaces resolve
121
+ // from it and these overlays are the fallback rather than the primary source. They stay:
122
+ // the overlay lookup is keyed by the configured provider id, so an account-pool log label
123
+ // like anthropic-pb51d9b still needs them, and only the jawcode bundle collapses
124
+ // anthropic-apikey onto anthropic.
118
125
  { provider: "anthropic", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" },
119
126
  { provider: "anthropic-apikey", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" },
120
127
  // Cursor canonicalizes every Fable 5.1 spelling onto this sole overlay row.
@@ -237,6 +244,11 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
237
244
  * therefore cannot reprice routed resellers that reuse the same model slug.
238
245
  */
239
246
  export const VERIFIED_PRICE_OVERRIDES: readonly ExpectedPriceOverlay[] = [
247
+ ...["openai", "openai-apikey"].map((provider): ExpectedPriceOverlay => ({
248
+ provider, modelId: "gpt-5.6-sol", cost4: GPT56_SOL,
249
+ source: provider === "openai" ? `API-reference comparison estimate: ${OPENAI_GPT56_PRICING}` : OPENAI_GPT56_PRICING,
250
+ verifiedAt: "2026-09-05", status: provider === "openai" ? "verified-derived" : "verified",
251
+ })),
240
252
  {
241
253
  provider: "xai",
242
254
  modelId: "grok-4.6",
@@ -278,6 +290,7 @@ export function findExpectedPriceOverlay(
278
290
 
279
291
  /** OpenAI Fast price multipliers retained as a compatibility export. */
280
292
  export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
293
+ "gpt-6-astra": 2,
281
294
  "gpt-5.6-sol": 2,
282
295
  "gpt-daybreak-blue-latest": 2,
283
296
  "daybreak-blue-latest": 2,
@@ -306,7 +319,6 @@ export interface PriorityPricingRule {
306
319
  verifiedAt: string;
307
320
  }
308
321
 
309
- const OPENAI_FAST_PRICING = "https://openai.com/api-fast-mode/";
310
322
  const XAI_PRIORITY_PRICING = "https://docs.x.ai/developers/advanced-api-usage/priority-processing";
311
323
 
312
324
  /**
@@ -325,10 +337,15 @@ export const PRIORITY_PRICING_RULES: readonly PriorityPricingRule[] = [
325
337
  provider,
326
338
  modelId,
327
339
  multiplier,
328
- source: OPENAI_FAST_PRICING,
329
- verifiedAt: "2026-08-05",
340
+ source: modelId === "gpt-6-astra" ? ASTRA_API_PRICING : "https://openai.com/api-fast-mode/",
341
+ verifiedAt: modelId === "gpt-6-astra" ? "2026-09-05" : "2026-08-05",
330
342
  })),
331
343
  ),
344
+ ...["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"].map((modelId): PriorityPricingRule => ({
345
+ provider: "openai-apikey", modelId, multiplier: 2,
346
+ source: "https://developers.openai.com/api/docs/pricing (derived from the virtual selection's base wire model)",
347
+ verifiedAt: "2026-09-05",
348
+ })),
332
349
  ...["grok-4.5", "grok-4.6"].map((modelId): PriorityPricingRule => ({
333
350
  provider: "xai",
334
351
  modelId,
@@ -374,7 +391,7 @@ export interface ContextTier {
374
391
  /** Per-field factor from the short rate to the published long rate. */
375
392
  multiplier: Cost4;
376
393
  /** Published relationship between confirmed priority and long-context bands. */
377
- confirmedPriorityRelation?: "exclusive" | "lower-bound";
394
+ confirmedPriorityRelation?: "exclusive" | "lower-bound" | "stack";
378
395
  source: string;
379
396
  verifiedAt: string;
380
397
  }
@@ -389,7 +406,8 @@ const OPENAI_LONG_CONTEXT: Cost4 = { input: 2, output: 1.5, cacheRead: 2, cacheW
389
406
  const UNIFORM_DOUBLE: Cost4 = { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 };
390
407
 
391
408
  const OPENAI_PRICING_DOC = "https://developers.openai.com/api/docs/pricing";
392
- const OPENAI_GPT56_CONTEXT_MODELS = [
409
+ const OPENAI_CONTEXT_MODELS = [
410
+ "gpt-6-astra",
393
411
  "gpt-5.6-sol",
394
412
  "gpt-5.6-terra",
395
413
  "gpt-5.6-luna",
@@ -401,16 +419,17 @@ const OPENAI_GPT56_CONTEXT_MODELS = [
401
419
  ];
402
420
 
403
421
  export const CONTEXT_TIERS: readonly ContextTier[] = [
422
+ // API-reference estimates do not apply subscription-only exemptions or multipliers.
404
423
  ...["openai", "openai-apikey"].flatMap(provider =>
405
- OPENAI_GPT56_CONTEXT_MODELS.map((modelId): ContextTier => ({
424
+ OPENAI_CONTEXT_MODELS.map((modelId): ContextTier => ({
406
425
  provider,
407
426
  modelId,
408
427
  thresholdInputTokens: 272_000,
409
428
  inclusive: false,
410
429
  multiplier: OPENAI_LONG_CONTEXT,
411
- confirmedPriorityRelation: "exclusive",
430
+ confirmedPriorityRelation: "stack",
412
431
  source: OPENAI_PRICING_DOC,
413
- verifiedAt: "2026-08-03",
432
+ verifiedAt: "2026-09-05",
414
433
  })),
415
434
  ),
416
435
  {
@@ -420,9 +439,9 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
420
439
  thresholdInputTokens: 272_000,
421
440
  inclusive: false,
422
441
  multiplier: OPENAI_LONG_CONTEXT,
423
- confirmedPriorityRelation: "exclusive",
442
+ confirmedPriorityRelation: "stack",
424
443
  source: OPENAI_PRICING_DOC,
425
- verifiedAt: "2026-08-11",
444
+ verifiedAt: "2026-09-05",
426
445
  },
427
446
  {
428
447
  // The bare selector is the separately billed API-key alias. Daybreak Red has no tier row:
@@ -432,9 +451,9 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
432
451
  thresholdInputTokens: 272_000,
433
452
  inclusive: false,
434
453
  multiplier: OPENAI_LONG_CONTEXT,
435
- confirmedPriorityRelation: "exclusive",
454
+ confirmedPriorityRelation: "stack",
436
455
  source: OPENAI_PRICING_DOC,
437
- verifiedAt: "2026-08-11",
456
+ verifiedAt: "2026-09-05",
438
457
  },
439
458
  {
440
459
  provider: "xai",
package/src/usage/log.ts CHANGED
@@ -46,6 +46,7 @@ export type AttemptRecoveryKind =
46
46
  | "transient-5xx"
47
47
  | "connection-reset"
48
48
  | "oauth-401"
49
+ | "key-401"
49
50
  | "key-429"
50
51
  | "rate-limit-429"
51
52
  | "anthropic-oauth-429"
@@ -257,6 +258,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
257
258
  "transient-5xx",
258
259
  "connection-reset",
259
260
  "oauth-401",
261
+ "key-401",
260
262
  "key-429",
261
263
  "rate-limit-429",
262
264
  "anthropic-oauth-429",
@@ -0,0 +1,26 @@
1
+ import { baseProviderLabel } from "../providers/label";
2
+ import type { PriceResolutionOptions } from "./cost";
3
+ import type { PersistedUsageEntry } from "./log";
4
+
5
+ type TraceSource = Pick<PersistedUsageEntry, "routeDecision">;
6
+ type ModelTarget = Pick<PersistedUsageEntry, "provider" | "model">;
7
+
8
+ /** Saved routing provenance, not a claim about the model a remote endpoint ran. */
9
+ export function isUnresolvedRequestedModel(source: TraceSource, target: ModelTarget): boolean {
10
+ const trace = source.routeDecision;
11
+ return trace !== undefined
12
+ && trace.routeKind === "default-provider"
13
+ && trace.selected.reason === "default-provider"
14
+ && trace.truncated?.strings !== true
15
+ && trace.selected.model.length > 0
16
+ && trace.requestedModel === trace.selected.model
17
+ && target.model === trace.selected.model
18
+ && baseProviderLabel(target.provider) === baseProviderLabel(trace.selected.provider);
19
+ }
20
+
21
+ /** Bare fallback rates remain eligible; unresolved slash IDs need a provider-specific rate. */
22
+ export function usageModelPriceOptions(source: TraceSource, target: ModelTarget): PriceResolutionOptions {
23
+ return {
24
+ allowModelLevelFallback: !target.model.includes("/") || !isUnresolvedRequestedModel(source, target),
25
+ };
26
+ }
@@ -1,6 +1,7 @@
1
1
  import { baseProviderLabel } from "../providers/label";
2
2
  import { canonicalAntigravityUsageModel } from "../providers/antigravity-models";
3
3
  import { usageDisplayTotalTokens } from "./totals";
4
+ import { isUnresolvedRequestedModel, usageModelPriceOptions } from "./model-identity";
4
5
  import { isCodexUsageAccountLogLabel, type PersistedUsageEntry, type UsageStatus } from "./log";
5
6
  import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext } from "./cost";
6
7
 
@@ -59,6 +60,8 @@ export interface UsageDay {
59
60
  export interface UsageDayModel {
60
61
  model: string;
61
62
  provider: string;
63
+ /** Includes trace-proven unresolved requested selectors; absence is not confirmation. */
64
+ hasUnresolvedRequestedModel?: true;
62
65
  requests: number;
63
66
  attemptCount: number;
64
67
  totalTokens: number;
@@ -73,6 +76,8 @@ export interface UsageDayModel {
73
76
  export interface UsageModel {
74
77
  provider: string;
75
78
  model: string;
79
+ /** Includes trace-proven unresolved requested selectors; absence is not confirmation. */
80
+ hasUnresolvedRequestedModel?: true;
76
81
  resolvedModel?: string;
77
82
  requests: number;
78
83
  attemptCount: number;
@@ -208,7 +213,7 @@ export function computeEntryCost(entry: PersistedUsageEntry): EntryCostInfo {
208
213
  const tier = serviceTierContext(entry);
209
214
  if (entry.attempts?.length) {
210
215
  const attemptEstimates = entry.attempts.map(attempt =>
211
- estimateAttemptCost(attempt, undefined, tier)
216
+ estimateAttemptCost({ ...attempt, ...usageModelPriceOptions(entry, attempt) }, undefined, tier)
212
217
  );
213
218
  let costTotal = 0;
214
219
  let isPriced = false;
@@ -221,6 +226,7 @@ export function computeEntryCost(entry: PersistedUsageEntry): EntryCostInfo {
221
226
  return { tier, estimate: null, attemptEstimates, costTotal, isPriced };
222
227
  }
223
228
  const estimate = estimateRequestCost({
229
+ ...usageModelPriceOptions(entry, entry),
224
230
  provider: entry.provider,
225
231
  model: entry.model,
226
232
  usage: entry.usage,
@@ -324,6 +330,7 @@ interface UsageAttribution {
324
330
  provider: string;
325
331
  model: string;
326
332
  resolvedModel?: string;
333
+ hasUnresolvedRequestedModel?: true;
327
334
  accountLogLabel?: string;
328
335
  usageStatus: UsageStatus;
329
336
  usage?: PersistedUsageEntry["usage"];
@@ -366,6 +373,7 @@ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
366
373
  requestId: entry.requestId,
367
374
  provider: entry.provider,
368
375
  ...usageModelIdentity(entry.provider, entry.model, entry.resolvedModel),
376
+ ...(isUnresolvedRequestedModel(entry, entry) ? { hasUnresolvedRequestedModel: true as const } : {}),
369
377
  ...(entry.accountLogLabel ? { accountLogLabel: entry.accountLogLabel } : {}),
370
378
  usageStatus: entry.usageStatus,
371
379
  ...(entry.usage ? { usage: entry.usage } : {}),
@@ -376,6 +384,7 @@ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
376
384
  requestId: entry.requestId,
377
385
  provider: attempt.provider,
378
386
  ...usageModelIdentity(attempt.provider, attempt.model),
387
+ ...(isUnresolvedRequestedModel(entry, attempt) ? { hasUnresolvedRequestedModel: true as const } : {}),
379
388
  ...(attempt.accountLogLabel ? { accountLogLabel: attempt.accountLogLabel } : {}),
380
389
  usageStatus: attempt.usageStatus,
381
390
  ...(attempt.usage ? { usage: attempt.usage } : {}),
@@ -517,6 +526,7 @@ interface UsageModelAccumulator {
517
526
  provider: string;
518
527
  model: string;
519
528
  resolvedModel?: string;
529
+ hasUnresolvedRequestedModel?: true;
520
530
  firstSeen: number;
521
531
  attemptCount: number;
522
532
  dayTotalTokens: number;
@@ -704,6 +714,7 @@ function cloneModelAccumulator(source: UsageModelAccumulator): UsageModelAccumul
704
714
  }
705
715
 
706
716
  function mergeModelAccumulator(target: UsageModelAccumulator, source: UsageModelAccumulator): void {
717
+ if (source.hasUnresolvedRequestedModel) target.hasUnresolvedRequestedModel = true;
707
718
  if (source.firstSeen < target.firstSeen) {
708
719
  target.firstSeen = source.firstSeen;
709
720
  target.resolvedModel = source.resolvedModel;
@@ -874,6 +885,7 @@ function buildDayModels(
874
885
  return retainedModelAccumulators(sorted, overlaps).map(model => ({
875
886
  model: model.model,
876
887
  provider: model.provider,
888
+ ...(model.hasUnresolvedRequestedModel ? { hasUnresolvedRequestedModel: true as const } : {}),
877
889
  requests: requestCountsFor(model).requests,
878
890
  attemptCount: model.attemptCount,
879
891
  totalTokens: model.dayTotalTokens,
@@ -900,6 +912,7 @@ function buildUsageModels(
900
912
  return {
901
913
  provider: model.provider,
902
914
  model: model.model,
915
+ ...(model.hasUnresolvedRequestedModel ? { hasUnresolvedRequestedModel: true as const } : {}),
903
916
  ...(model.resolvedModel ? { resolvedModel: model.resolvedModel } : {}),
904
917
  requests,
905
918
  attemptCount: model.attemptCount,
@@ -1115,6 +1128,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
1115
1128
  attribution: UsageAttribution,
1116
1129
  estimate: AttemptCostEstimate | CostEstimate | null,
1117
1130
  ): void {
1131
+ if (attribution.hasUnresolvedRequestedModel) breakdown.hasUnresolvedRequestedModel = true;
1118
1132
  breakdown.attemptCount += 1;
1119
1133
  if (attribution.usage) {
1120
1134
  breakdown.inputTokens += attribution.usage.inputTokens;