@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -68,15 +68,15 @@ export interface TransientRetryPolicy {
68
68
  }
69
69
 
70
70
  /**
71
- * Opt-in replacement of a native Responses send whose upstream connection closed while the
71
+ * Opt-in replacement of a Responses send whose upstream connection closed while the
72
72
  * caller had observed nothing (`providers.<name>.retryOnReset`).
73
73
  *
74
- * Covers both ambiguous stages the proxy can be in: no response head at all, and a head whose
75
- * SSE body carried only control events. Disabled unless the object is present; a bare `{}`
76
- * opts in with defaults. Only a request the proxy can judge self-contained is ever replaced;
77
- * see `src/server/responses/reset-replay.ts`. The replacement inference may still be billed if
78
- * the origin had already started the first one, which is what makes this opt-in rather than
79
- * default.
74
+ * Native Responses covers pre-header resets and post-header SSE carrying only control events.
75
+ * Generic translated dispatch covers initial and rebuilt pre-header sends, sharing the same
76
+ * grant and send budget; adapter-owned transports and translated post-header failures are excluded.
77
+ * Disabled unless present; `{}` opts in. Only self-contained requests qualify (see
78
+ * `src/server/responses/reset-replay.ts`). Replacement inference may still be billed if the
79
+ * origin had already started the first one, so this is opt-in rather than default.
80
80
  */
81
81
  export interface ResetReplayPolicy {
82
82
  /** Master switch. Presence of the object also enables the policy (default true). */
@@ -411,6 +411,22 @@ export interface OcxProviderConfig {
411
411
  * `ocxr1` envelopes are still stripped because no upstream can decrypt them.
412
412
  */
413
413
  preserveResponsesReasoningContent?: boolean;
414
+ /**
415
+ * Whether to preserve Codex-private `input[].id` on `store: false` requests.
416
+ * Disabled by default: ordinary Responses upstreams interpret input IDs as references
417
+ * to nonexistent stored items and return 404. Upstream launcher relays such as
418
+ * Codex Web GPT (`chatgpt-web/*`) require the current-turn user message ID for
419
+ * browser-session replay (#6220).
420
+ */
421
+ preserveResponsesInputItemIds?: boolean;
422
+ /**
423
+ * Whether to preserve ChatGPT-internal `internal_chat_message_metadata_passthrough`
424
+ * on outgoing input messages for noncanonical destinations. Disabled by default:
425
+ * public Responses gateways reject the private field as an unknown parameter.
426
+ * Upstream launcher relays such as Codex Web GPT require this metadata (or the item ID)
427
+ * to extract turn provenance (#6220).
428
+ */
429
+ preserveResponsesMessageMetadata?: boolean;
414
430
  /**
415
431
  * Treat this provider's `modelReasoningEfforts` as authoritative at the wire, not only in the
416
432
  * catalog. Adapters that ship their own per-model effort table (currently `command-code`)
@@ -652,6 +668,8 @@ export interface OcxProviderConfig {
652
668
  */
653
669
  autoReviewModelOverrides?: Record<string, string>;
654
670
  headers?: Record<string, string>;
671
+ /** Inbound client metadata headers to copy to this provider when the outbound field is otherwise unset. */
672
+ forwardClientHeaders?: string[];
655
673
  /** Default provider-routing preferences for models sent through the canonical OpenRouter API. */
656
674
  openRouterRouting?: OpenRouterProviderRouting;
657
675
  /** Exact model-id overrides for `openRouterRouting`. Each matching entry replaces the default. */
@@ -992,9 +1010,9 @@ export interface OcxProviderConfig {
992
1010
  */
993
1011
  transientRetryOn5xx?: TransientRetryPolicy;
994
1012
  /**
995
- * Opt-in replacement of a native Responses send that died while the caller had observed
996
- * nothing (`providers.<name>.retryOnReset`). Disabled unless present; a bare `{}` opts in
997
- * with defaults. Native Responses sends only, and only for self-contained requests.
1013
+ * Opt-in replacement of self-contained Responses sends (`providers.<name>.retryOnReset`).
1014
+ * Disabled unless present; `{}` opts in. Native sends and generic translated initial/rebuilt
1015
+ * pre-header sends share the grant/budget; adapter-owned and translated post-header failures are excluded.
998
1016
  */
999
1017
  retryOnReset?: ResetReplayPolicy;
1000
1018
  /**
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { manualUpdateFailureGuidance } from "./update-failure-guidance.mjs";
2
3
  import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
3
4
  import { proxyIdentityAt } from "../server/proxy-liveness";
4
5
  import { probeProxyLiveness } from "./proxy-liveness-probe.mjs";
@@ -922,7 +923,7 @@ export async function runUpdate(): Promise<void> {
922
923
  spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true, env: mutation.unprivilegedEnvironment() });
923
924
  }
924
925
  await recoverStoppedRuntime("package update failed");
925
- console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`);
926
+ console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). ${manualUpdateFailureGuidance({ bin, args: cmdArgs, owner }).join("\n")}`);
926
927
  return 1;
927
928
  }
928
929
  } catch (error) {
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, realpathSync } from "node:fs";
2
2
  import { win32 } from "node:path";
3
3
 
4
4
  const CMD_META = /([()%!^"`<>&|;, *?])/g;
@@ -38,11 +38,41 @@ export function resolveNpmCommand(
38
38
  ) {
39
39
  if (platform !== "win32") return "npm";
40
40
  const exists = deps.exists ?? existsSync;
41
+ const realpath = deps.realpath ?? realpathSync.native;
41
42
  const cwd = deps.cwd ?? process.cwd();
42
43
  const trustedRoots = [env.APPDATA, env.LOCALAPPDATA, env.ProgramFiles, env["ProgramFiles(x86)"],
43
44
  env.USERPROFILE && win32.join(env.USERPROFILE, "scoop", "shims")]
44
45
  .filter(root => typeof root === "string" && win32.isAbsolute(root));
45
46
  const trustedEntry = entry => trustedRoots.some(root => isInside(root, entry) && !isInside(root, cwd));
47
+ // Scoop installs Node's npm in the app tree, not in scoop/shims. Admit only
48
+ // the two Node apps' current npm directories, never arbitrary Scoop apps or
49
+ // a launch directory inside the installation itself.
50
+ const scoopNodeRoots = typeof env.USERPROFILE === "string" && win32.isAbsolute(env.USERPROFILE)
51
+ ? ["nodejs", "nodejs-lts"].map(app => win32.join(env.USERPROFILE, "scoop", "apps", app))
52
+ : [];
53
+ const trustedScoopNodeEntry = (entry, candidate) => {
54
+ const app = scoopNodeRoots.find(root => {
55
+ const current = win32.join(root, "current");
56
+ return isSamePath(current, entry) || isSamePath(win32.join(current, "bin"), entry);
57
+ });
58
+ if (!app) return null;
59
+ try {
60
+ const appPath = realpath(app);
61
+ const currentPath = realpath(win32.join(app, "current"));
62
+ const entryPath = realpath(entry);
63
+ const candidatePath = realpath(candidate);
64
+ const cwdPath = realpath(cwd);
65
+ const persistBin = win32.join(realpath(env.USERPROFILE), "scoop", "persist", win32.basename(app), "bin");
66
+ const insideInstall = isInside(currentPath, entryPath);
67
+ const persistedBin = isSamePath(entry, win32.join(app, "current", "bin"))
68
+ && isSamePath(entryPath, persistBin);
69
+ return !isSamePath(appPath, currentPath) && isInside(appPath, currentPath)
70
+ && (insideInstall || persistedBin) && isInside(entryPath, candidatePath)
71
+ && !isInside(currentPath, cwdPath) && !isInside(entryPath, cwdPath);
72
+ } catch {
73
+ return false;
74
+ }
75
+ };
46
76
  const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
47
77
  .split(";")
48
78
  .filter(Boolean);
@@ -54,10 +84,12 @@ export function resolveNpmCommand(
54
84
  for (const entry of pathEntries) {
55
85
  if (!win32.isAbsolute(entry)) continue;
56
86
  if (isSamePath(entry, cwd)) continue;
57
- if (isInside(cwd, entry) && !trustedEntry(entry)) continue;
58
87
  for (const extension of extensions) {
59
88
  const candidate = win32.join(entry, `npm${extension.toLowerCase()}`);
60
- if (exists(candidate)) return win32.resolve(candidate);
89
+ if (!exists(candidate)) continue;
90
+ const scoopTrust = trustedScoopNodeEntry(entry, candidate);
91
+ if (scoopTrust === false || (scoopTrust === null && isInside(cwd, entry) && !trustedEntry(entry))) continue;
92
+ return win32.resolve(candidate);
61
93
  }
62
94
  }
63
95
  return null;
@@ -16,7 +16,16 @@ import { spawnSync } from "node:child_process";
16
16
  * stop. Fail-open was wrong here: a listener that accepts connections but withholds
17
17
  * `/healthz`, or a probe that times out, is exactly the state where replacing package
18
18
  * files is most dangerous, and "we could not tell" is not evidence the proxy is gone.
19
- * Only a refused connection or a definitive non-OpenCodex answer earns `"dead"`.
19
+ * A refused connection or a definitive non-OpenCodex answer earns `"dead"`. So does one
20
+ * more observation when the dial is neither answered nor refused (a timeout, or a dropped
21
+ * SYN as with a listener bound to a tailnet address): the child tries one transient
22
+ * exclusive bind of the same host and port, and a successful bind means nothing holds the
23
+ * port at that instant. The fallback runs only for a literal IP address, because a name can
24
+ * resolve to different addresses for the dial and the bind. A failed bind (`EADDRINUSE`,
25
+ * `EADDRNOTAVAIL`, any error) stays
26
+ * `"unknown"`, a held-but-silent port included. The whole probe is bounded by the child's
27
+ * own `timeoutMs` plus a 1500 ms spawn ceiling, after which it is `"unknown"`. A successful
28
+ * bind is a statement about that moment, not a promise that the endpoint cannot restart.
20
29
  */
21
30
  export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 1500) {
22
31
  // An unusable port is not an ambiguous probe: there is nothing to ask.
@@ -38,6 +47,8 @@ export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 150
38
47
  const script = [
39
48
  "const http = require('node:http');",
40
49
  "const [host, port, timeout] = process.argv.slice(1);",
50
+ "let settled = false;",
51
+ "const settle = word => { if (!settled) { settled = true; process.stdout.write(word); } };",
41
52
  "const req = http.get({ host, port: Number(port), path: '/healthz', timeout: Number(timeout) }, res => {",
42
53
  " let body = '';",
43
54
  " res.setEncoding('utf8');",
@@ -56,15 +67,34 @@ export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 150
56
67
  " && typeof parsed.uptime === 'number'));",
57
68
  " // Only a clean 200 decides anything. Any other status means the endpoint is",
58
69
  " // answering but not telling us what it is, which is not evidence of absence.",
59
- " if (res.statusCode !== 200) process.stdout.write('UNKNOWN');",
60
- " else process.stdout.write(isOpencodex ? 'LIVE' : 'DEAD');",
61
- " } catch { process.stdout.write('UNKNOWN'); }",
70
+ " if (res.statusCode !== 200) settle('UNKNOWN');",
71
+ " else settle(isOpencodex ? 'LIVE' : 'DEAD');",
72
+ " } catch { settle('UNKNOWN'); }",
62
73
  " });",
63
74
  "});",
64
- "req.on('timeout', () => { process.stdout.write('UNKNOWN'); req.destroy(); });",
65
- "// ECONNREFUSED is the one error that proves nothing is listening. Everything else -",
66
- "// reset, unreachable host, TLS confusion - leaves the question open.",
67
- "req.on('error', err => process.stdout.write(err && err.code === 'ECONNREFUSED' ? 'DEAD' : 'UNKNOWN'));",
75
+ // A refused connection is the clean proof that nothing is listening. Everything else -
76
+ // reset, unreachable host, a silent drop - leaves the question open, with one more way
77
+ // to close it: a port nobody listens on can be bound, a held one cannot (EADDRINUSE).
78
+ // A proxy bound to a Tailscale address is the case that needs this: the stack drops
79
+ // the SYN instead of refusing it, so the dial only ever times out.
80
+ "let bindStarted = false;",
81
+ "const bindCheck = () => {",
82
+ " if (bindStarted || settled) return;",
83
+ " bindStarted = true;",
84
+ // A name can resolve to several addresses, and the dial and the bind resolve it on
85
+ // their own: a live proxy on one address would not stop a bind on another. Only a
86
+ // literal address names the same endpoint on both sides.
87
+ " if (require('node:net').isIP(host) === 0) { settle('UNKNOWN'); return; }",
88
+ " const server = require('node:net').createServer();",
89
+ " server.on('connection', socket => socket.destroy());",
90
+ " server.once('error', () => settle('UNKNOWN'));",
91
+ " server.listen({ host, port: Number(port), exclusive: true }, () => server.close(() => settle('DEAD')));",
92
+ "};",
93
+ "req.on('timeout', () => { req.destroy(); bindCheck(); });",
94
+ "req.on('error', err => {",
95
+ " if (err && err.code === 'ECONNREFUSED') settle('DEAD');",
96
+ " else bindCheck();",
97
+ "});",
68
98
  ].join("\n");
69
99
  try {
70
100
  const probe = spawnSync(
@@ -1,3 +1,9 @@
1
+ export function manualUpdateFailureGuidance(command: {
2
+ bin: string;
3
+ args: string[];
4
+ owner?: import("./pnpm-global-install.mjs").PnpmGlobalOwner;
5
+ platform?: NodeJS.Platform;
6
+ }): string[];
1
7
  export function npmUpdateFailureGuidance(failure: {
2
8
  phase?: string;
3
9
  rolledBack?: boolean;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * What to tell the operator after a failed npm self-update (#5624).
2
+ * What to tell the operator after a failed self-update (#5624).
3
3
  *
4
4
  * The transactional updater (`transactional-install.mjs`) either leaves the live package
5
5
  * untouched, rolls it back, or — only on a double fault — leaves it moved aside with a recovery
@@ -10,6 +10,28 @@
10
10
  * Plain ESM with no Bun APIs: the Node launcher (`bin/ocx.mjs`) imports it.
11
11
  */
12
12
 
13
+ import { pnpmGlobalCommandArgs } from "./pnpm-global-install.mjs";
14
+
15
+ /**
16
+ * Manual replacement must wait for recovery and stop the runtime through its owner first.
17
+ * @param {{ bin: string; args: string[]; owner?: import("./pnpm-global-install.mjs").PnpmGlobalOwner; platform?: NodeJS.Platform }} command
18
+ * @returns {string[]}
19
+ */
20
+ export function manualUpdateFailureGuidance({ bin, args, owner, platform = process.platform }) {
21
+ const windows = platform === "win32";
22
+ const quote = value => /^[a-zA-Z0-9_@./:=-]+$/.test(value) ? value
23
+ : "'" + value.replaceAll("'", windows ? "''" : "'\\''") + "'";
24
+ const commandBin = owner ? owner.commandPath : bin;
25
+ const commandArgs = owner ? pnpmGlobalCommandArgs(args, owner) : args;
26
+ const install = (windows ? "& " : "") + [commandBin, ...commandArgs].map(quote).join(" ");
27
+ return [
28
+ "Next: run 'ocx status' and let any in-progress recovery finish.",
29
+ "If a proxy is running, stop it through its owner ('ocx stop', the owning service, or the desktop app) and confirm it has stopped before installing.",
30
+ (windows ? "Then run in PowerShell: " : "Then run: ") + install,
31
+ "After installation completes, restart through the owning service ('ocx service restart') or desktop app; run 'ocx start' if unmanaged.",
32
+ ];
33
+ }
34
+
13
35
  /** Phases in which the live package was never touched. */
14
36
  const UNTOUCHED_PHASES = new Set(["stage", "verify", "swap-backup"]);
15
37
 
@@ -18,14 +40,17 @@ const UNTOUCHED_PHASES = new Set(["stage", "verify", "swap-backup"]);
18
40
  * @returns {{ previousVersionKept: boolean; lines: string[] }}
19
41
  */
20
42
  export function npmUpdateFailureGuidance({ phase, rolledBack, pkgName, version, tag }) {
21
- const reinstall = "npm install -g --allow-scripts=bun " + pkgName + "@" + (version || tag || "latest");
43
+ const manual = manualUpdateFailureGuidance({
44
+ bin: "npm", args: ["install", "-g", "--allow-scripts=bun", pkgName + "@" + (version || tag || "latest")],
45
+ });
22
46
  const kept = UNTOUCHED_PHASES.has(phase ?? "") || rolledBack === true;
23
47
  if (!kept) {
24
48
  return {
25
49
  previousVersionKept: false,
26
50
  lines: [
27
51
  "The previous version was moved aside and could not be put back automatically.",
28
- "Next: run the \"restore\" command recorded in .ocx-recovery.json next to the package, or reinstall with '" + reinstall + "'.",
52
+ "Next: run the \"restore\" command recorded in .ocx-recovery.json next to the package, or follow the manual reinstall steps below.",
53
+ ...manual,
29
54
  ],
30
55
  };
31
56
  }
@@ -33,8 +58,8 @@ export function npmUpdateFailureGuidance({ phase, rolledBack, pkgName, version,
33
58
  previousVersionKept: true,
34
59
  lines: [
35
60
  "The previous version is still installed.",
36
- "Next: run 'ocx update' again. If it fails the same way, run 'ocx stop', then '" + reinstall
37
- + "', then 'ocx service restart' ('ocx start' when no background service is installed).",
61
+ "Next: run 'ocx update' again. If it fails the same way, reinstall manually:",
62
+ ...manual,
38
63
  ],
39
64
  };
40
65
  }
@@ -370,6 +370,15 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
370
370
  { provider: "google-antigravity", modelId: "claude-opus-4-6-thinking", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
371
371
  // Alias without the Antigravity "-thinking" suffix (if logs/UI ever surface it).
372
372
  { provider: "google-antigravity", modelId: "claude-opus-4-6", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
373
+ // Antigravity uses subscription quota; these are derived Anthropic reference estimates.
374
+ { provider: "google-antigravity", modelId: "claude-sonnet-5-5", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
375
+ { provider: "google-antigravity", modelId: "claude-sonnet-5-5-low", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
376
+ { provider: "google-antigravity", modelId: "claude-sonnet-5-5-medium", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
377
+ { provider: "google-antigravity", modelId: "claude-sonnet-5-5-high", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
378
+ { provider: "google-antigravity", modelId: "claude-opus-5-5", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
379
+ { provider: "google-antigravity", modelId: "claude-opus-5-5-low", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
380
+ { provider: "google-antigravity", modelId: "claude-opus-5-5-medium", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
381
+ { provider: "google-antigravity", modelId: "claude-opus-5-5-high", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
373
382
  { provider: "google-antigravity", modelId: "gpt-oss-120b-medium", cost4: { input: 0.03, output: 0.15, cacheRead: 0, cacheWrite: 0 }, source: "derived: gpt-oss-120b open-weights — OpenRouter advertised lowest https://openrouter.ai/openai/gpt-oss-120b/providers", verifiedAt: "2026-07-20", status: "verified-derived" },
374
383
  // Kimi / Moonshot — official price tables are now published (2026-07-20 re-check;
375
384
  // previously empty). kimi = Kimi Code OAuth surface, moonshot = CN key surface,
package/src/usage/log.ts CHANGED
@@ -347,6 +347,9 @@ export interface PersistedUsageEntry {
347
347
  durationMs: number;
348
348
  /** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
349
349
  firstOutputMs?: number;
350
+ /** Request-relative generation window (#6309): first output item, last output delta. Written as a pair. */
351
+ genStartMs?: number;
352
+ lastOutputMs?: number;
350
353
  usageStatus: UsageStatus;
351
354
  usage?: OcxUsage;
352
355
  totalTokens?: number;
@@ -1035,6 +1038,10 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
1035
1038
  ...(isNonNegativeFiniteNumber(entry.firstOutputMs)
1036
1039
  ? { firstOutputMs: entry.firstOutputMs }
1037
1040
  : {}),
1041
+ ...(isNonNegativeFiniteNumber(entry.genStartMs) && isNonNegativeFiniteNumber(entry.lastOutputMs)
1042
+ && entry.lastOutputMs >= entry.genStartMs
1043
+ ? { genStartMs: entry.genStartMs, lastOutputMs: entry.lastOutputMs }
1044
+ : {}),
1038
1045
  usageStatus: entry.usageStatus,
1039
1046
  ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
1040
1047
  ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
@@ -10,7 +10,7 @@ import {
10
10
  type PersistedUsageEntry,
11
11
  type UsageStatus,
12
12
  } from "./log";
13
- import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext } from "./cost";
13
+ import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext, tokensPerSecond } from "./cost";
14
14
 
15
15
  /**
16
16
  * Canonical range members. The warm-up loop in the management usage route
@@ -73,6 +73,18 @@ export interface UsageSummaryTotals {
73
73
  cacheSynthesizedRequests?: number;
74
74
  /** Rows with no cache detail at all. Not a miss, and not a zero. */
75
75
  cacheUnknownRequests?: number;
76
+ /**
77
+ * Aggregated output throughput (#6309): output tokens and wall-clock milliseconds summed over
78
+ * the attempts whose usage was measured, plus how many attempts qualified. The rate is the
79
+ * token sum over the duration sum — a time-weighted average, not a mean of per-request rates —
80
+ * so one fast tiny request cannot outweigh many slow large ones. Attempts without both a
81
+ * positive token count and a positive duration contribute nothing.
82
+ */
83
+ throughputOutputTokens?: number;
84
+ throughputDurationMs?: number;
85
+ throughputSamples?: number;
86
+ /** Derived at finalize time: throughputOutputTokens / throughputDurationMs * 1000. */
87
+ throughputTokensPerSec?: number;
76
88
  }
77
89
 
78
90
  export interface UsageDay {
@@ -118,6 +130,10 @@ export interface UsageModel {
118
130
  totalTokens: number;
119
131
  inputTokens: number;
120
132
  outputTokens: number;
133
+ /** Aggregate output throughput for this row (#6309): summed measured tokens over summed duration. */
134
+ throughputTokensPerSec?: number;
135
+ /** Attempts behind the throughput aggregate; absent when nothing was measured. */
136
+ throughputSamples?: number;
121
137
  cachedInputTokens?: number;
122
138
  cacheReadInputTokens?: number;
123
139
  cacheCreationInputTokens?: number;
@@ -141,6 +157,12 @@ export interface UsageProvider {
141
157
  totalTokens: number;
142
158
  inputTokens?: number;
143
159
  outputTokens?: number;
160
+ /**
161
+ * Aggregate output throughput for this row (#6309): summed measured output tokens over summed
162
+ * wall-clock duration. Undefined when no attempt in the row reported both.
163
+ */
164
+ throughputTokensPerSec?: number;
165
+ throughputSamples?: number;
144
166
  cachedInputTokens?: number;
145
167
  cacheReadInputTokens?: number;
146
168
  cacheCreationInputTokens?: number;
@@ -420,6 +442,8 @@ export interface UsageAttribution {
420
442
  usageStatus: UsageStatus;
421
443
  usage?: PersistedUsageEntry["usage"];
422
444
  totalTokens?: number;
445
+ /** Wall-clock duration of the attempt, or of the row when it has no attempts. */
446
+ durationMs?: number;
423
447
  /** Attempt provenance when the row has one, else the entry's; never assumed observed. */
424
448
  cacheProvenance?: CacheTelemetryProvenance;
425
449
  }
@@ -465,6 +489,7 @@ export function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[
465
489
  usageStatus: entry.usageStatus,
466
490
  ...(entry.usage ? { usage: entry.usage } : {}),
467
491
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
492
+ durationMs: entry.durationMs,
468
493
  ...(entry.cacheProvenance ? { cacheProvenance: entry.cacheProvenance } : {}),
469
494
  }];
470
495
  }
@@ -482,6 +507,7 @@ export function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[
482
507
  usageStatus: attempt.usageStatus,
483
508
  ...(attempt.usage ? { usage: attempt.usage } : {}),
484
509
  ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
510
+ durationMs: attempt.durationMs,
485
511
  ...(cacheProvenance ? { cacheProvenance } : {}),
486
512
  };
487
513
  });
@@ -611,6 +637,30 @@ function finalizeCoverage(totals: UsageSummaryTotals): void {
611
637
  totals.coverageRatio = totals.requests === 0 ? 0 : totals.measuredRequests / totals.requests;
612
638
  }
613
639
 
640
+ /**
641
+ * Aggregate output throughput inputs (#6309) into the window totals. A measured attempt
642
+ * contributes its output tokens and duration only when both are positive, so a zero-token
643
+ * or zero-duration row cannot deflate or inflate the rate. Without attempts, the row-level
644
+ * usage and duration describe the request directly.
645
+ */
646
+ function addThroughputTotals(totals: UsageSummaryTotals, entry: PersistedUsageEntry): void {
647
+ const samples = entry.attempts?.length
648
+ ? entry.attempts
649
+ : [{ usage: entry.usage, durationMs: entry.durationMs }];
650
+ for (const sample of samples) {
651
+ if (!sample.usage || tokensPerSecond(sample.usage.outputTokens, sample.durationMs) === null) continue;
652
+ totals.throughputOutputTokens = (totals.throughputOutputTokens ?? 0) + sample.usage.outputTokens;
653
+ totals.throughputDurationMs = (totals.throughputDurationMs ?? 0) + sample.durationMs;
654
+ totals.throughputSamples = (totals.throughputSamples ?? 0) + 1;
655
+ }
656
+ }
657
+
658
+ /** Fold the raw throughput sums into the rate the dashboard and API expose. */
659
+ function finalizeThroughput(totals: UsageSummaryTotals): void {
660
+ if ((totals.throughputDurationMs ?? 0) <= 0) return;
661
+ totals.throughputTokensPerSec = tokensPerSecond(totals.throughputOutputTokens ?? 0, totals.throughputDurationMs!) ?? undefined;
662
+ }
663
+
614
664
  function addEstimatedCost(
615
665
  totals: UsageSummaryTotals,
616
666
  entry: Pick<PersistedUsageEntry, "usageStatus" | "usage" | "attempts">,
@@ -670,6 +720,9 @@ interface UsageModelAccumulator {
670
720
  cacheObserved: boolean;
671
721
  /** Input tokens from attributions with OBSERVED cache detail; the hit-rate denominator. */
672
722
  cacheObservedInputTokens: number;
723
+ throughputOutputTokens: number;
724
+ throughputDurationMs: number;
725
+ throughputSamples: number;
673
726
  estimatedCostUsd?: number;
674
727
  requestCounts: UsageRequestCounts;
675
728
  requestFacts?: Map<number, number>;
@@ -820,6 +873,9 @@ function mergeTotals(target: UsageSummaryTotals, source: UsageSummaryTotals): vo
820
873
  target.cacheObservedRequests = mergeOptionalTotal(target.cacheObservedRequests, source.cacheObservedRequests);
821
874
  target.cacheSynthesizedRequests = mergeOptionalTotal(target.cacheSynthesizedRequests, source.cacheSynthesizedRequests);
822
875
  target.cacheUnknownRequests = mergeOptionalTotal(target.cacheUnknownRequests, source.cacheUnknownRequests);
876
+ target.throughputOutputTokens = mergeOptionalTotal(target.throughputOutputTokens, source.throughputOutputTokens);
877
+ target.throughputDurationMs = mergeOptionalTotal(target.throughputDurationMs, source.throughputDurationMs);
878
+ target.throughputSamples = mergeOptionalTotal(target.throughputSamples, source.throughputSamples);
823
879
  }
824
880
 
825
881
  /** Sum an optional total. Absent on one side means "not computed there", so it contributes nothing. */
@@ -849,6 +905,9 @@ function blankModelAccumulator(
849
905
  cacheCreationInputTokens: 0,
850
906
  cacheObserved: false,
851
907
  cacheObservedInputTokens: 0,
908
+ throughputOutputTokens: 0,
909
+ throughputDurationMs: 0,
910
+ throughputSamples: 0,
852
911
  requestCounts: blankRequestCounts(),
853
912
  ...(mode === "exact" ? { requestFacts: new Map() } : {}),
854
913
  };
@@ -877,6 +936,9 @@ function mergeModelAccumulator(target: UsageModelAccumulator, source: UsageModel
877
936
  target.cacheCreationInputTokens += source.cacheCreationInputTokens;
878
937
  target.cacheObserved ||= source.cacheObserved;
879
938
  target.cacheObservedInputTokens += source.cacheObservedInputTokens;
939
+ target.throughputOutputTokens += source.throughputOutputTokens;
940
+ target.throughputDurationMs += source.throughputDurationMs;
941
+ target.throughputSamples += source.throughputSamples;
880
942
  if (source.estimatedCostUsd !== undefined) {
881
943
  target.estimatedCostUsd = (target.estimatedCostUsd ?? 0) + source.estimatedCostUsd;
882
944
  }
@@ -1081,6 +1143,12 @@ function buildUsageModels(
1081
1143
  totalTokens: model.summaryTotalTokens,
1082
1144
  inputTokens: model.inputTokens,
1083
1145
  outputTokens: model.outputTokens,
1146
+ ...(model.throughputDurationMs > 0
1147
+ ? {
1148
+ throughputTokensPerSec: tokensPerSecond(model.throughputOutputTokens, model.throughputDurationMs) ?? undefined,
1149
+ throughputSamples: model.throughputSamples,
1150
+ }
1151
+ : {}),
1084
1152
  cachedInputTokens: model.cacheReadInputTokens,
1085
1153
  cacheReadInputTokens: model.cacheReadInputTokens,
1086
1154
  cacheCreationInputTokens: model.cacheCreationInputTokens,
@@ -1120,6 +1188,12 @@ function buildUsageProviders(
1120
1188
  totalTokens: provider.summaryTotalTokens,
1121
1189
  inputTokens: provider.inputTokens,
1122
1190
  outputTokens: provider.outputTokens,
1191
+ ...(provider.throughputDurationMs > 0
1192
+ ? {
1193
+ throughputTokensPerSec: tokensPerSecond(provider.throughputOutputTokens, provider.throughputDurationMs) ?? undefined,
1194
+ throughputSamples: provider.throughputSamples,
1195
+ }
1196
+ : {}),
1123
1197
  cachedInputTokens: provider.cacheReadInputTokens,
1124
1198
  cacheReadInputTokens: provider.cacheReadInputTokens,
1125
1199
  cacheCreationInputTokens: provider.cacheCreationInputTokens,
@@ -1298,6 +1372,11 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
1298
1372
  if (attribution.usage) {
1299
1373
  breakdown.inputTokens += attribution.usage.inputTokens;
1300
1374
  breakdown.outputTokens += attribution.usage.outputTokens;
1375
+ if (tokensPerSecond(attribution.usage.outputTokens, attribution.durationMs ?? 0) !== null) {
1376
+ breakdown.throughputOutputTokens += attribution.usage.outputTokens;
1377
+ breakdown.throughputDurationMs += attribution.durationMs!;
1378
+ breakdown.throughputSamples += 1;
1379
+ }
1301
1380
  const { read, creation, provenance } = cacheObservationFromUsage(
1302
1381
  attribution.usage,
1303
1382
  attribution.cacheProvenance,
@@ -1463,6 +1542,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
1463
1542
  addTokens(partition.totals, entry);
1464
1543
  addSpendTotals(partition.totals, entry);
1465
1544
  addCacheProvenanceTotals(partition.totals, entry);
1545
+ addThroughputTotals(partition.totals, entry);
1466
1546
  addEstimatedCost(partition.totals, entry, costInfo);
1467
1547
 
1468
1548
  const requestKey = this.mode === "exact" ? this.requestKey(entry.requestId) : null;
@@ -1556,6 +1636,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
1556
1636
  day.modelOverlaps.push(...partition.modelOverlaps.values());
1557
1637
  }
1558
1638
  finalizeCoverage(totals);
1639
+ finalizeThroughput(totals);
1559
1640
 
1560
1641
  const customDates = this.window ? new Set(customWindowDates(this.window)) : null;
1561
1642
  const dayCount = customDates?.size ?? (range === "all" ? dayCountForAllRange(oldestTimestamp, now) : fixedDays);
@@ -382,9 +382,6 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
382
382
  const messages: OcxMessage[] = [...parsed.context.messages];
383
383
  const loopT0 = Date.now();
384
384
  const allTools = parsed.context.tools ?? [];
385
- // For the forced-answer pass we drop the synthetic web_search tool so the model MUST answer from the
386
- // results already in `messages` (can't search again) — this guarantees a non-empty final answer.
387
- const toolsNoWebSearch = allTools.filter(t => !t.webSearch);
388
385
  let searchesExecuted = 0;
389
386
  let executedSearchCount = 0;
390
387
  // Queries whose search already failed this turn — repeats are short-circuited so a model that keeps
@@ -438,8 +435,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
438
435
  * restart) before the `on429` key rotation.
439
436
  */
440
437
  const prepareIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
441
- // On the forced-answer pass the synthetic web_search tool is gone, so the model MUST answer
442
- // from the results already in `messages`. A weak model can still produce a thin answer that
438
+ // On the forced-answer pass the model is asked to answer from the results already in
439
+ // `messages`. A weak model can still produce a thin answer that
443
440
  // ignores what the search found, which reads to the user as "the search did nothing". Nudge it
444
441
  // (iteration-locally — never mutate the shared `messages`) to actually use the gathered results.
445
442
  // Only when a REAL search ran (executedSearchCount, not empty-query/limit/repeat placeholders).
@@ -455,7 +452,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
455
452
  const iterParsed: OcxParsedRequest = {
456
453
  ...parsed, stream: true,
457
454
  ...(recoveringEmptyAnswer ? { options: { ...parsed.options, toolChoice: "none" as const } } : {}),
458
- context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : forceAnswer ? toolsNoWebSearch : allTools },
455
+ context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : allTools },
459
456
  };
460
457
  // One cumulative header deadline spans every pool-key 429 rotation in this model iteration.
461
458
  // clear() stops only its timer after final headers; the direct turn signal remains attached to
@@ -885,8 +882,13 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
885
882
 
886
883
  // Loop (search + re-ask) ONLY when the model's actionable output is purely web_search. A real
887
884
  // tool call (e.g. shell/apply_patch) means this turn is terminal for Codex — finalize so those
888
- // calls reach Codex. forceAnswer also finalizes.
889
- const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceAnswer;
885
+ // calls reach Codex.
886
+ // The budget check happens per query inside runSearchCall, so a
887
+ // forced pass that emits one more web_search call is served the
888
+ // limit-reached result instead of failing the turn (#6464: stripping
889
+ // the declaration makes some models emit the raw call as visible
890
+ // text).
891
+ const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall;
890
892
  if (!shouldLoop) {
891
893
  // #1001: a forced-answer pass that ends `done` must have produced
892
894
  // usable output — never a malformed tool call, and never silence.
@@ -959,6 +961,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
959
961
  return;
960
962
  }
961
963
  }
964
+ yield { type: "error", message: "web-search loop exceeded its iteration cap" };
962
965
  } finally {
963
966
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
964
967
  }