@bitkyc08/opencodex 2.58.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -30,6 +30,7 @@ import {
30
30
  createSseTerminalOutputBoundary,
31
31
  doneFrame,
32
32
  failedTailFrame,
33
+ refusalFailedTailFrame,
33
34
  upstreamErrorTailFrame,
34
35
  } from "./relay";
35
36
  import {
@@ -127,6 +128,23 @@ export function relaySseEagerBounded(
127
128
  return null;
128
129
  }
129
130
  };
131
+ /**
132
+ * Same tail, except that a refusal the boundary already captured outranks the
133
+ * read failure that followed it — the upstream ended the turn before the
134
+ * socket did (#5176).
135
+ */
136
+ const encodeTerminalTail = (error: unknown): Uint8Array | null => {
137
+ const refusalCode = terminalBoundary.upstreamRefusalCode();
138
+ const refusalMessage = terminalBoundary.upstreamError();
139
+ if (refusalCode === undefined || refusalMessage === undefined) {
140
+ return encodeFailedTail(error);
141
+ }
142
+ try {
143
+ return refusalFailedTailFrame(terminalEncoder, refusalMessage, refusalCode);
144
+ } catch {
145
+ return null;
146
+ }
147
+ };
130
148
  const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
131
149
  const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
132
150
  const rewriteBudget = opts?.rewriteBudget;
@@ -313,7 +331,11 @@ export function relaySseEagerBounded(
313
331
  const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
314
332
  const upstreamErrorFrame = upstreamError === undefined
315
333
  ? adapterEofFrame
316
- : upstreamErrorTailFrame(terminalEncoder, upstreamError);
334
+ : upstreamErrorTailFrame(
335
+ terminalEncoder,
336
+ upstreamError,
337
+ terminalBoundary.upstreamRefusalCode(),
338
+ );
317
339
  queuedBytes += upstreamErrorFrame.byteLength + terminalSentinel.byteLength;
318
340
  try {
319
341
  controllerRef?.enqueue(upstreamErrorFrame);
@@ -439,7 +461,7 @@ export function relaySseEagerBounded(
439
461
  // getters, toString) that re-entrantly cancel the client or abort the
440
462
  // upstream. Build the tail FIRST, then re-check eligibility before
441
463
  // committing to the synthetic terminal (adversarial review blocker).
442
- const tail = encodeFailedTail(err);
464
+ const tail = encodeTerminalTail(err);
443
465
  if (tail && canDeliver()) {
444
466
  // Inspection and client framing have separate bounded parsers. If
445
467
  // inspection resynchronized after an oversized frame and observed a
@@ -5,6 +5,9 @@ import {
5
5
  CYBER_POLICY_FALLBACK_MESSAGE,
6
6
  isCyberPolicyCode,
7
7
  isCyberPolicyMessage,
8
+ isTerminalRefusalCode,
9
+ safetyRefusalCodeFromMessage,
10
+ terminalRefusalFallbackMessage,
8
11
  upstreamErrorMessageFromPayload,
9
12
  } from "../lib/errors";
10
13
  import { redactSecretString } from "../lib/redact";
@@ -22,10 +25,11 @@ import {
22
25
  } from "./request-log";
23
26
  import {
24
27
  BoundedSseFrameBuffer,
28
+ EMPTY_BYTES,
25
29
  joinSseFrameBytes,
26
30
  MAX_CLIENT_SSE_FRAME_BYTES,
27
31
  } from "./sse-frame-buffer";
28
- import { replaceSseDataPayload } from "./sse-payload-rewrite";
32
+ import { replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
29
33
  import { createBoundedResponseLogBody } from "./response-log-body";
30
34
 
31
35
  const nativePassthroughSseResponses = new WeakSet<Response>();
@@ -149,16 +153,61 @@ export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array
149
153
  return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
150
154
  }
151
155
 
152
- export function upstreamErrorTailFrame(encoder: TextEncoder, message: string): Uint8Array {
156
+ /**
157
+ * Close a turn the upstream ended without a Responses terminal.
158
+ *
159
+ * `refusalCode` carries the upstream's own verdict when it gave one. Codex
160
+ * classifies this terminal by `error.code` alone and retries everything outside
161
+ * its fatal set (codex-rs/codex-api/src/sse/responses.rs:423-450), so stamping
162
+ * `upstream_server_error` on a refusal delivered it as a retryable disconnect
163
+ * and drove the reconnect loop in #5176. Without a refusal code the terminal is
164
+ * unchanged: a genuine transport failure stays retryable, which is what it is.
165
+ */
166
+ export function upstreamErrorTailFrame(
167
+ encoder: TextEncoder,
168
+ message: string,
169
+ refusalCode?: string,
170
+ ): Uint8Array {
171
+ return encoder.encode(
172
+ `event: response.failed\ndata: ${upstreamErrorFailedPayload(message, refusalCode)}\n\n`,
173
+ );
174
+ }
175
+
176
+ function upstreamErrorFailedPayload(message: string, refusalCode?: string): string {
153
177
  const error = {
154
- type: "upstream_error",
155
- code: "upstream_server_error",
178
+ type: refusalCode === undefined ? "upstream_error" : "invalid_request_error",
179
+ code: refusalCode ?? "upstream_server_error",
156
180
  message: redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
157
181
  };
158
- return encoder.encode(`event: response.failed\ndata: ${JSON.stringify({
182
+ return JSON.stringify({
159
183
  type: "response.failed",
160
- response: { status: "failed", error, last_error: error },
161
- })}\n\n`);
184
+ response: {
185
+ status: "failed",
186
+ error,
187
+ last_error: error,
188
+ ...(refusalCode === undefined ? {} : { retryable: false }),
189
+ },
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Terminal for a read that failed after the upstream had already refused.
195
+ *
196
+ * Framed exactly like {@link failedTailFrame} — leading blank line to close a
197
+ * partial block, then the sentinel — but carrying the refusal instead of the
198
+ * generic reset. The refusal is the real outcome of the turn and the socket
199
+ * teardown that followed it is not, so reporting `upstream_reset` here would
200
+ * restart a turn the upstream has already ended (#5176).
201
+ */
202
+ export function refusalFailedTailFrame(
203
+ encoder: TextEncoder,
204
+ message: string,
205
+ refusalCode: string,
206
+ ): Uint8Array {
207
+ const payload = upstreamErrorFailedPayload(message, refusalCode);
208
+ return encoder.encode(
209
+ `\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`,
210
+ );
162
211
  }
163
212
 
164
213
  function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
@@ -168,12 +217,57 @@ function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
168
217
  return message ? redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS) : undefined;
169
218
  }
170
219
 
220
+ /**
221
+ * A bare upstream `error` event reduced to what the synthesized terminal needs.
222
+ *
223
+ * The structured code is authoritative whenever the upstream sent one: a code
224
+ * that is not a refusal means the upstream did not refuse, whatever its
225
+ * diagnostic text happens to quote. Recognized refusal copy is read only when
226
+ * no code was carried anywhere on the event, which is the shape #5176 reports.
227
+ * A refusal code with no message still yields a terminal, because Codex accepts
228
+ * that shape and supplies its own copy for it.
229
+ *
230
+ * The candidate topology mirrors {@link upstreamErrorMessageFromPayload}: code
231
+ * and message must be read from the same places, or an event whose message is
232
+ * nested under `response.error` would contribute text while its verdict went
233
+ * unseen.
234
+ */
235
+ function boundedBareUpstreamError(payload: unknown): {
236
+ message: string;
237
+ refusalCode: string | undefined;
238
+ } | undefined {
239
+ const root = asJsonRecord(payload);
240
+ if (!root || root.type !== "error") return undefined;
241
+ const message = boundedBareUpstreamErrorMessage(payload);
242
+ const response = asJsonRecord(root.response);
243
+ // Precedence is {@link upstreamErrorMessageFromPayload}'s, so the envelope
244
+ // that supplied the message also supplies the verdict. Taking the FIRST code
245
+ // rather than searching for a refusal is what stops a refusal nested below a
246
+ // transient one from overruling it.
247
+ const code = [
248
+ asJsonRecord(root.error),
249
+ asJsonRecord(root.last_error),
250
+ asJsonRecord(response?.error),
251
+ asJsonRecord(response?.incomplete_details),
252
+ root,
253
+ ]
254
+ .map(candidate => stringField(candidate, "code"))
255
+ .find(candidate => candidate !== undefined);
256
+ const refusalCode = code !== undefined
257
+ ? (isTerminalRefusalCode(code) ? code : undefined)
258
+ : message === undefined ? undefined : safetyRefusalCodeFromMessage(message);
259
+ if (message !== undefined) return { message, refusalCode };
260
+ if (refusalCode === undefined) return undefined;
261
+ return { message: terminalRefusalFallbackMessage(refusalCode), refusalCode };
262
+ }
263
+
171
264
  export type SseTerminalOutputBoundary = {
172
265
  feed(chunk: Uint8Array): Uint8Array;
173
266
  finish(): Uint8Array;
174
267
  terminalSeen(): boolean;
175
268
  doneSeen(): boolean;
176
269
  upstreamError(): string | undefined;
270
+ upstreamRefusalCode(): string | undefined;
177
271
  dispose(): void;
178
272
  };
179
273
 
@@ -196,11 +290,12 @@ export function createSseTerminalOutputBoundary(
196
290
  let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
197
291
  let disposed = false;
198
292
  let upstreamError: string | undefined;
293
+ let upstreamRefusalCode: string | undefined;
199
294
 
200
295
  const processFrames = (
201
296
  frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
202
297
  ): Uint8Array => {
203
- if (disposed || terminal || frames.length === 0) return new Uint8Array(0);
298
+ if (disposed || terminal || frames.length === 0) return EMPTY_BYTES;
204
299
  const output: Uint8Array[] = [];
205
300
  let responsesTerminal = false;
206
301
  for (const frame of frames) {
@@ -209,8 +304,11 @@ export function createSseTerminalOutputBoundary(
209
304
  const parsed = payload === null ? undefined : parseSsePayload(payload);
210
305
  // Observe on the client reader itself: a tee inspection branch may lag
211
306
  // behind EOF, so its log context cannot determine the outgoing terminal.
212
- const message = boundedBareUpstreamErrorMessage(parsed);
213
- if (message !== undefined) upstreamError = message;
307
+ const bare = boundedBareUpstreamError(parsed);
308
+ if (bare !== undefined) {
309
+ upstreamError = bare.message;
310
+ upstreamRefusalCode = bare.refusalCode;
311
+ }
214
312
  const safetyBuffering = dropSafetyBuffering && parsed !== undefined
215
313
  ? codexSafetyBufferingBlockAction(parsed) : "keep";
216
314
  if (safetyBuffering === "drop") continue;
@@ -259,13 +357,13 @@ export function createSseTerminalOutputBoundary(
259
357
 
260
358
  return {
261
359
  feed(chunk) {
262
- if (disposed || terminal) return new Uint8Array(0);
360
+ if (disposed || terminal) return EMPTY_BYTES;
263
361
  return processFrames(framer.feed(chunk));
264
362
  },
265
363
  finish() {
266
- if (disposed || terminal) return new Uint8Array(0);
364
+ if (disposed || terminal) return EMPTY_BYTES;
267
365
  const tail = framer.finish();
268
- if (tail.byteLength === 0) return new Uint8Array(0);
366
+ if (tail.byteLength === 0) return EMPTY_BYTES;
269
367
  // EOF may cut off the final SSE block before its blank-line delimiter.
270
368
  // Feed it through the exact same parser/rewrite/terminal path as a
271
369
  // complete frame, using a synthetic delimiter so the client receives a
@@ -277,6 +375,7 @@ export function createSseTerminalOutputBoundary(
277
375
  terminalSeen: () => terminal,
278
376
  doneSeen: () => done,
279
377
  upstreamError: () => upstreamError,
378
+ upstreamRefusalCode: () => upstreamRefusalCode,
280
379
  dispose() {
281
380
  if (disposed) return;
282
381
  disposed = true;
@@ -348,7 +447,11 @@ export function relaySseWithFailedTail(
348
447
  const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
349
448
  controller.enqueue(upstreamError === undefined
350
449
  ? adapterEofIncompleteFrame(encoder)
351
- : upstreamErrorTailFrame(encoder, upstreamError));
450
+ : upstreamErrorTailFrame(
451
+ encoder,
452
+ upstreamError,
453
+ terminalBoundary.upstreamRefusalCode(),
454
+ ));
352
455
  controller.enqueue(doneFrame(encoder));
353
456
  }
354
457
  terminalBoundary.dispose();
@@ -359,7 +462,7 @@ export function relaySseWithFailedTail(
359
462
  if (result !== "buffered") return;
360
463
  }
361
464
  } catch (err) {
362
- let partial: Uint8Array = new Uint8Array(0);
465
+ let partial: Uint8Array = EMPTY_BYTES;
363
466
  let tailTerminal = false;
364
467
  try {
365
468
  partial = terminalBoundary.finish();
@@ -377,7 +480,11 @@ export function relaySseWithFailedTail(
377
480
  if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
378
481
  } else {
379
482
  // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
380
- controller.enqueue(failedTailFrame(encoder, err));
483
+ const refusalCode = terminalBoundary.upstreamRefusalCode();
484
+ const refusalMessage = terminalBoundary.upstreamError();
485
+ controller.enqueue(refusalCode !== undefined && refusalMessage !== undefined
486
+ ? refusalFailedTailFrame(encoder, refusalMessage, refusalCode)
487
+ : failedTailFrame(encoder, err));
381
488
  }
382
489
  controller.close();
383
490
  } catch { /* client already torn down */ }
@@ -403,15 +510,7 @@ export function nextSseBlock(buffer: string): { block: string; delimiter: string
403
510
  };
404
511
  }
405
512
 
406
- export function sseDataPayload(block: string): string | null {
407
- const data: string[] = [];
408
- for (const line of block.split(/\r?\n/)) {
409
- if (!line.startsWith("data:")) continue;
410
- const value = line.slice(5);
411
- data.push(value.startsWith(" ") ? value.slice(1) : value);
412
- }
413
- return data.length > 0 ? data.join("\n") : null;
414
- }
513
+ export { sseDataPayload } from "./sse-payload-rewrite";
415
514
 
416
515
  type JsonRecord = Record<string, unknown>;
417
516
 
@@ -727,6 +826,9 @@ export function responseWithDeferredRequestLog(
727
826
  // convention for a client cancellation or upstream read failure.
728
827
  const status = reason === "cancel" ? 499 : reason === "read_error" ? 502 : response.status;
729
828
  addFinalRequestLog(requestId, start, logCtx, status, {
829
+ ...(reason === "eof" && logCtx.observedTerminalStatus
830
+ ? { terminalStatus: logCtx.observedTerminalStatus }
831
+ : {}),
730
832
  closeReason: reason === "cancel" ? "client_cancel" : "non_stream",
731
833
  }, addLog);
732
834
  },
@@ -943,8 +1045,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
943
1045
  let reported = false;
944
1046
  let sawTerminal = false;
945
1047
  let disposed = false;
946
- let delimiterTail: Uint8Array = new Uint8Array(0);
947
- let candidate: Uint8Array = new Uint8Array(0);
1048
+ let delimiterTail: Uint8Array = EMPTY_BYTES;
1049
+ let candidate: Uint8Array = EMPTY_BYTES;
948
1050
  let candidateBytes = 0;
949
1051
  let discardingOversizedFrame = false;
950
1052
  const reportFirstOutput = createFirstOutputReporter(handlers.onFirstOutput);
@@ -957,8 +1059,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
957
1059
  let firstResponseId: string | undefined;
958
1060
 
959
1061
  const clearFrameState = (): void => {
960
- delimiterTail = new Uint8Array(0);
961
- candidate = new Uint8Array(0);
1062
+ delimiterTail = EMPTY_BYTES;
1063
+ candidate = EMPTY_BYTES;
962
1064
  candidateBytes = 0;
963
1065
  discardingOversizedFrame = false;
964
1066
  };
@@ -995,9 +1097,9 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
995
1097
  };
996
1098
 
997
1099
  const takeCandidate = (): Uint8Array => {
998
- if (candidateBytes === 0) return new Uint8Array(0);
1100
+ if (candidateBytes === 0) return EMPTY_BYTES;
999
1101
  const frame = candidate.slice(0, candidateBytes);
1000
- candidate = new Uint8Array(0);
1102
+ candidate = EMPTY_BYTES;
1001
1103
  candidateBytes = 0;
1002
1104
  return frame;
1003
1105
  };
@@ -1010,7 +1112,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
1010
1112
  Math.min(nextBytes, MAX_INSPECTION_SSE_FRAME_BYTES),
1011
1113
  );
1012
1114
  if (nextBytes > MAX_INSPECTION_SSE_FRAME_BYTES) {
1013
- candidate = new Uint8Array(0);
1115
+ candidate = EMPTY_BYTES;
1014
1116
  candidateBytes = 0;
1015
1117
  discardingOversizedFrame = true;
1016
1118
  inspectionCounters.frameCapOverflows += 1;
@@ -1173,7 +1275,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
1173
1275
 
1174
1276
  const scanChunk = (chunk: Uint8Array): void => {
1175
1277
  const previousTail = delimiterTail;
1176
- delimiterTail = new Uint8Array(0);
1278
+ delimiterTail = EMPTY_BYTES;
1177
1279
  const tailLength = previousTail.byteLength;
1178
1280
  const totalLength = tailLength + chunk.byteLength;
1179
1281
  const byteAt = (index: number): number => index < tailLength
@@ -1219,7 +1321,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
1219
1321
  if (disposed) return;
1220
1322
  try {
1221
1323
  retainCandidateSlice(delimiterTail);
1222
- delimiterTail = new Uint8Array(0);
1324
+ delimiterTail = EMPTY_BYTES;
1223
1325
  if (!discardingOversizedFrame && candidateBytes > 0 && !reported) {
1224
1326
  const sourceBytes = candidateBytes;
1225
1327
  const decoded = decoder!.decode(takeCandidate());
@@ -12,6 +12,7 @@ import {
12
12
  isCyberPolicyCode,
13
13
  isCyberPolicyMessage,
14
14
  isRateLimitOrQuotaFailureMessage,
15
+ isUpstreamResetReplayRefusedMessage,
15
16
  upstreamErrorMessageFromPayload,
16
17
  } from "../lib/errors";
17
18
  import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
@@ -22,6 +23,7 @@ import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routi
22
23
  import type { AdapterRequest } from "../adapters/base";
23
24
  import type { RequestSpendSettlement } from "./responses/request-spend";
24
25
  import type { AdapterTierMetadata } from "../providers/fastwire";
26
+ import { UPSTREAM_RESET_REPLAY_REFUSED_CODE } from "../lib/upstream-retry";
25
27
  import { redactSecretString, sanitizeLogMetadataString } from "../lib/redact";
26
28
  import {
27
29
  appendUsageEntry,
@@ -44,6 +46,7 @@ import {
44
46
  usageStatusForFinalLog,
45
47
  usageTotalTokens,
46
48
  type AttemptRecoveryKind,
49
+ type AttemptRecoveryWithheld,
47
50
  type CacheTelemetryProvenance,
48
51
  type PersistedRequestSpend,
49
52
  type PersistedUsageAttempt,
@@ -66,10 +69,15 @@ import { inferCursorContextWindow } from "../adapters/cursor/discovery";
66
69
  import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
67
70
  import { DEVIN_MODEL_CONTEXT_WINDOWS } from "../adapters/devin/live-models";
68
71
  import { modelRecordValue } from "../reasoning-effort";
72
+ import type { RequestMetricsRecorder } from "./request-metrics";
69
73
 
70
74
  export interface RequestLogContext {
71
75
  model: string;
72
76
  provider: string;
77
+ /** Optional process-lifetime aggregate sink, injected by the server composition owner. */
78
+ requestMetricsRecorder?: RequestMetricsRecorder;
79
+ /** Bounded terminal enum observed while inspecting a buffered response body. */
80
+ observedTerminalStatus?: ResponsesTerminalStatus;
73
81
  /**
74
82
  * Identity of the ONE logical request this context serves (#4546). Set from the execution
75
83
  * budget minted at ingress; a retry leg, a repair refetch and a combo child share it.
@@ -290,6 +298,7 @@ export interface RequestLogEntry {
290
298
  }
291
299
 
292
300
  const requestLog: RequestLogEntry[] = [];
301
+ const requestLogObserversForTests = new Set<(entry: RequestLogEntry) => void>();
293
302
  const MAX_LOG_SIZE = 2000;
294
303
  const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
295
304
  let requestLogBytes = 0;
@@ -490,6 +499,9 @@ export function addRequestLog(entry: RequestLogEntry) {
490
499
  else if (retained !== entry) delete retained.claudeCompatibility;
491
500
  entry = retained;
492
501
  retainRequestLogEntry(entry);
502
+ for (const observer of requestLogObserversForTests) {
503
+ try { observer(entry); } catch { /* test observation must never fail request logging */ }
504
+ }
493
505
  try {
494
506
  // Failure diagnostics survive the 200-entry ring buffer by riding the persisted
495
507
  // usage entry (devlog/_plan/260716_claudecode_hardening/030). Success rows stay
@@ -565,6 +577,12 @@ export function addRequestLog(entry: RequestLogEntry) {
565
577
  }
566
578
  }
567
579
 
580
+ /** Test-only finalized-row observation without polling the management projection. */
581
+ export function observeRequestLogsForTests(observer: (entry: RequestLogEntry) => void): () => void {
582
+ requestLogObserversForTests.add(observer);
583
+ return () => { requestLogObserversForTests.delete(observer); };
584
+ }
585
+
568
586
  export function nextRequestLogId(_timestamp = Date.now()): string {
569
587
  return `ocx-${randomBytes(16).toString("hex")}`;
570
588
  }
@@ -713,7 +731,15 @@ export function requestLogErrorCode(
713
731
  }
714
732
  return "permission_denied";
715
733
  }
716
- if (status === 429) return "rate_limit_exceeded";
734
+ if (status === 429) {
735
+ // A refused ambiguous reset answers 429 by design (it must not invite a client
736
+ // retry that could duplicate inference); classify it by its message so the log
737
+ // distinguishes a proxy refusal from provider throttling.
738
+ if (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError)) {
739
+ return UPSTREAM_RESET_REPLAY_REFUSED_CODE;
740
+ }
741
+ return "rate_limit_exceeded";
742
+ }
717
743
  if (status === 503) return "server_is_overloaded";
718
744
  if (status >= 500) return "upstream_server_error";
719
745
  return `http_${status}`;
@@ -765,6 +791,22 @@ export function catalogModelSupportsServiceTier(modelId: string, serviceTier: st
765
791
 
766
792
  export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unknown): void {
767
793
  if (!payload || typeof payload !== "object") return;
794
+ if (logCtx.observedTerminalStatus === undefined) {
795
+ const eventType = (payload as { type?: unknown }).type;
796
+ const response = (payload as { response?: unknown }).response;
797
+ const responseStatus = response && typeof response === "object"
798
+ ? (response as { status?: unknown }).status
799
+ : (payload as { status?: unknown }).status;
800
+ if (eventType === "response.completed") {
801
+ logCtx.observedTerminalStatus = "completed";
802
+ } else if (eventType === "response.failed") {
803
+ logCtx.observedTerminalStatus = "failed";
804
+ } else if (eventType === "response.incomplete") {
805
+ logCtx.observedTerminalStatus = "incomplete";
806
+ } else if (responseStatus === "completed" || responseStatus === "failed" || responseStatus === "incomplete") {
807
+ logCtx.observedTerminalStatus = responseStatus;
808
+ }
809
+ }
768
810
  const source = "response" in payload && typeof (payload as { response?: unknown }).response === "object"
769
811
  ? (payload as { response?: unknown }).response
770
812
  : payload;
@@ -854,7 +896,13 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
854
896
  * Mark a refusal this proxy synthesized locally. Sets origin to `synthetic` and a
855
897
  * distinct local reason so the request log cannot be read as an upstream overload.
856
898
  */
857
- export function markLocalRequestLogRefusal(logCtx: RequestLogContext, reason: string): void {
899
+ // Typed by the two fields it writes rather than by the whole context: the durable-spend tracker
900
+ // has to mark a row from a narrow view of it, and widening that view to the full context there
901
+ // would pull the entire log shape into a module that touches two of its fields.
902
+ export function markLocalRequestLogRefusal(
903
+ logCtx: Pick<RequestLogContext, "localTerminalReason" | "terminalSource">,
904
+ reason: string,
905
+ ): void {
858
906
  logCtx.localTerminalReason = reason;
859
907
  logCtx.terminalSource = "synthetic";
860
908
  }
@@ -1300,6 +1348,7 @@ export function addFinalRequestLog(
1300
1348
  const attempts = logCtx.attempts?.map(attempt => ({
1301
1349
  ...attempt,
1302
1350
  recoveryKinds: [...attempt.recoveryKinds],
1351
+ ...(attempt.recoveryWithheld?.length ? { recoveryWithheld: [...attempt.recoveryWithheld] } : {}),
1303
1352
  ...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
1304
1353
  ...(attempt.tierOutcome ? { tierOutcome: { ...attempt.tierOutcome } } : {}),
1305
1354
  }));
@@ -1309,6 +1358,17 @@ export function addFinalRequestLog(
1309
1358
  const usageStatus = aggregate?.status ?? existing.status;
1310
1359
  const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
1311
1360
  const spend = requestSpendRecord(logCtx, attempts);
1361
+ const durationMs = Date.now() - start;
1362
+ logCtx.requestMetricsRecorder?.recordFinalRequest({
1363
+ ...(logCtx.inboundProtocol ? { protocol: logCtx.inboundProtocol } : {}),
1364
+ status: effectiveStatus,
1365
+ durationMs,
1366
+ ...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
1367
+ ...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
1368
+ ...(closeReason ? { closeReason } : {}),
1369
+ ...(attempts !== undefined ? { attempts } : {}),
1370
+ ...(spend ? { spendSends: spend.sends } : {}),
1371
+ });
1312
1372
  const cacheProvenance = classifyCacheTelemetryProvenance(loggedUsage, {
1313
1373
  wireParsed: logCtx.usageWireParsed === true,
1314
1374
  });
@@ -1356,7 +1416,7 @@ export function addFinalRequestLog(
1356
1416
  : {}),
1357
1417
  ...(logCtx.resolvedModel ? { resolvedModel: logCtx.resolvedModel } : {}),
1358
1418
  status: effectiveStatus,
1359
- durationMs: Date.now() - start,
1419
+ durationMs,
1360
1420
  ...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
1361
1421
  ...(errorCode ? { errorCode } : {}),
1362
1422
  ...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
@@ -1703,6 +1763,22 @@ export function noteAttemptSend(
1703
1763
  }
1704
1764
  }
1705
1765
 
1766
+ /**
1767
+ * Record that a recovery this attempt was eligible for did not happen.
1768
+ *
1769
+ * Deliberately NOT `noteAttemptSend`: nothing was sent, so `sendCount` must not move. The two
1770
+ * together are what make a one-send log readable — no kind and no withheld reason means nothing
1771
+ * was eligible, a withheld reason means something was and the budget refused it (#5044).
1772
+ */
1773
+ export function noteAttemptRecoveryWithheld(
1774
+ attempt: PersistedUsageAttempt | undefined,
1775
+ reason: AttemptRecoveryWithheld,
1776
+ ): void {
1777
+ if (!attempt) return;
1778
+ if (!attempt.recoveryWithheld) attempt.recoveryWithheld = [];
1779
+ if (!attempt.recoveryWithheld.includes(reason)) attempt.recoveryWithheld.push(reason);
1780
+ }
1781
+
1706
1782
  export function finishRequestAttempt(
1707
1783
  attempt: PersistedUsageAttempt,
1708
1784
  status: number,
@@ -1785,4 +1861,5 @@ export function clearRequestLogsForTests(): void {
1785
1861
  requestLog.length = 0;
1786
1862
  requestLogBytes = 0;
1787
1863
  requestLogsHydratedFromDisk = false;
1864
+ requestLogObserversForTests.clear();
1788
1865
  }