@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/usage/log.ts CHANGED
@@ -7,11 +7,36 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
7
7
  import { recordOwnedConfigPath } from "../lib/config-ownership";
8
8
  import { sanitizeLogMetadataString } from "../lib/redact";
9
9
  import { usageDisplayTotalTokens } from "./totals";
10
+ import { normalizeAttemptDeliverySummary } from "./attempt-delivery";
11
+ import {
12
+ isRequestCloseReason,
13
+ isRequestTerminalStatus,
14
+ type RequestCloseReason,
15
+ type RequestTerminalStatus,
16
+ } from "./request-outcome";
10
17
  import type { AttemptTierOutcome, OcxUsage } from "../types";
11
18
  import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
12
19
  import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
13
20
  import { claudeCompatibilityReason, normalizeClaudeFeatureCodes, type ClaudeFeatureCode } from "../claude/compatibility";
14
21
  import type { CodexWsStageRecord } from "../server/responses/codex-ws-wire";
22
+ import {
23
+ ATTEMPT_RECOVERY_KIND_ROSTER,
24
+ ATTEMPT_RECOVERY_WITHHELD_ROSTER,
25
+ REQUEST_FAILURE_CAUSES,
26
+ REQUEST_FAILURE_STAGES,
27
+ REQUEST_TRANSPORT_PHASES,
28
+ type AttemptRecoveryKind,
29
+ type AttemptRecoveryWithheld,
30
+ type AttemptDeliverySummary,
31
+ type RequestFailureCause,
32
+ type RequestFailureStage,
33
+ type RequestSpendTotals,
34
+ } from "./telemetry-contract";
35
+
36
+ // Re-exported so every existing importer keeps its path. The declarations moved to a leaf the
37
+ // dashboard can import without pulling node:fs and the config barrel into the browser build.
38
+ export { ATTEMPT_RECOVERY_KIND_ROSTER, ATTEMPT_RECOVERY_WITHHELD_ROSTER, REQUEST_FAILURE_CAUSES, REQUEST_FAILURE_STAGES };
39
+ export type { AttemptRecoveryKind, AttemptRecoveryWithheld, RequestFailureCause, RequestFailureStage, RequestSpendTotals };
15
40
 
16
41
  export interface PersistedClaudeCompatibilityLog {
17
42
  decision: "shadow";
@@ -58,45 +83,6 @@ export function isCodexPoolAccountLogLabel(value: unknown): value is "main" | `p
58
83
  return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
59
84
  }
60
85
 
61
- /**
62
- * Recovery kinds recorded per attempt in the usage log; the GUI renders localized labels
63
- * for these wire values.
64
- */
65
- export type AttemptRecoveryKind =
66
- | "transient-5xx"
67
- | "connection-reset"
68
- | "oauth-401"
69
- | "key-401"
70
- | "key-429"
71
- | "rate-limit-429"
72
- | "anthropic-oauth-429"
73
- | "oauth-account-429"
74
- | "image-413"
75
- | "console-go-upload-retry"
76
- | "opaque-blob-rejection"
77
- | "empty-completion"
78
- | "reasoning-effort-downgrade";
79
-
80
- /**
81
- * Why a recovery this request was otherwise willing to make did not happen.
82
- *
83
- * Recorded separately from `recoveryKinds` and from `sendCount`, because the question it
84
- * answers is different from either. A log showing one physical send and no recovery kind used
85
- * to be ambiguous: it could mean nothing was eligible, or that something was eligible and the
86
- * send budget withheld it. Those need opposite follow-ups, and the second one was invisible
87
- * (#5044).
88
- *
89
- * `sendCount` deliberately does not move for these. A refused attempt is not a physical send,
90
- * and inflating the count to signal the refusal would corrupt the one number that means
91
- * "requests this proxy actually made".
92
- *
93
- * Bounded vocabulary on purpose: it is a wire value a maintainer reads, never a credential, an
94
- * account id, an upstream body, prompt content, or exception text.
95
- */
96
- export type AttemptRecoveryWithheld =
97
- | "retry-send-budget"
98
- | "rotation-send-budget";
99
-
100
86
  /** Request-time upstream credential class, never a credential or account identifier. */
101
87
  export type UsageCredentialSource = "grok-oauth" | "xai-api-key";
102
88
 
@@ -200,6 +186,21 @@ export interface PersistedUsageAttempt {
200
186
  * account identifiers.
201
187
  */
202
188
  codexWsStage?: CodexWsStageRecord;
189
+ /**
190
+ * What this attempt delivered, as five bounded counts (#3983). Absent on attempts whose
191
+ * transport does not pass through the Responses bridge and on pre-instrumentation rows.
192
+ */
193
+ deliverySummary?: AttemptDeliverySummary;
194
+ /**
195
+ * How far this attempt's exchange got and why it failed, in the shared vocabulary (#2366).
196
+ *
197
+ * Both values are closed roster members, so the pair can be a metric label and a grouping key
198
+ * without a masking pass. Absent on a completed attempt and on every row written before the
199
+ * attribution existed. The resend verdict these two imply is NOT stored: it is derived at read
200
+ * time, so a stored row can never carry a verdict the current table would no longer reach.
201
+ */
202
+ failureStage?: RequestFailureStage;
203
+ failureCause?: RequestFailureCause;
203
204
  }
204
205
 
205
206
  /**
@@ -210,19 +211,7 @@ export interface PersistedUsageAttempt {
210
211
  * operator needs is the total that reached upstream carrying the full prompt. These fields are
211
212
  * that total, decomposed by how much of it is explained.
212
213
  */
213
- export interface PersistedRequestSpend {
214
- /** Physical upstream sends summed across every attempt of this logical request, combo children included. */
215
- sends: number;
216
- /** Sends whose attempt reached a terminal status, so the spend has a known outcome. */
217
- settled: number;
218
- /**
219
- * Sends charged with no terminal outcome behind them: an attempt abandoned mid-flight, or a
220
- * budget charge no attempt row ever accounted for. Never folded into `settled` — an unexplained
221
- * send is the exact quantity this record exists to make visible.
222
- */
223
- unresolved: number;
224
- /** Model sends the request execution budget charged. Absent when no budget was attached. */
225
- reserved?: number;
214
+ export interface PersistedRequestSpend extends RequestSpendTotals {
226
215
  /** Budget profile that produced `reserved`, so a count can be read against the policy it obeyed. */
227
216
  policyVersion?: string;
228
217
  /**
@@ -329,8 +318,13 @@ export interface PersistedUsageEntry {
329
318
  // Failure diagnostics (devlog/_plan/260716_claudecode_hardening/030): persisted for
330
319
  // status>=400 or non-completed terminals so incidents survive the in-memory ring buffer.
331
320
  errorCode?: string;
332
- terminalStatus?: string;
333
- closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
321
+ /**
322
+ * Closed, like `closeReason` beside it has always been. It was `string` while it was only
323
+ * rendered; it is a grouping-key slot now, and the value is assembled from an upstream
324
+ * terminal frame, so an open type here is the one way upstream text could reach that key.
325
+ */
326
+ terminalStatus?: RequestTerminalStatus;
327
+ closeReason?: RequestCloseReason;
334
328
  /** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
335
329
  upstreamError?: string;
336
330
  /** Where the terminal/failure was observed; absent on historic rows. */
@@ -357,6 +351,53 @@ export interface PersistedUsageEntry {
357
351
  routeDecision?: RouteDecisionTraceV1;
358
352
  /** Closed Claude protocol codes only; absent on older rows. */
359
353
  claudeCompatibility?: PersistedClaudeCompatibilityLog;
354
+ /**
355
+ * How far this request got and why it failed (#2366). Projected from the attempt that ended
356
+ * the request so every surface reads the answer off the same row. Absent on a completed
357
+ * request and on rows written before the attribution existed.
358
+ */
359
+ failureStage?: RequestFailureStage;
360
+ failureCause?: RequestFailureCause;
361
+ }
362
+
363
+ /**
364
+ * Attribution for the logical request, projected from the attempt that ended it (#2366).
365
+ *
366
+ * Carried on the entry as well as the attempt because the three surfaces that have to agree read
367
+ * the entry: a projection that had to reach into `attempts` to answer "why did this fail" would
368
+ * be reading a different row from the exporter, which is the disagreement the landed terminal
369
+ * classifier already removed once.
370
+ */
371
+ export interface PersistedRequestFailureAttribution {
372
+ failureStage?: RequestFailureStage;
373
+ failureCause?: RequestFailureCause;
374
+ }
375
+
376
+ const KNOWN_REQUEST_FAILURE_STAGES: ReadonlySet<string> = new Set(REQUEST_FAILURE_STAGES);
377
+ const KNOWN_REQUEST_FAILURE_CAUSES: ReadonlySet<string> = new Set(REQUEST_FAILURE_CAUSES);
378
+
379
+ /**
380
+ * Same closed-set discipline as `isKnownTransportPhase`, with the set DERIVED from the roster
381
+ * rather than restated. The recovery vocabulary was written twice once -- as a union and as the
382
+ * read-back whitelist -- and a member present in only one of them is written to disk and dropped
383
+ * on the next read, which loses exactly the field that says why the row failed.
384
+ */
385
+ export function isKnownRequestFailureStage(value: unknown): value is RequestFailureStage {
386
+ return typeof value === "string" && KNOWN_REQUEST_FAILURE_STAGES.has(value);
387
+ }
388
+
389
+ export function isKnownRequestFailureCause(value: unknown): value is RequestFailureCause {
390
+ return typeof value === "string" && KNOWN_REQUEST_FAILURE_CAUSES.has(value);
391
+ }
392
+
393
+ /** The stage/cause pair a normalizer keeps, dropping either half that is not a roster member. */
394
+ export function normalizeRequestFailureAttribution(
395
+ raw: { failureStage?: unknown; failureCause?: unknown },
396
+ ): PersistedRequestFailureAttribution {
397
+ return {
398
+ ...(isKnownRequestFailureStage(raw.failureStage) ? { failureStage: raw.failureStage } : {}),
399
+ ...(isKnownRequestFailureCause(raw.failureCause) ? { failureCause: raw.failureCause } : {}),
400
+ };
360
401
  }
361
402
 
362
403
  const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
@@ -394,12 +435,10 @@ export function isKnownInboundProtocol(value: unknown): value is NonNullable<Per
394
435
  return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
395
436
  }
396
437
 
397
- const KNOWN_TRANSPORT_PHASES = new Set<NonNullable<PersistedUsageEntry["transportPhase"]>>([
398
- "pre_headers", "mid_stream", "terminal_sse",
399
- ]);
438
+ const KNOWN_TRANSPORT_PHASES: ReadonlySet<string> = new Set(REQUEST_TRANSPORT_PHASES);
400
439
 
401
440
  export function isKnownTransportPhase(value: unknown): value is NonNullable<PersistedUsageEntry["transportPhase"]> {
402
- return typeof value === "string" && KNOWN_TRANSPORT_PHASES.has(value as NonNullable<PersistedUsageEntry["transportPhase"]>);
441
+ return typeof value === "string" && KNOWN_TRANSPORT_PHASES.has(value);
403
442
  }
404
443
 
405
444
  const KNOWN_TERMINAL_SOURCES = new Set<NonNullable<PersistedUsageEntry["terminalSource"]>>([
@@ -497,25 +536,8 @@ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined
497
536
  };
498
537
  }
499
538
 
500
- const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
501
- "transient-5xx",
502
- "connection-reset",
503
- "oauth-401",
504
- "key-401",
505
- "key-429",
506
- "rate-limit-429",
507
- "anthropic-oauth-429",
508
- "oauth-account-429",
509
- "image-413",
510
- "console-go-upload-retry",
511
- "opaque-blob-rejection",
512
- "empty-completion",
513
- "reasoning-effort-downgrade",
514
- ]);
515
- const ATTEMPT_RECOVERY_WITHHELD = new Set<AttemptRecoveryWithheld>([
516
- "retry-send-budget",
517
- "rotation-send-budget",
518
- ]);
539
+ const ATTEMPT_RECOVERY_KINDS: ReadonlySet<AttemptRecoveryKind> = new Set(ATTEMPT_RECOVERY_KIND_ROSTER);
540
+ const ATTEMPT_RECOVERY_WITHHELD: ReadonlySet<AttemptRecoveryWithheld> = new Set(ATTEMPT_RECOVERY_WITHHELD_ROSTER);
519
541
  const USAGE_STATUSES = new Set<UsageStatus>([
520
542
  "reported",
521
543
  "unreported",
@@ -643,6 +665,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
643
665
  const codexWsStage = "codexWsStage" in attempt
644
666
  ? normalizeCodexWsStageRecord(attempt.codexWsStage)
645
667
  : undefined;
668
+ const deliverySummary = "deliverySummary" in attempt
669
+ ? normalizeAttemptDeliverySummary(attempt.deliverySummary)
670
+ : undefined;
646
671
  const recoveryKinds = Array.isArray(attempt.recoveryKinds)
647
672
  ? [...new Set(attempt.recoveryKinds.filter(
648
673
  (value): value is AttemptRecoveryKind => typeof value === "string"
@@ -711,6 +736,8 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
711
736
  : {}),
712
737
  ...(tierOutcome ? { tierOutcome } : {}),
713
738
  ...(codexWsStage ? { codexWsStage } : {}),
739
+ ...(deliverySummary ? { deliverySummary } : {}),
740
+ ...normalizeRequestFailureAttribution(attempt),
714
741
  };
715
742
  }
716
743
 
@@ -882,11 +909,15 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
882
909
  ...(affinityReason ? { affinityReason } : {}),
883
910
  ...(conversationStateScrub ? { conversationStateScrub } : {}),
884
911
  ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
885
- ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
886
- ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
912
+ // Validated rather than copied on truthiness, like the inbound protocol and transport phase
913
+ // above. Harmless while these were only rendered; not harmless once the terminal status is
914
+ // a grouping-key slot, because the string is assembled from an upstream frame.
915
+ ...(isRequestTerminalStatus(entry.terminalStatus) ? { terminalStatus: entry.terminalStatus } : {}),
916
+ ...(isRequestCloseReason(entry.closeReason) ? { closeReason: entry.closeReason } : {}),
887
917
  ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
888
918
  ...(routeDecision ? { routeDecision } : {}),
889
919
  ...(claudeCompatibility ? { claudeCompatibility } : {}),
920
+ ...normalizeRequestFailureAttribution(entry),
890
921
  };
891
922
  }
892
923
 
@@ -921,6 +952,20 @@ function ensureUsageLogDir(now: number): void {
921
952
  ensuredUsageLogDir = { path: dir, checkedAt: now };
922
953
  }
923
954
 
955
+ /**
956
+ * One owner hook, run after an append lands.
957
+ *
958
+ * A slot rather than a direct call, because the only consumer -- ledger retention -- reads this
959
+ * module's revision helpers, and importing it back here would be a static cycle. The hook runs
960
+ * INSIDE the synchronous append call stack on purpose: that is what makes "no in-process append
961
+ * can interleave with a compaction" true rather than merely likely.
962
+ */
963
+ let afterUsageLedgerAppend: (() => void) | null = null;
964
+
965
+ export function setUsageLedgerAppendHook(hook: (() => void) | null): void {
966
+ afterUsageLedgerAppend = hook;
967
+ }
968
+
924
969
  export function appendUsageEntry(entry: PersistedUsageEntry): void {
925
970
  const line = `${JSON.stringify(normalizeUsageEntry(entry))}\n`;
926
971
  const path = usageLogPath();
@@ -941,10 +986,12 @@ export function appendUsageEntry(entry: PersistedUsageEntry): void {
941
986
  ensuredUsageLogDir = null;
942
987
  ensuredUsageLogFile = null;
943
988
  doAppend();
989
+ afterUsageLedgerAppend?.();
944
990
  return;
945
991
  }
946
992
  throw error;
947
993
  }
994
+ afterUsageLedgerAppend?.();
948
995
  }
949
996
 
950
997
  export type UsageLogRevision = {
@@ -0,0 +1,150 @@
1
+ /**
2
+ * One terminal classification for a finished logical request, and the counts that go with it.
3
+ *
4
+ * Three surfaces answer "how did this request end" and they used to answer it three different
5
+ * ways. The durable row carries `terminalStatus` and `closeReason`; the Prometheus exporter had
6
+ * its own private `classifyResult`; the dashboard read the numeric HTTP status and nothing else.
7
+ * That is not a cosmetic difference. A turn cut short by `max_output_tokens` is durably
8
+ * `status: 200, terminalStatus: "incomplete"`, which the exporter reports as `incomplete` and the
9
+ * dashboard rendered as a green 200 — the operator and the metric disagreed about whether the
10
+ * user got an answer.
11
+ *
12
+ * The fix is not a third classifier. It is this one, which the exporter imports, the management
13
+ * payload carries, and the dashboard renders, so agreement is structural rather than a rule
14
+ * someone has to keep.
15
+ *
16
+ * Leaf module: its only import is a type, erased at runtime.
17
+ */
18
+ import type { RequestSpendTotals } from "./telemetry-contract";
19
+
20
+ /**
21
+ * Ordered by how much of an answer the caller received. The order is not a ranking of severity;
22
+ * `aborted` is last because the caller chose it, not because it is the worst outcome.
23
+ */
24
+ export const REQUEST_OUTCOME_CLASSES = Object.freeze([
25
+ /** A terminal event settled the turn and the caller received the answer. */
26
+ "completed",
27
+ /** The turn ended without an answer. */
28
+ "failed",
29
+ /** The turn produced part of an answer and stopped. */
30
+ "incomplete",
31
+ /** The caller went away before the turn finished. */
32
+ "aborted",
33
+ ] as const);
34
+
35
+ export type RequestOutcomeClass = typeof REQUEST_OUTCOME_CLASSES[number];
36
+
37
+ /**
38
+ * The terminal statuses a Responses turn can settle on.
39
+ *
40
+ * Derived from the outcome classes rather than restated: a turn reports whether it completed,
41
+ * failed or stopped short, and `aborted` is not one of them because the caller leaving is not a
42
+ * terminal the origin emits. Deriving it means a fifth outcome class cannot leave this list
43
+ * stale, and restating the three would be the same copy that let the recovery roster drift.
44
+ */
45
+ export type RequestTerminalStatus = Exclude<RequestOutcomeClass, "aborted">;
46
+
47
+ /**
48
+ * The same three members as a runtime list, filtered out of the outcome roster rather than
49
+ * typed out again, so the guard below cannot disagree with the type above it.
50
+ */
51
+ export const REQUEST_TERMINAL_STATUSES: readonly RequestTerminalStatus[] = Object.freeze(
52
+ REQUEST_OUTCOME_CLASSES.filter((value): value is RequestTerminalStatus => value !== "aborted"),
53
+ );
54
+
55
+ /** Why the response body stopped being read. Closed, and persisted as such. */
56
+ export const REQUEST_CLOSE_REASONS = Object.freeze([
57
+ "terminal",
58
+ "client_cancel",
59
+ "non_stream",
60
+ "body_stall",
61
+ "body_overflow",
62
+ ] as const);
63
+
64
+ export type RequestCloseReason = typeof REQUEST_CLOSE_REASONS[number];
65
+
66
+ /**
67
+ * Read-back guards for the two facts that reach a durable row as strings.
68
+ *
69
+ * `terminalStatus` was typed `string` on the persisted entry and copied through the normalizer
70
+ * on truthiness alone, unlike the inbound protocol, transport phase and terminal source beside
71
+ * it. That was harmless while the value was only rendered; it stops being harmless the moment
72
+ * the value becomes part of a grouping key, because the string is assembled from an upstream
73
+ * frame and an unvalidated one would put upstream-controlled text into the key.
74
+ */
75
+ export function isRequestTerminalStatus(value: unknown): value is RequestTerminalStatus {
76
+ return typeof value === "string"
77
+ && (REQUEST_TERMINAL_STATUSES as readonly string[]).includes(value);
78
+ }
79
+
80
+ export function isRequestCloseReason(value: unknown): value is RequestCloseReason {
81
+ return typeof value === "string"
82
+ && (REQUEST_CLOSE_REASONS as readonly string[]).includes(value);
83
+ }
84
+
85
+ /**
86
+ * The facts a terminal classification is allowed to read.
87
+ *
88
+ * Deliberately narrow, and deliberately NOT the whole durable row: an outcome that could consult
89
+ * a provider name or an error message would be a different answer per provider, which is how the
90
+ * three surfaces drifted apart in the first place.
91
+ */
92
+ export interface RequestOutcomeFacts {
93
+ readonly status: number;
94
+ readonly terminalStatus?: string | undefined;
95
+ readonly closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow" | undefined;
96
+ }
97
+
98
+ /**
99
+ * Classify one finished logical request.
100
+ *
101
+ * Semantic terminal facts are read BEFORE the numeric status, which is the whole point. An HTTP
102
+ * 200 that carried an incomplete terminal is incomplete; a 502 that carried an incomplete
103
+ * terminal is also incomplete, and reading the status first would have called them success and
104
+ * failure. The numeric status is consulted only when no terminal event was recorded at all.
105
+ */
106
+ export function classifyRequestOutcome(facts: RequestOutcomeFacts): RequestOutcomeClass {
107
+ if (facts.closeReason === "client_cancel" || facts.status === 499) return "aborted";
108
+ if (facts.terminalStatus === "failed") return "failed";
109
+ if (facts.terminalStatus === "incomplete"
110
+ || facts.closeReason === "body_stall"
111
+ || facts.closeReason === "body_overflow") return "incomplete";
112
+ if (facts.terminalStatus === "completed") return "completed";
113
+ if (facts.terminalStatus === undefined
114
+ && (facts.status === 101 || (facts.status >= 200 && facts.status < 400))) return "completed";
115
+ return "failed";
116
+ }
117
+
118
+ /** A count is reportable only when the writer recorded a non-negative integer. */
119
+ function reportableCount(value: number | undefined): number {
120
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
121
+ }
122
+
123
+ /**
124
+ * Physical sends a finished request made, as the one number every surface shows.
125
+ *
126
+ * This READS the recorded total rather than recomputing one. An earlier draft returned
127
+ * `max(sends, reserved)` on the reasoning that a budget charge with no attempt row behind it is
128
+ * still a send that left — true, but it made the dashboard report four where the exporter, which
129
+ * sums the same attempts the recorder summed, reported three. Two defensible formulas are still
130
+ * two answers. The recorder already decided this, and `unresolved` below is where a charge with
131
+ * no attempt behind it becomes visible.
132
+ */
133
+ export function requestPhysicalSends(spend: RequestSpendTotals | undefined): number {
134
+ return reportableCount(spend?.sends);
135
+ }
136
+
137
+ /**
138
+ * Sends whose attempt reached a terminal status, and sends that did not.
139
+ *
140
+ * Kept beside {@link requestPhysicalSends} because an operator reading a send total needs to know
141
+ * how much of it is explained. An unresolved send is the quantity a duplicate-send incident shows
142
+ * up in, and folding it into the total is what made #4546 invisible for so long.
143
+ */
144
+ export function requestSettledSends(spend: RequestSpendTotals | undefined): number {
145
+ return reportableCount(spend?.settled);
146
+ }
147
+
148
+ export function requestUnresolvedSends(spend: RequestSpendTotals | undefined): number {
149
+ return reportableCount(spend?.unresolved);
150
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The usage-ledger retention limits, stated once for the schema, the server and the dashboard.
3
+ *
4
+ * Import-free, for the reason `telemetry-contract.ts` gives: the dashboard is a separate
5
+ * TypeScript project, and a type-only import still drags the imported file's whole graph into
6
+ * the browser build. Anything added here must keep that property.
7
+ */
8
+
9
+ /**
10
+ * Below this a ceiling cannot hold even one large row plus its successor, so a value under it is
11
+ * treated as unset rather than enforced into an empty ledger.
12
+ */
13
+ export const MIN_USAGE_LEDGER_MAX_BYTES = 1024 * 1024;
14
+
15
+ /** What the dashboard offers when a user turns the limit on. Not a default for the proxy. */
16
+ export const SUGGESTED_USAGE_LEDGER_MAX_BYTES = 1024 * 1024 * 1024;
17
+
18
+ /**
19
+ * Trim to this fraction of the ceiling rather than to the ceiling itself, so the next append does
20
+ * not immediately re-cross it and charge every subsequent append a full rewrite.
21
+ */
22
+ export const USAGE_LEDGER_RETENTION_TARGET_RATIO = 0.9;
23
+
24
+ export interface UsageLedgerRetentionStatus {
25
+ /** Absent when no limit is configured. */
26
+ maxBytes?: number;
27
+ currentBytes: number;
28
+ }
@@ -410,7 +410,7 @@ function isMeasuredStatus(status: UsageStatus): boolean {
410
410
  return status === "reported" || status === "estimated";
411
411
  }
412
412
 
413
- interface UsageAttribution {
413
+ export interface UsageAttribution {
414
414
  requestId: string;
415
415
  provider: string;
416
416
  model: string;
@@ -454,7 +454,7 @@ function usageModelKey(providerKey: string, model: string): string {
454
454
  return `${providerKey}\0${model}`;
455
455
  }
456
456
 
457
- function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
457
+ export function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
458
458
  if (!entry.attempts?.length) {
459
459
  return [{
460
460
  requestId: entry.requestId,