@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
@@ -0,0 +1,236 @@
1
+ import type { ResponsesTerminalStatus } from "../bridge";
2
+ import type { AttemptRecoveryKind } from "../usage/log";
3
+
4
+ export const REQUEST_METRICS_PROTOCOLS = Object.freeze(["responses", "chat", "messages", "unknown"] as const);
5
+ export const REQUEST_METRICS_RESULTS = Object.freeze(["completed", "failed", "incomplete", "aborted"] as const);
6
+ export const REQUEST_METRICS_RECOVERY_CLASSES = Object.freeze([
7
+ "transient",
8
+ "connection",
9
+ "credential",
10
+ "rate_limit",
11
+ "payload",
12
+ "empty_completion",
13
+ "effort_downgrade",
14
+ "other",
15
+ ] as const);
16
+
17
+ export const REQUEST_DURATION_BUCKETS_SECONDS = Object.freeze([0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60] as const);
18
+ export const REQUEST_TTFT_BUCKETS_SECONDS = Object.freeze([0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30] as const);
19
+
20
+ export type RequestMetricsProtocol = typeof REQUEST_METRICS_PROTOCOLS[number];
21
+ export type RequestMetricsResult = typeof REQUEST_METRICS_RESULTS[number];
22
+ export type RequestMetricsRecoveryClass = typeof REQUEST_METRICS_RECOVERY_CLASSES[number];
23
+
24
+ export interface RequestMetricFinalFact {
25
+ protocol?: "responses" | "chat" | "messages";
26
+ status: number;
27
+ durationMs: number;
28
+ firstOutputMs?: number;
29
+ terminalStatus?: ResponsesTerminalStatus;
30
+ closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
31
+ attempts?: ReadonlyArray<{
32
+ sendCount: number;
33
+ recoveryKinds: readonly AttemptRecoveryKind[];
34
+ }>;
35
+ spendSends?: number;
36
+ }
37
+
38
+ export interface RequestMetricsRecorder {
39
+ recordFinalRequest(fact: RequestMetricFinalFact): void;
40
+ }
41
+
42
+ export interface RequestMetricsSnapshotter {
43
+ snapshot(): string;
44
+ }
45
+
46
+ export interface RequestMetricsOwner extends RequestMetricsRecorder, RequestMetricsSnapshotter {
47
+ resetForTests(): void;
48
+ }
49
+
50
+ interface HistogramCell {
51
+ buckets: number[];
52
+ count: number;
53
+ sum: number;
54
+ }
55
+
56
+ const protocolCell = (value: RequestMetricsProtocol): number => REQUEST_METRICS_PROTOCOLS.indexOf(value);
57
+ const resultCell = (value: RequestMetricsResult): number => REQUEST_METRICS_RESULTS.indexOf(value);
58
+ const recoveryCell = (value: RequestMetricsRecoveryClass): number => REQUEST_METRICS_RECOVERY_CLASSES.indexOf(value);
59
+
60
+ function matrix(rows: number, columns: number): number[][] {
61
+ return Array.from({ length: rows }, () => Array.from({ length: columns }, () => 0));
62
+ }
63
+
64
+ function histograms(bounds: readonly number[]): HistogramCell[][] {
65
+ return Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => (
66
+ Array.from({ length: REQUEST_METRICS_RESULTS.length }, () => ({
67
+ buckets: Array.from({ length: bounds.length + 1 }, () => 0),
68
+ count: 0,
69
+ sum: 0,
70
+ }))
71
+ ));
72
+ }
73
+
74
+ function classifyResult(fact: RequestMetricFinalFact): RequestMetricsResult {
75
+ if (fact.closeReason === "client_cancel" || fact.status === 499) return "aborted";
76
+ if (fact.terminalStatus === "failed") return "failed";
77
+ if (fact.terminalStatus === "incomplete"
78
+ || fact.closeReason === "body_stall"
79
+ || fact.closeReason === "body_overflow") return "incomplete";
80
+ if (fact.terminalStatus === "completed") return "completed";
81
+ if (fact.terminalStatus === undefined
82
+ && (fact.status === 101 || (fact.status >= 200 && fact.status < 400))) return "completed";
83
+ return "failed";
84
+ }
85
+
86
+ function recoveryClass(kind: AttemptRecoveryKind): RequestMetricsRecoveryClass {
87
+ switch (kind) {
88
+ case "transient-5xx": return "transient";
89
+ case "connection-reset": return "connection";
90
+ case "oauth-401":
91
+ case "key-401": return "credential";
92
+ case "key-429":
93
+ case "rate-limit-429":
94
+ case "anthropic-oauth-429":
95
+ case "oauth-account-429": return "rate_limit";
96
+ case "image-413":
97
+ case "console-go-upload-retry":
98
+ case "opaque-blob-rejection": return "payload";
99
+ case "empty-completion": return "empty_completion";
100
+ case "reasoning-effort-downgrade": return "effort_downgrade";
101
+ default: return "other";
102
+ }
103
+ }
104
+
105
+ function observeHistogram(cell: HistogramCell, bounds: readonly number[], value: number): void {
106
+ if (!Number.isFinite(value) || value < 0) return;
107
+ cell.count += 1;
108
+ cell.sum += value;
109
+ for (let index = 0; index < bounds.length; index += 1) {
110
+ if (value <= bounds[index]!) cell.buckets[index]! += 1;
111
+ }
112
+ cell.buckets[bounds.length]! += 1;
113
+ }
114
+
115
+ function sampleLabels(protocol: RequestMetricsProtocol, result?: RequestMetricsResult): string {
116
+ return result === undefined
117
+ ? `{protocol="${protocol}"}`
118
+ : `{protocol="${protocol}",result="${result}"}`;
119
+ }
120
+
121
+ function appendHistogram(
122
+ lines: string[],
123
+ name: string,
124
+ help: string,
125
+ values: HistogramCell[][],
126
+ bounds: readonly number[],
127
+ ): void {
128
+ lines.push(`# HELP ${name} ${help}`, `# TYPE ${name} histogram`);
129
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
130
+ for (const result of REQUEST_METRICS_RESULTS) {
131
+ const cell = values[protocolCell(protocol)]![resultCell(result)]!;
132
+ for (let index = 0; index < bounds.length; index += 1) {
133
+ lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="${bounds[index]}"} ${cell.buckets[index]}`);
134
+ }
135
+ lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="+Inf"} ${cell.buckets[bounds.length]}`);
136
+ lines.push(`${name}_sum${sampleLabels(protocol, result)} ${cell.sum}`);
137
+ lines.push(`${name}_count${sampleLabels(protocol, result)} ${cell.count}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ export function createRequestMetricsOwner(
143
+ processStartTimeSeconds = Date.now() / 1000,
144
+ ): RequestMetricsOwner {
145
+ let logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
146
+ let physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
147
+ let recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
148
+ let durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
149
+ let ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
150
+ let missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
151
+
152
+ return {
153
+ recordFinalRequest(fact): void {
154
+ const protocol: RequestMetricsProtocol = fact.protocol ?? "unknown";
155
+ const result = classifyResult(fact);
156
+ const protocolIndex = protocolCell(protocol);
157
+ const resultIndex = resultCell(result);
158
+ logicalRequests[protocolIndex]![resultIndex]! += 1;
159
+
160
+ const attempts = fact.attempts;
161
+ const sends = attempts === undefined
162
+ ? (Number.isInteger(fact.spendSends) && fact.spendSends! >= 0 ? fact.spendSends! : 0)
163
+ : attempts.reduce((total, attempt) => (
164
+ Number.isInteger(attempt.sendCount) && attempt.sendCount >= 0 ? total + attempt.sendCount : total
165
+ ), 0);
166
+ physicalSends[protocolIndex]! += sends;
167
+
168
+ for (const attempt of attempts ?? []) {
169
+ for (const kind of new Set(attempt.recoveryKinds)) {
170
+ recoveries[protocolIndex]![recoveryCell(recoveryClass(kind))]! += 1;
171
+ }
172
+ }
173
+
174
+ observeHistogram(durations[protocolIndex]![resultIndex]!, REQUEST_DURATION_BUCKETS_SECONDS, fact.durationMs / 1000);
175
+ if (typeof fact.firstOutputMs === "number" && Number.isFinite(fact.firstOutputMs) && fact.firstOutputMs >= 0) {
176
+ observeHistogram(ttft[protocolIndex]![resultIndex]!, REQUEST_TTFT_BUCKETS_SECONDS, fact.firstOutputMs / 1000);
177
+ } else {
178
+ missingTtft[protocolIndex]![resultIndex]! += 1;
179
+ }
180
+ },
181
+
182
+ snapshot(): string {
183
+ const lines: string[] = [
184
+ "# HELP opencodex_logical_requests_total Finalized logical requests in this process.",
185
+ "# TYPE opencodex_logical_requests_total counter",
186
+ ];
187
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
188
+ for (const result of REQUEST_METRICS_RESULTS) {
189
+ lines.push(`opencodex_logical_requests_total${sampleLabels(protocol, result)} ${logicalRequests[protocolCell(protocol)]![resultCell(result)]}`);
190
+ }
191
+ }
192
+ lines.push(
193
+ "# HELP opencodex_physical_sends_total Upstream sends made by finalized logical requests in this process.",
194
+ "# TYPE opencodex_physical_sends_total counter",
195
+ );
196
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
197
+ lines.push(`opencodex_physical_sends_total${sampleLabels(protocol)} ${physicalSends[protocolCell(protocol)]}`);
198
+ }
199
+ lines.push(
200
+ "# HELP opencodex_recoveries_total Distinct recovery kinds observed per physical attempt in this process.",
201
+ "# TYPE opencodex_recoveries_total counter",
202
+ );
203
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
204
+ for (const recovery of REQUEST_METRICS_RECOVERY_CLASSES) {
205
+ lines.push(`opencodex_recoveries_total{protocol="${protocol}",recovery="${recovery}"} ${recoveries[protocolCell(protocol)]![recoveryCell(recovery)]}`);
206
+ }
207
+ }
208
+ appendHistogram(lines, "opencodex_request_duration_seconds", "Finalized logical request duration in seconds.", durations, REQUEST_DURATION_BUCKETS_SECONDS);
209
+ appendHistogram(lines, "opencodex_ttft_seconds", "Observed time to first output in seconds.", ttft, REQUEST_TTFT_BUCKETS_SECONDS);
210
+ lines.push(
211
+ "# HELP opencodex_ttft_missing_total Finalized logical requests without an observed time to first output.",
212
+ "# TYPE opencodex_ttft_missing_total counter",
213
+ );
214
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
215
+ for (const result of REQUEST_METRICS_RESULTS) {
216
+ lines.push(`opencodex_ttft_missing_total${sampleLabels(protocol, result)} ${missingTtft[protocolCell(protocol)]![resultCell(result)]}`);
217
+ }
218
+ }
219
+ lines.push(
220
+ "# HELP opencodex_metrics_process_start_time_seconds Unix time when this process metrics owner started.",
221
+ "# TYPE opencodex_metrics_process_start_time_seconds gauge",
222
+ `opencodex_metrics_process_start_time_seconds ${processStartTimeSeconds}`,
223
+ );
224
+ return `${lines.join("\n")}\n`;
225
+ },
226
+
227
+ resetForTests(): void {
228
+ logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
229
+ physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
230
+ recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
231
+ durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
232
+ ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
233
+ missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
234
+ },
235
+ };
236
+ }
@@ -13,6 +13,7 @@ import {
13
13
  sealRequestAttemptIdentity,
14
14
  recordAttemptCredentialSource,
15
15
  } from "../request-log";
16
+ import { noteAttemptRecoveryWithheld } from "../request-log";
16
17
  import { waitForProviderRequestSlot } from "../../providers/request-pacing";
17
18
  import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
18
19
  import {
@@ -46,7 +47,12 @@ import {
46
47
  import { shouldAttemptImageTierRetry } from "../image-retry";
47
48
  import { readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
48
49
  import { isCyberPolicyCode, CYBER_POLICY_FALLBACK_MESSAGE, CYBER_POLICY_ERROR_CODE } from "../../lib/errors";
49
- import { cancelBodyOnAbort } from "../../lib/abort";
50
+ import {
51
+ readResponseBodyWithInactivity,
52
+ readResponseStreamWithInactivity,
53
+ ResponseBodyInactivityError,
54
+ } from "../../lib/response-body-inactivity";
55
+ import { resolveStallTimeoutSec } from "../../stall-timeout";
50
56
  import { guardTerminalEventStream } from "./terminal-guard";
51
57
 
52
58
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
@@ -78,6 +84,7 @@ export function createAdapterContinuations(
78
84
  | "genericFailoverAccountId"
79
85
  | "genericFailovers"
80
86
  | "applyFailoverSnapshot"
87
+ | "replayOAuthCredentialSnapshot"
81
88
  | "noteRoutedAttemptSend"
82
89
  >,
83
90
  sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
@@ -85,6 +92,7 @@ export function createAdapterContinuations(
85
92
  ResponsesSendBudget,
86
93
  | "adapterDispatchBudget"
87
94
  | "noteAdapterPhysicalSend"
95
+ | "noteAdapterRecoveryWithheld"
88
96
  | "remainingTransientSendBudget"
89
97
  | "noteTransientSends"
90
98
  | "reserveCredentialHop"
@@ -112,9 +120,11 @@ export function createAdapterContinuations(
112
120
  const { route, translatorBudget, inboundWire, parsed } = requestState;
113
121
  const { routedCompaction } = sidecarState;
114
122
  const { upstream, connectMs, rateLimitPolicy, stallTimeoutMs } = adapterExchange;
123
+ const bodyInactivityMs = resolveStallTimeoutSec(config.stallTimeoutSec) * 1000;
115
124
  const {
116
125
  adapterDispatchBudget,
117
126
  noteAdapterPhysicalSend,
127
+ noteAdapterRecoveryWithheld,
118
128
  remainingTransientSendBudget,
119
129
  noteTransientSends,
120
130
  reserveCredentialHop,
@@ -193,6 +203,7 @@ export function createAdapterContinuations(
193
203
  timeoutMs: connectMs,
194
204
  sendBudget: adapterDispatchBudget,
195
205
  onPhysicalSend: send => noteAdapterPhysicalSend(continuationEstimate, send),
206
+ onRecoveryWithheld: noteAdapterRecoveryWithheld,
196
207
  stream: nextParsed.stream,
197
208
  executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
198
209
  pacingSlotAcquired: true,
@@ -263,12 +274,12 @@ export function createAdapterContinuations(
263
274
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) before key/account failover:
264
275
  // a primary-key rate-limit blip replays on the SAME key, matching the main recovery
265
276
  // loop; only after the attempts are exhausted does the continuation fail over.
266
- while (
267
- response.status === 429
277
+ while (
278
+ response.status === 429
268
279
  // A synthesized replay refusal is not a rate limit; replaying the continuation on
269
280
  // it would re-send a turn whose first send may already have been processed.
270
281
  && !isNonReplayableResponse(response)
271
- && rateLimitPolicy !== null
282
+ && rateLimitPolicy !== null
272
283
  && adapterExchange.rateLimitRetries < rateLimitPolicy.attempts
273
284
  // The main recovery loop and the passthrough ladder both consult the shared remainder
274
285
  // here; this loop did not, so a request whose budget was already spent could still
@@ -327,7 +338,7 @@ export function createAdapterContinuations(
327
338
  route.provider = rotated;
328
339
  invalidateSameTargetRequest();
329
340
  transportState.activeAdapter = resolveSelectionAdapter(
330
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
341
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
331
342
  config.cacheRetention,
332
343
  );
333
344
  bindRouteReasoningReplayScope({
@@ -348,11 +359,11 @@ export function createAdapterContinuations(
348
359
  continue;
349
360
  }
350
361
  }
351
- if (
352
- response.status === 429
362
+ if (
363
+ response.status === 429
364
+ && transportState.anthropicPoolAccountId
353
365
  && !isNonReplayableResponse(response)
354
- && transportState.anthropicPoolAccountId
355
- && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
366
+ && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
356
367
  ) {
357
368
  const nextAccountId = rotateAnthropicAccountOn429(
358
369
  config,
@@ -373,7 +384,7 @@ export function createAdapterContinuations(
373
384
  invalidateSameTargetRequest();
374
385
  logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
375
386
  transportState.activeAdapter = resolveSelectionAdapter(
376
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
387
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
377
388
  config.cacheRetention,
378
389
  );
379
390
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
@@ -390,11 +401,11 @@ export function createAdapterContinuations(
390
401
  // 429 stayed terminal even with failover fully active -- the same class of divergence the
391
402
  // two sidecars already produced once. Request-local state is shared with the other arms so
392
403
  // the per-request bound cannot be silently re-armed by reaching a different loop.
393
- if (
394
- response.status === 429
404
+ if (
405
+ response.status === 429
406
+ && transportState.genericFailoverAccountId
395
407
  && !isNonReplayableResponse(response)
396
- && transportState.genericFailoverAccountId
397
- && transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
408
+ && transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
398
409
  && isGenericOAuthFailoverEnabled(config, route.providerName)
399
410
  ) {
400
411
  // Intersection with the shared request budget. The continuation loop re-sends the
@@ -416,8 +427,13 @@ export function createAdapterContinuations(
416
427
  route.providerName,
417
428
  transportState.genericFailoverAccountId,
418
429
  response.headers.get("retry-after"),
430
+ Date.now(),
431
+ route.modelId,
419
432
  )
420
433
  : null;
434
+ // Eligible and refused by the shared budget, as opposed to eligible and finding no next
435
+ // account: the two produce the same response and need different follow-ups (#5044).
436
+ if (!hop.allowed) noteAttemptRecoveryWithheld(logCtx.activeAttempt, "rotation-send-budget");
421
437
  if (!nextAccountId) hop.permit?.release();
422
438
  if (nextAccountId) {
423
439
  try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
@@ -433,9 +449,25 @@ export function createAdapterContinuations(
433
449
  if (applied) {
434
450
  invalidateSameTargetRequest();
435
451
  transportState.activeAdapter = resolveSelectionAdapter(
436
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
452
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
437
453
  config.cacheRetention,
438
454
  );
455
+ bindRouteReasoningReplayScope({
456
+ parsed: nextParsed,
457
+ providerName: route.providerName,
458
+ provider: route.provider,
459
+ adapterName: transportState.activeAdapter.name,
460
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
461
+ });
462
+ // Response persistence closes over the outer parsed request; keep its owner binding in
463
+ // sync with the terminal-guard clone that builds the rotated continuation request.
464
+ bindRouteReasoningReplayScope({
465
+ parsed,
466
+ providerName: route.providerName,
467
+ provider: route.provider,
468
+ adapterName: transportState.activeAdapter.name,
469
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
470
+ });
439
471
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
440
472
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
441
473
  // The replay goes out on the next iteration. An adapter that owns its ladder
@@ -493,23 +525,35 @@ export function createAdapterContinuations(
493
525
  }
494
526
 
495
527
  try {
496
- // Protect the continuation body against a client abort landing between fetch resolution and
497
- // reader attach, exactly as the initial response is guarded above (#390/366e3053). Without
498
- // this, a client cancel during the continuation reopens the Bun fetch-to-reader abort race.
499
- const detachContinuationBodyGuard = cancelBodyOnAbort(response.body, upstream.signal);
500
- try {
501
- if (nextParsed.stream) {
502
- yield* transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata);
503
- } else if (transportState.activeAdapter.parseResponse) {
504
- yield* await transportState.activeAdapter.parseResponse(response, translatorBudget, logCtx.activeTierMetadata);
505
- } else {
506
- yield { type: "error", message: "Provider continuation does not support response parsing" };
507
- }
508
- } finally {
509
- detachContinuationBodyGuard();
528
+ // Each successful continuation owns a fresh pending-read deadline and an abort
529
+ // listener that can cancel its reader even while the parser holds the body lock.
530
+ // The shared signal is not aborted on timeout: this generator must still emit
531
+ // the error that the already-live Responses bridge turns into a terminal.
532
+ if (nextParsed.stream) {
533
+ yield* readResponseStreamWithInactivity(
534
+ response,
535
+ upstream.signal,
536
+ bodyInactivityMs,
537
+ guarded => transportState.activeAdapter.parseStream(guarded, translatorBudget, logCtx.activeTierMetadata),
538
+ );
539
+ } else if (transportState.activeAdapter.parseResponse) {
540
+ yield* await readResponseBodyWithInactivity(
541
+ response,
542
+ upstream.signal,
543
+ bodyInactivityMs,
544
+ guarded => transportState.activeAdapter.parseResponse!(guarded, translatorBudget, logCtx.activeTierMetadata),
545
+ );
546
+ } else {
547
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
548
+ yield { type: "error", message: "Provider continuation does not support response parsing" };
510
549
  }
511
550
  } catch (error) {
512
- if (options.abortSignal?.aborted) {
551
+ // Classify the thrown error before reading signal state: cancelling the stalled
552
+ // source can synchronously abort the shared signal, which would otherwise report
553
+ // this timeout as a client cancellation.
554
+ if (error instanceof ResponseBodyInactivityError) {
555
+ yield { type: "error", message: "Provider continuation response body stalled before completing", status: 504 };
556
+ } else if (options.abortSignal?.aborted || upstream.signal.aborted) {
513
557
  yield { type: "error", message: "client closed request during terminal continuation", status: 499 };
514
558
  } else {
515
559
  yield { type: "error", message: `Provider continuation parse failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}` };
@@ -14,6 +14,12 @@ import { rememberResponseState } from "../../responses/state";
14
14
  import { trackStreamLifetime } from "../lifecycle";
15
15
  import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
16
16
  import { adapterResponseReachedServingTerminal } from "./core-replay";
17
+ import {
18
+ readResponseBodyWithInactivity,
19
+ readResponseStreamWithInactivity,
20
+ ResponseBodyInactivityError,
21
+ } from "../../lib/response-body-inactivity";
22
+ import { resolveStallTimeoutSec } from "../../stall-timeout";
17
23
 
18
24
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
19
25
  export async function deliverAdapterResponse(
@@ -61,14 +67,33 @@ export async function deliverAdapterResponse(
61
67
  notifyResponseComplete,
62
68
  } = responseEffects;
63
69
  const { routedCompaction } = sidecarState;
70
+ const bodyInactivityMs = resolveStallTimeoutSec(config.stallTimeoutSec) * 1000;
64
71
 
65
72
 
66
73
  if (parsed.stream) {
67
- const initialEventStream = transportState.activeAdapter.parseStream(
68
- upstreamResponse,
69
- translatorBudget,
70
- logCtx.activeTierMetadata,
71
- );
74
+ // The continuation legs classify a stalled body themselves; the initial stream needs the
75
+ // same mapping or the bridge catch reports this upstream timeout as a 500 proxy_error.
76
+ const initialEventStream = (async function* (): AsyncGenerator<AdapterEvent> {
77
+ try {
78
+ yield* readResponseStreamWithInactivity(
79
+ upstreamResponse,
80
+ upstream.signal,
81
+ bodyInactivityMs,
82
+ response => transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata),
83
+ );
84
+ } catch (error) {
85
+ if (error instanceof ResponseBodyInactivityError) {
86
+ yield {
87
+ type: "error",
88
+ message: "Upstream response body stalled before completing",
89
+ status: 504,
90
+ errorType: "upstream_error",
91
+ };
92
+ return;
93
+ }
94
+ throw error;
95
+ }
96
+ })();
72
97
  const eventStream = terminalGuardEnabled
73
98
  ? guardTerminalEventStream({
74
99
  parsed,
@@ -137,10 +162,11 @@ export async function deliverAdapterResponse(
137
162
  if (transportState.activeAdapter.parseResponse) {
138
163
  let events: AdapterEvent[];
139
164
  try {
140
- const initialEvents = await transportState.activeAdapter.parseResponse(
165
+ const initialEvents = await readResponseBodyWithInactivity(
141
166
  upstreamResponse,
142
- translatorBudget,
143
- logCtx.activeTierMetadata,
167
+ upstream.signal,
168
+ bodyInactivityMs,
169
+ response => transportState.activeAdapter.parseResponse!(response, translatorBudget, logCtx.activeTierMetadata),
144
170
  );
145
171
  let guardedEvents: AdapterEvent[];
146
172
  if (terminalGuardEnabled) {
@@ -164,6 +190,11 @@ export async function deliverAdapterResponse(
164
190
  } else {
165
191
  events = guardedEvents;
166
192
  }
193
+ } catch (error) {
194
+ if (error instanceof ResponseBodyInactivityError) {
195
+ return formatErrorResponse(504, "upstream_error", "Upstream response body stalled before completing");
196
+ }
197
+ throw error;
167
198
  } finally {
168
199
  cleanupUpstreamAbort();
169
200
  }