@bitkyc08/opencodex 2.88.0 → 2.90.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 (291) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-DgxkE6Md.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
  7. package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +2 -2
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic.ts +10 -3
  13. package/src/adapters/base.ts +4 -0
  14. package/src/adapters/claude-cli/adapter.ts +13 -7
  15. package/src/adapters/claude-cli/usage-admission.ts +14 -2
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/coding-agent/turn.ts +14 -4
  18. package/src/adapters/cursor/protobuf-request.ts +3 -0
  19. package/src/adapters/google-http.ts +3 -2
  20. package/src/adapters/google-progress-history.ts +19 -1
  21. package/src/adapters/google-vertex-endpoint.ts +67 -0
  22. package/src/adapters/google.ts +62 -37
  23. package/src/adapters/image.ts +5 -3
  24. package/src/adapters/input-media-guard.ts +5 -2
  25. package/src/adapters/kiro-refusal.ts +41 -1
  26. package/src/adapters/ollama-native.ts +1 -0
  27. package/src/adapters/openai-chat/messages.ts +34 -4
  28. package/src/adapters/openai-chat/passthrough.ts +37 -4
  29. package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
  30. package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
  31. package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
  32. package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
  33. package/src/adapters/openai-chat.ts +7 -6
  34. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  35. package/src/adapters/openai-responses/opengateway-input.ts +139 -0
  36. package/src/adapters/openai-responses/passthrough.ts +22 -13
  37. package/src/adapters/openai-responses/reasoning.ts +68 -1
  38. package/src/adapters/openai-responses/transport.ts +13 -1
  39. package/src/adapters/registry.ts +1 -1
  40. package/src/adapters/upstream-http-error.ts +3 -2
  41. package/src/adapters/zed.ts +7 -4
  42. package/src/bridge/internal.ts +11 -1
  43. package/src/bridge/response-json.ts +2 -2
  44. package/src/bridge/sse.ts +10 -4
  45. package/src/claude/desktop-3p-library.ts +43 -3
  46. package/src/claude/desktop-3p.ts +20 -10
  47. package/src/claude/desktop-picker-profile.ts +18 -9
  48. package/src/claude/desktop-remote-store-io.ts +2 -2
  49. package/src/claude/inbound.ts +12 -26
  50. package/src/claude/outbound.ts +5 -0
  51. package/src/cli/account-api.ts +13 -1
  52. package/src/cli/account-extended.ts +17 -5
  53. package/src/cli/account-key-quota.ts +4 -1
  54. package/src/cli/account.ts +1 -1
  55. package/src/cli/capabilities-agents-routing.ts +45 -8
  56. package/src/cli/capabilities-base.ts +5 -5
  57. package/src/cli/capabilities-provider-models.ts +5 -4
  58. package/src/cli/combo-result.ts +8 -2
  59. package/src/cli/combo.ts +17 -0
  60. package/src/cli/ensure-desktop-cold-start.ts +2 -2
  61. package/src/cli/help.ts +1 -1
  62. package/src/cli/message-command.ts +4 -1
  63. package/src/cli/message-remote-args.ts +69 -0
  64. package/src/cli/message-remote-command.ts +77 -0
  65. package/src/cli/provider-runtime.ts +69 -1
  66. package/src/cli/registry.ts +3 -3
  67. package/src/codex/account-lifecycle.ts +19 -6
  68. package/src/codex/account-pause.ts +6 -1
  69. package/src/codex/account-runtime-state.ts +5 -0
  70. package/src/codex/account-store.ts +55 -9
  71. package/src/codex/account-usability.ts +4 -1
  72. package/src/codex/auth-api/account-list.ts +13 -5
  73. package/src/codex/auth-api/account-pause-group.ts +2 -22
  74. package/src/codex/auth-api/login-collision.ts +59 -0
  75. package/src/codex/auth-api/login-flow.ts +12 -7
  76. package/src/codex/auth-api/main-account-probe.ts +3 -2
  77. package/src/codex/auth-api/pool-mode-gate.ts +81 -13
  78. package/src/codex/auth-collision.ts +31 -4
  79. package/src/codex/auth-context.ts +57 -10
  80. package/src/codex/catalog/aggregation.ts +15 -0
  81. package/src/codex/catalog/auto-review.ts +2 -1
  82. package/src/codex/catalog/build-entries.ts +17 -7
  83. package/src/codex/catalog/derive-entry.ts +3 -1
  84. package/src/codex/catalog/metadata.ts +2 -0
  85. package/src/codex/catalog/model-visibility.ts +0 -2
  86. package/src/codex/catalog/ownership.ts +71 -0
  87. package/src/codex/catalog/parsing.ts +12 -3
  88. package/src/codex/catalog/retained-sync.ts +2 -1
  89. package/src/codex/catalog/routed-removal.ts +4 -4
  90. package/src/codex/cli-update-plan.ts +18 -3
  91. package/src/codex/collision-identity.ts +78 -0
  92. package/src/codex/convergence.ts +2 -1
  93. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  94. package/src/codex/low-quota-protection.ts +57 -31
  95. package/src/codex/main-account-cache.ts +77 -3
  96. package/src/codex/main-account-hard-lock.ts +14 -0
  97. package/src/codex/main-account.ts +6 -2
  98. package/src/codex/native-residue.ts +2 -3
  99. package/src/codex/quota.ts +26 -2
  100. package/src/codex/routing/cooldown-math.ts +5 -0
  101. package/src/codex/routing/failure-window.ts +61 -9
  102. package/src/codex/routing.ts +4 -3
  103. package/src/codex/shim-diagnostics.ts +2 -1
  104. package/src/codex/shim-fingerprint.ts +11 -2
  105. package/src/codex/shim-templates.ts +16 -5
  106. package/src/codex/shim-windows-refresh.ts +224 -0
  107. package/src/codex/shim.ts +45 -21
  108. package/src/combos/failover.ts +10 -2
  109. package/src/combos/jev-model-backend.ts +30 -7
  110. package/src/combos/jev-model-request.ts +20 -0
  111. package/src/combos/jev-quota-activation.ts +153 -0
  112. package/src/combos/jev-quota-config.ts +62 -0
  113. package/src/combos/jev-quota-route.ts +26 -0
  114. package/src/combos/jev-quota.ts +88 -0
  115. package/src/combos/jev.ts +25 -12
  116. package/src/combos/types.ts +27 -0
  117. package/src/compatibility/index.ts +3 -0
  118. package/src/compatibility/openai-responses.ts +8 -1
  119. package/src/compatibility/opengateway-responses.ts +61 -0
  120. package/src/config/account-auto-pause-revisions.ts +136 -0
  121. package/src/config/diagnostics.ts +13 -0
  122. package/src/config/live-reconcile.ts +9 -0
  123. package/src/config/persist-unlocked.ts +10 -1
  124. package/src/config/provider-validation.ts +7 -2
  125. package/src/config/removed-opencode-free.ts +135 -0
  126. package/src/config/schema/account-auto-pause.ts +14 -0
  127. package/src/config/schema/config-schema.ts +15 -0
  128. package/src/config/schema/leaf-validators.ts +7 -0
  129. package/src/config.ts +10 -3
  130. package/src/generated/compatibility-version.json +476 -216
  131. package/src/generated/model-metadata.ts +1 -1
  132. package/src/images/loop.ts +34 -11
  133. package/src/lib/account-auto-pause-response.ts +16 -0
  134. package/src/lib/bun-runtime.ts +18 -1
  135. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  136. package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
  137. package/src/lib/decision-quota-sink.ts +76 -0
  138. package/src/lib/provider-admission-wait.ts +14 -0
  139. package/src/lib/service-secrets.ts +37 -2
  140. package/src/lib/state-store-registrations.ts +25 -0
  141. package/src/lib/translator-budget.ts +1 -1
  142. package/src/lib/upstream-retry.ts +15 -0
  143. package/src/link/ssh-argv.ts +3 -0
  144. package/src/messaging/envelope.ts +6 -3
  145. package/src/messaging/remote-auth.ts +83 -0
  146. package/src/messaging/remote-bridge.ts +270 -0
  147. package/src/messaging/remote-contract.ts +108 -0
  148. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  149. package/src/messaging/remote-enrollment.ts +244 -0
  150. package/src/messaging/remote-envelope.ts +40 -0
  151. package/src/messaging/remote-files.ts +117 -0
  152. package/src/messaging/remote-owner.ts +169 -0
  153. package/src/messaging/remote-ports.ts +56 -0
  154. package/src/messaging/remote-process.ts +131 -0
  155. package/src/messaging/remote-replies.ts +98 -0
  156. package/src/messaging/remote-rpc-admission.ts +32 -0
  157. package/src/messaging/remote-send.ts +72 -0
  158. package/src/messaging/remote-store.ts +111 -0
  159. package/src/messaging/remote-tunnels.ts +65 -0
  160. package/src/messaging/rpc.ts +13 -1
  161. package/src/oauth/account-auto-pause-policy.ts +66 -0
  162. package/src/oauth/account-auto-pause.ts +171 -0
  163. package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
  164. package/src/oauth/account-import/parser.ts +15 -2
  165. package/src/oauth/account-import/types.ts +7 -0
  166. package/src/oauth/account-quota-rank.ts +2 -1
  167. package/src/oauth/anthropic-account-refusal.ts +6 -0
  168. package/src/oauth/anthropic-model-family.ts +8 -0
  169. package/src/oauth/anthropic-model-quota.ts +2 -6
  170. package/src/oauth/api-key-pool-preserve.ts +37 -0
  171. package/src/oauth/chatgpt-shared.ts +10 -1
  172. package/src/oauth/dsh.ts +45 -2
  173. package/src/oauth/index.ts +5 -35
  174. package/src/oauth/kiro.ts +44 -1
  175. package/src/oauth/login-flow-state.ts +11 -2
  176. package/src/oauth/store.ts +35 -8
  177. package/src/oauth/types.ts +2 -0
  178. package/src/plugins/upstream-hooks.ts +14 -1
  179. package/src/providers/api-key-pause.ts +32 -0
  180. package/src/providers/api-key-resolve.ts +10 -4
  181. package/src/providers/api-key-selection.ts +6 -1
  182. package/src/providers/api-keys.ts +8 -4
  183. package/src/providers/claude-cli-usage.ts +25 -5
  184. package/src/providers/initial-model-selection.ts +2 -2
  185. package/src/providers/key-failover.ts +21 -13
  186. package/src/providers/key-store.ts +8 -3
  187. package/src/providers/mistral-transport.ts +207 -0
  188. package/src/providers/model-rename-fields.ts +1 -0
  189. package/src/providers/openai-sidecar.ts +5 -0
  190. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  191. package/src/providers/quota/account-cache.ts +15 -1
  192. package/src/providers/quota/anthropic-account-quota.ts +20 -7
  193. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  194. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  195. package/src/providers/quota/antigravity.ts +45 -8
  196. package/src/providers/quota/failure-codes.ts +18 -0
  197. package/src/providers/quota/report-cache.ts +14 -0
  198. package/src/providers/quota/vendor-probes-oauth.ts +15 -6
  199. package/src/providers/quota-decision-publication.ts +77 -0
  200. package/src/providers/quota-decision-snapshot.ts +145 -0
  201. package/src/providers/quota-routing-cache.ts +4 -1
  202. package/src/providers/quota-types.ts +6 -1
  203. package/src/providers/quota.ts +8 -3
  204. package/src/providers/registry/entries-extended.ts +14 -53
  205. package/src/providers/registry/model-seeds.ts +1 -1
  206. package/src/providers/registry.ts +1 -2
  207. package/src/providers/routed-config.ts +1 -1
  208. package/src/providers/stale-vision-classification-migration.ts +1 -1
  209. package/src/providers/xai-transport.ts +2 -1
  210. package/src/responses/audio.ts +46 -0
  211. package/src/responses/input-media.ts +37 -15
  212. package/src/responses/parser-content.ts +22 -27
  213. package/src/responses/parser.ts +18 -10
  214. package/src/responses/reasoning-retention.ts +218 -11
  215. package/src/responses/schema.ts +3 -4
  216. package/src/responses/web-search-replay.ts +147 -0
  217. package/src/routing/compatibility/behavior.ts +4 -1
  218. package/src/server/auth-cors.ts +7 -0
  219. package/src/server/background-lifecycle.ts +2 -1
  220. package/src/server/chat-native.ts +36 -20
  221. package/src/server/claude-messages.ts +2 -1
  222. package/src/server/index/optional-listeners.ts +6 -0
  223. package/src/server/index/serve-options.ts +8 -2
  224. package/src/server/index/websocket-handler.ts +10 -0
  225. package/src/server/lifecycle.ts +17 -1
  226. package/src/server/management/combo-routes.ts +17 -2
  227. package/src/server/management/oauth-account-routes.ts +12 -2
  228. package/src/server/management/provider-completion-test.ts +289 -0
  229. package/src/server/management/provider-routes.ts +9 -1
  230. package/src/server/management/route-registry.ts +2 -0
  231. package/src/server/messages-native.ts +29 -4
  232. package/src/server/messages-shared.ts +4 -1
  233. package/src/server/request-log.ts +23 -9
  234. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  235. package/src/server/responses/adapter-continuation.ts +2 -2
  236. package/src/server/responses/adapter-dispatch.ts +79 -30
  237. package/src/server/responses/codex-ws-exchange.ts +3 -0
  238. package/src/server/responses/codex-ws-metadata.ts +44 -0
  239. package/src/server/responses/codex-ws-session.ts +21 -1
  240. package/src/server/responses/combo-session-recall.ts +25 -0
  241. package/src/server/responses/combo-stream-preflight.ts +10 -3
  242. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  243. package/src/server/responses/compact.ts +10 -5
  244. package/src/server/responses/context-overflow.ts +141 -2
  245. package/src/server/responses/core-auth.ts +20 -0
  246. package/src/server/responses/core-codex-account.ts +28 -15
  247. package/src/server/responses/core-combo.ts +34 -5
  248. package/src/server/responses/core-opaque-recovery.ts +20 -11
  249. package/src/server/responses/core.ts +12 -9
  250. package/src/server/responses/deepseek-image-files.ts +121 -0
  251. package/src/server/responses/empty-completion-guard.ts +10 -0
  252. package/src/server/responses/fetch-helpers.ts +65 -14
  253. package/src/server/responses/input-admission.ts +6 -2
  254. package/src/server/responses/jev-model-invoke.ts +13 -14
  255. package/src/server/responses/main-hard-lock-routing.ts +33 -8
  256. package/src/server/responses/native-steering-log.ts +2 -2
  257. package/src/server/responses/opengateway-completion-guard.ts +232 -0
  258. package/src/server/responses/passthrough-completion.ts +41 -0
  259. package/src/server/responses/passthrough-delivery.ts +5 -2
  260. package/src/server/responses/passthrough-dispatch.ts +42 -45
  261. package/src/server/responses/passthrough-key-rotation.ts +40 -0
  262. package/src/server/responses/passthrough-observations.ts +38 -0
  263. package/src/server/responses/reasoning-archive-error.ts +21 -0
  264. package/src/server/responses/request-compression.ts +98 -0
  265. package/src/server/responses/request-prepare.ts +11 -1
  266. package/src/server/responses/request-transport.ts +92 -5
  267. package/src/server/responses/run-turn-execution.ts +2 -0
  268. package/src/server/responses/sidecar-execution.ts +16 -3
  269. package/src/server/responses/terminal-guard.ts +2 -0
  270. package/src/server/startup-action-control.ts +2 -2
  271. package/src/server/startup-health-cache.ts +2 -2
  272. package/src/server/windows-tray-control.ts +2 -2
  273. package/src/service/definition.ts +9 -1
  274. package/src/service/orchestration.ts +17 -0
  275. package/src/types/account-auto-pause.ts +15 -0
  276. package/src/types/config.ts +23 -1
  277. package/src/types/provider.ts +11 -5
  278. package/src/types/request.ts +36 -3
  279. package/src/types.ts +2 -0
  280. package/src/usage/cost.ts +17 -1
  281. package/src/usage/expected-prices.ts +3 -1
  282. package/src/usage/jev-stats.ts +4 -0
  283. package/src/usage/log.ts +2 -0
  284. package/src/usage/summary.ts +7 -0
  285. package/src/web-search/loop.ts +39 -13
  286. package/src/web-search/progress-stream.ts +4 -1
  287. package/src/web-search/run-turn-loop.ts +9 -3
  288. package/src/web-search/sidecar-search-replay.ts +96 -0
  289. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  290. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  291. package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
@@ -1089,7 +1089,11 @@ export interface OcxConfig {
1089
1089
  modelCacheTtlMs?: number;
1090
1090
  /** Evictable retained app-state budget in MiB. Default 256; valid 64..4096. */
1091
1091
  appOwnedMemoryBudgetMb?: number;
1092
- /** Anthropic prompt-cache retention: "short" = 5-min ephemeral (default), "long" = 1-hour extended, "none" = disabled. */
1092
+ /**
1093
+ * Anthropic prompt-cache retention: "short" = 5-min ephemeral, "long" = 1-hour extended,
1094
+ * "none" = disabled. Unset = "long" on first-party api.anthropic.com, "short" on
1095
+ * Anthropic-compatible endpoints (adapters/anthropic/cache-retention.ts).
1096
+ */
1093
1097
  cacheRetention?: "none" | "short" | "long";
1094
1098
  /** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
1095
1099
  webSearchSidecar?: OcxWebSearchSidecarConfig;
@@ -1113,6 +1117,9 @@ export interface OcxConfig {
1113
1117
  codexAccounts?: CodexAccount[];
1114
1118
  /** Account ids administratively excluded from future pool selection until resumed. */
1115
1119
  pausedCodexAccountIds?: string[];
1120
+ /** Internal policy epochs; null marks malformed disk input and suppresses automatic pause. */
1121
+ accountAutoPausePolicyRevisions?: Record<string, string> | null;
1122
+ codexAccountPauseState?: Record<string, { revision: string; details?: import("./account-auto-pause").AccountPauseDetails }>;
1116
1123
  /**
1117
1124
  * Account ids, `__main__` included, allowed to keep serving from ChatGPT credits once one of
1118
1125
  * their usage windows is full. Every other account is skipped by selection at 100% until its
@@ -1293,6 +1300,13 @@ export interface OcxConfig {
1293
1300
  * that work today — on Azure and custom Responses gateways as well, whose limits are unknown.
1294
1301
  */
1295
1302
  maxUpstreamBodyBytes?: number;
1303
+ /**
1304
+ * Opt-in compression of native ChatGPT Responses HTTP request bodies (#7247): "gzip" or "zstd".
1305
+ * Omitted, "off", or any other value = identity, the default. Applies only at the final HTTP send
1306
+ * to chatgpt.com Codex responses/compact, to string bodies from 1 KiB to 16 MiB; WebSocket turns
1307
+ * and every other destination are unchanged. Compression runs synchronously on the event loop.
1308
+ */
1309
+ upstreamRequestCompression?: "off" | "gzip" | "zstd";
1296
1310
  /**
1297
1311
  * Opt-in ceiling, in bytes, on a decompressed INBOUND data-plane request body (#3573).
1298
1312
  *
@@ -1454,6 +1468,8 @@ export interface OcxComboConfig {
1454
1468
  nativeAlias?: boolean;
1455
1469
  /** Display-only label for the public catalog row. Required for native aliases. */
1456
1470
  displayName?: string;
1471
+ /** Display-only description for the public catalog row; unset keeps the generated text (#7229). */
1472
+ description?: string;
1457
1473
  /**
1458
1474
  * `strategy: "jev"` only: provider id of the decision service. Omitted or `"jev"` uses the
1459
1475
  * canonical TypeSafe endpoint; any other id must name a configured `adapter: "jev-decision"`
@@ -1469,6 +1485,10 @@ export interface OcxComboConfig {
1469
1485
  * whose first call may include a cold model load.
1470
1486
  */
1471
1487
  decisionTimeoutMs?: number;
1488
+ /** JEV-only advisory loaded quota evidence; off/unset leaves request bytes unchanged. */
1489
+ decisionQuotaSignals?: boolean | null;
1490
+ /** Partial 0..100 tier overrides; defaults limited 70/nearlyExhausted 90. null clears. */
1491
+ decisionQuotaTiers?: import("../combos/jev-quota-config").JevQuotaTiers | null;
1472
1492
  }
1473
1493
 
1474
1494
  export type OcxRoutingUnknownEvidenceMode = "allow" | "penalize" | "exclude";
@@ -1757,6 +1777,8 @@ export interface CodexLowQuotaProtectionConfig {
1757
1777
  actions: {
1758
1778
  pause: boolean;
1759
1779
  notify: boolean;
1780
+ /** Opt-in: also hand each notice to this host's OS notifier (macOS, Linux). Requires notify. */
1781
+ desktop?: boolean;
1760
1782
  };
1761
1783
  windows: {
1762
1784
  short: boolean;
@@ -1,4 +1,5 @@
1
1
  import type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./wire";
2
+ import type { AccountAutoPausePolicy, AccountPauseDetails } from "./account-auto-pause";
2
3
  import type { AnthropicAccountPoolConfig } from "./anthropic-account-pool";
3
4
 
4
5
  /**
@@ -287,6 +288,8 @@ export interface ModelCapabilities {
287
288
  /** Requested tier only; does not imply an upstream window or activate an unverified wire. */
288
289
  contextTier?: "default" | "long_context";
289
290
  video?: { processing?: "static" | "agentic" };
291
+ /** Opt in exact first-party DeepSeek models to one-hour image file persistence. */
292
+ deepseekImageFiles?: boolean;
290
293
  }
291
294
 
292
295
  export interface OcxProviderConfig {
@@ -448,8 +451,9 @@ export interface OcxProviderConfig {
448
451
  */
449
452
  preserveResponsesMessageMetadata?: boolean;
450
453
  /**
451
- * Opt in to lossy hosted-search history notes on ordinary canonical Codex Lite requests.
452
- * Default off; only literal true enables it, and non-Lite/native compact stay unchanged.
454
+ * Opt in to lossy hosted-search history notes on canonical Codex Lite ordinary turns and
455
+ * native v2 compaction triggers, even when search is declared. Default off; only literal true
456
+ * enables it. History without a search declaration is always projected, independent of this.
453
457
  */
454
458
  projectResponsesLiteHostedSearchHistory?: boolean;
455
459
  /**
@@ -567,7 +571,9 @@ export interface OcxProviderConfig {
567
571
  * entry so routing stays single-key; managed via /api/providers/keys. A legacy bare
568
572
  * `apiKey` seeds a one-entry pool on first management touch.
569
573
  */
570
- apiKeyPool?: Array<{ id: string; key: string; label?: string; addedAt?: number }>;
574
+ apiKeyPool?: Array<{ id: string; key: string; label?: string; addedAt?: number; disabled?: boolean; pauseDetails?: AccountPauseDetails; pauseRevision?: string }>;
575
+ /** Explicit opt-in permanent account refusal policy; default off. */
576
+ accountAutoPause?: AccountAutoPausePolicy;
571
577
  /**
572
578
  * Optional proactive ordering for `apiKeyPool` when the committed key is already
573
579
  * cooling. Deliberately NOT named like the OAuth `accountPoolStrategy`: an API key
@@ -1110,9 +1116,9 @@ export interface OcxProviderConfig {
1110
1116
  googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
1111
1117
  /** Google tool-schema compatibility policy. Omitted preserves compatible report-only behavior. */
1112
1118
  googleToolSchemaPolicy?: "compatible" | "reject-lossy";
1113
- /** Vertex AI GCP project id (or GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env). */
1119
+ /** Vertex AI GCP project id. API keys use it to select the project endpoint; ADC falls back to GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env. */
1114
1120
  project?: string;
1115
- /** Vertex AI location, e.g. "us-central1" or "global" (or GOOGLE_CLOUD_LOCATION env). */
1121
+ /** Vertex AI location, e.g. "us-central1" or "global". API-key project endpoints default to "global"; ADC falls back to GOOGLE_CLOUD_LOCATION env. */
1116
1122
  location?: string;
1117
1123
  /**
1118
1124
  * Cursor adapter only: MCP servers opencodex starts/connects and exposes to the Cursor agent
@@ -46,6 +46,14 @@ export interface OcxReasoningReplayScopeRef {
46
46
  current?: Readonly<OcxReasoningReplayIdentity>;
47
47
  }
48
48
 
49
+ /** One replayed hosted search cell, rebuilt as the `web_search` call/result pair `callId`. */
50
+ export interface OcxReplayedWebSearchCell {
51
+ /** The client-visible cell id; absent when the cell carried no usable id (no recorded result). */
52
+ cellId?: string;
53
+ callId: string;
54
+ failed: boolean;
55
+ }
56
+
49
57
  export interface OcxParsedRequest {
50
58
  modelId: string;
51
59
  /** Client-facing model selector retained for Anthropic routes after wire-model normalization. */
@@ -152,6 +160,12 @@ export interface OcxParsedRequest {
152
160
  * executes searches via the gpt-5.6-luna sidecar (see src/web-search). Absent when not requested.
153
161
  */
154
162
  _webSearch?: Record<string, unknown>;
163
+ /**
164
+ * Replayed hosted `web_search_call` cells the parser rebuilt as `web_search` call/result pairs
165
+ * (src/responses/web-search-replay.ts). The web-search sidecar swaps in the result it recorded for
166
+ * a cell, or removes the pairs when it does not run (src/web-search/sidecar-search-replay.ts).
167
+ */
168
+ _replayedWebSearchCells?: OcxReplayedWebSearchCell[];
155
169
  /** Hosted image_generation tool config stashed for the image bridge sidecar (see src/images). */
156
170
  _imageGeneration?: { toolNames: Set<string>; originalTool?: Record<string, unknown> };
157
171
  /**
@@ -308,8 +322,23 @@ export interface OcxDocumentContent {
308
322
  filename?: string;
309
323
  }
310
324
 
325
+ /**
326
+ * Inline audio carried by Codex's Responses input. Chat Completions accepts this as an
327
+ * `input_audio` content part only for WAV and MP3 payloads, so the normalized form keeps the
328
+ * base64 bytes and a wire-safe format label rather than the original data URL.
329
+ */
330
+ export interface OcxAudioContent {
331
+ type: "audio";
332
+ /** Base64 payload with no `data:` prefix. */
333
+ data: string;
334
+ /** Chat Completions input_audio supports WAV and MP3. */
335
+ format: "mp3" | "wav";
336
+ /** Text marker for wires that cannot carry the bytes. */
337
+ text: string;
338
+ }
339
+
311
340
  /** A user/developer message content part: text, native media, or an attached document. */
312
- export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent | OcxDocumentContent;
341
+ export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent | OcxDocumentContent | OcxAudioContent;
313
342
 
314
343
  export interface OcxThinkingContent {
315
344
  type: "thinking";
@@ -448,9 +477,10 @@ export type AdapterEvent =
448
477
  // "Searching the web" spinner, then `end` once it resolves. The bridge maps begin → an
449
478
  // output_item.added(in_progress) and end → the matching output_item.done(completed|failed) under
450
479
  // the SAME output index, so the activity animates instead of flashing completed instantly.
451
- | { type: "web_search_call_begin"; id: string }
480
+ // `itemId` lets the loop name the client-visible cell so it can record the result for replay.
481
+ | { type: "web_search_call_begin"; id: string; itemId?: string }
452
482
  | {
453
- type: "web_search_call_end"; id: string; queries: string[]; status?: "completed" | "failed"; sources?: OcxUrlCitation[];
483
+ type: "web_search_call_end"; id: string; itemId?: string; queries: string[]; status?: "completed" | "failed"; sources?: OcxUrlCitation[];
454
484
  /** Anthropic's server-side call (adapters/anthropic.ts): its `server_tool_use` and result block, verbatim. */
455
485
  anthropic?: AnthropicServerBlocks;
456
486
  }
@@ -510,6 +540,8 @@ export interface OcxUrlCitation {
510
540
  * - `cachedInputTokens` is cache READ tokens only (a subset of `inputTokens`).
511
541
  * - `cacheReadInputTokens`/`cacheCreationInputTokens` carry the read/write split when
512
542
  * the provider reports both; reads mirror `cachedInputTokens`.
543
+ * - `cacheCreation1hInputTokens` is the 1-hour-TTL subset of `cacheCreationInputTokens`
544
+ * (Anthropic `cache_creation.ephemeral_1h_input_tokens`), priced at the 1h write tier.
513
545
  * - `totalTokens` = inputTokens + outputTokens. Never re-add cache detail on top.
514
546
  */
515
547
  export interface OcxUsage {
@@ -527,6 +559,7 @@ export interface OcxUsage {
527
559
  cachedInputTokens?: number;
528
560
  cacheReadInputTokens?: number;
529
561
  cacheCreationInputTokens?: number;
562
+ cacheCreation1hInputTokens?: number;
530
563
  reasoningOutputTokens?: number;
531
564
  estimated?: boolean;
532
565
  /**
package/src/types.ts CHANGED
@@ -41,6 +41,7 @@ export {
41
41
  export type {
42
42
  OcxReasoningReplayIdentity,
43
43
  OcxReasoningReplayScopeRef,
44
+ OcxReplayedWebSearchCell,
44
45
  OcxParsedRequest,
45
46
  OcxContext,
46
47
  OcxMessage,
@@ -50,6 +51,7 @@ export type {
50
51
  OcxToolResultMessage,
51
52
  OcxTextContent,
52
53
  OcxImageContent,
54
+ OcxAudioContent,
53
55
  OcxDocumentContent,
54
56
  OcxContentPart,
55
57
  OcxThinkingContent,
package/src/usage/cost.ts CHANGED
@@ -65,6 +65,8 @@ export interface CostTokens {
65
65
  output: number;
66
66
  cacheRead: number;
67
67
  cacheWrite: number;
68
+ /** The 1-hour-TTL subset of `cacheWrite`, when the provider reports the split. */
69
+ cacheWrite1h?: number;
68
70
  }
69
71
 
70
72
  export interface CostBreakdown {
@@ -163,22 +165,35 @@ export function normalizeCostTokens(usage: OcxUsage): CostTokens | null {
163
165
  for (const cacheRead of candidates) {
164
166
  if (![input, output, cacheRead, cacheWrite].every(finiteNonNegative)) return null;
165
167
  if (cacheRead + cacheWrite > input) continue;
168
+ const cacheWrite1h = usage.cacheCreation1hInputTokens;
166
169
  return {
167
170
  input: Math.max(0, input - cacheRead - cacheWrite),
168
171
  output,
169
172
  cacheRead,
170
173
  cacheWrite,
174
+ ...(typeof cacheWrite1h === "number" && finiteNonNegative(cacheWrite1h) && cacheWrite1h > 0
175
+ ? { cacheWrite1h: Math.min(cacheWrite1h, cacheWrite) } : {}),
171
176
  };
172
177
  }
173
178
  return null;
174
179
  }
175
180
 
181
+ /**
182
+ * Price of a 1-hour cache write relative to the 5-minute rate that Cost4.cacheWrite records.
183
+ * Anthropic bills 5m writes at 1.25x and 1h writes at 2x base input, so 2 / 1.25. Scaling the
184
+ * 5m rate (rather than input) keeps every tier multiplier already applied to the Cost4 and keeps
185
+ * a free-write row (cacheWrite 0) free.
186
+ */
187
+ export const CACHE_WRITE_1H_OVER_5M = 2 / 1.25;
188
+
176
189
  /** jawcode unit convention: USD per 1M tokens (jawcode stats/db.ts calculateCatalogCost). */
177
190
  export function calculateCost(tokens: CostTokens, cost4: Cost4): CostBreakdown {
178
191
  const input = cost4.input * tokens.input / 1_000_000;
179
192
  const output = cost4.output * tokens.output / 1_000_000;
180
193
  const cacheRead = cost4.cacheRead * tokens.cacheRead / 1_000_000;
181
- const cacheWrite = cost4.cacheWrite * tokens.cacheWrite / 1_000_000;
194
+ const write1h = Math.min(tokens.cacheWrite1h ?? 0, tokens.cacheWrite);
195
+ const cacheWrite = (cost4.cacheWrite * (tokens.cacheWrite - write1h)
196
+ + cost4.cacheWrite * CACHE_WRITE_1H_OVER_5M * write1h) / 1_000_000;
182
197
  return { input, output, cacheRead, cacheWrite, total: input + output + cacheRead + cacheWrite };
183
198
  }
184
199
 
@@ -656,6 +671,7 @@ export function estimateComboCost(
656
671
  tokens.output += est.tokens.output;
657
672
  tokens.cacheRead += est.tokens.cacheRead;
658
673
  tokens.cacheWrite += est.tokens.cacheWrite;
674
+ if (est.tokens.cacheWrite1h) tokens.cacheWrite1h = (tokens.cacheWrite1h ?? 0) + est.tokens.cacheWrite1h;
659
675
  cost.input += est.cost.input;
660
676
  cost.output += est.cost.output;
661
677
  cost.cacheRead += est.cost.cacheRead;
@@ -125,7 +125,9 @@ const GLM_52: Cost4 = { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0
125
125
  const GLM_53: Cost4 = { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 };
126
126
  const GLM_53_FLASH: Cost4 = { input: 0.15, output: 0.5, cacheRead: 0.03, cacheWrite: 0 };
127
127
  const QWEN38_MAX: Cost4 = { input: 2, output: 6, cacheRead: 0, cacheWrite: 0 };
128
- // Anthropic official list prices (USD / 1M tokens). Cache write uses the published 5-minute rate.
128
+ // Anthropic official list prices (USD / 1M tokens). Cache write uses the published 5-minute rate;
129
+ // writes the usage reports as 1-hour (cache_creation.ephemeral_1h_input_tokens) are priced at
130
+ // 2x base input through CACHE_WRITE_1H_OVER_5M in cost.ts.
129
131
  const CLAUDE_SONNET_46: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
130
132
  const CLAUDE_OPUS_46: Cost4 = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
131
133
  // Claude Fable 5.1: 10 / 50, 5m cache write 12.50. Cache hits are 0.025x base input
@@ -1,3 +1,4 @@
1
+ import { normalizeJevQuotaSummary, type JevQuotaDecisionSummary } from "../combos/jev-quota-config";
1
2
  import type { OcxComboDefaultEffort } from "../types";
2
3
  import type { PersistedUsageEntry } from "./log";
3
4
  import { usageDisplayTotalTokens } from "./totals";
@@ -39,6 +40,7 @@ export interface PersistedJevDecisionV1 {
39
40
  };
40
41
  gate: JevDecisionGate;
41
42
  latencyMs: number;
43
+ quota?: JevQuotaDecisionSummary;
42
44
  confidence?: number;
43
45
  chosenProbability?: number;
44
46
  usage?: {
@@ -182,6 +184,7 @@ export function normalizePersistedJevDecision(value: unknown): PersistedJevDecis
182
184
  const confidence = probability(value.confidence);
183
185
  const chosenProbability = probability(value.chosenProbability);
184
186
  const usage = normalizedDecisionUsage(value.usage);
187
+ const quota = normalizeJevQuotaSummary(value.quota);
185
188
  const backend = value.backend === "typesafe" || value.backend === "systemone" || value.backend === "model"
186
189
  ? value.backend
187
190
  : undefined;
@@ -196,6 +199,7 @@ export function normalizePersistedJevDecision(value: unknown): PersistedJevDecis
196
199
  },
197
200
  gate: gate as JevDecisionGate,
198
201
  latencyMs,
202
+ ...(quota ? { quota } : {}),
199
203
  ...(confidence !== undefined ? { confidence } : {}),
200
204
  ...(chosenProbability !== undefined ? { chosenProbability } : {}),
201
205
  ...(usage ? { usage } : {}),
package/src/usage/log.ts CHANGED
@@ -581,6 +581,7 @@ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined
581
581
  ...(typeof usage.cachedInputTokens === "number" ? { cachedInputTokens: usage.cachedInputTokens } : {}),
582
582
  ...(typeof usage.cacheReadInputTokens === "number" ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
583
583
  ...(typeof usage.cacheCreationInputTokens === "number" ? { cacheCreationInputTokens: usage.cacheCreationInputTokens } : {}),
584
+ ...(typeof usage.cacheCreation1hInputTokens === "number" ? { cacheCreation1hInputTokens: usage.cacheCreation1hInputTokens } : {}),
584
585
  ...(typeof usage.reasoningOutputTokens === "number" ? { reasoningOutputTokens: usage.reasoningOutputTokens } : {}),
585
586
  ...(isNonNegativeFiniteNumber(usage.providerCredits) ? { providerCredits: usage.providerCredits } : {}),
586
587
  ...(usage.estimated ? { estimated: true } : {}),
@@ -625,6 +626,7 @@ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
625
626
  "cachedInputTokens",
626
627
  "cacheReadInputTokens",
627
628
  "cacheCreationInputTokens",
629
+ "cacheCreation1hInputTokens",
628
630
  "reasoningOutputTokens",
629
631
  "providerCredits",
630
632
  ] as const) {
@@ -520,6 +520,8 @@ function projectedComboUsage(
520
520
  let cachedInputTokens = 0;
521
521
  let cacheReadInputTokens = 0;
522
522
  let cacheCreationInputTokens = 0;
523
+ let cacheCreation1hInputTokens = 0;
524
+ let hasCacheWrite1h = false;
523
525
  let reasoningOutputTokens = 0;
524
526
  let hasUsage = false;
525
527
  let hasCacheTelemetry = false;
@@ -540,6 +542,10 @@ function projectedComboUsage(
540
542
  cacheReadInputTokens += cache.read;
541
543
  }
542
544
  if (typeof cache.creation === "number") cacheCreationInputTokens += cache.creation;
545
+ if (typeof attempt.usage.cacheCreation1hInputTokens === "number") {
546
+ hasCacheWrite1h = true;
547
+ cacheCreation1hInputTokens += attempt.usage.cacheCreation1hInputTokens;
548
+ }
543
549
  if (typeof attempt.usage.reasoningOutputTokens === "number") {
544
550
  hasReasoningTelemetry = true;
545
551
  reasoningOutputTokens += attempt.usage.reasoningOutputTokens;
@@ -559,6 +565,7 @@ function projectedComboUsage(
559
565
  inputTokens,
560
566
  outputTokens,
561
567
  ...(hasCacheTelemetry ? { cachedInputTokens, cacheReadInputTokens, cacheCreationInputTokens } : {}),
568
+ ...(hasCacheWrite1h ? { cacheCreation1hInputTokens } : {}),
562
569
  ...(hasReasoningTelemetry ? { reasoningOutputTokens } : {}),
563
570
  ...(estimated ? { estimated: true } : {}),
564
571
  }
@@ -14,6 +14,7 @@ import { bridgeToResponsesSSE } from "../bridge";
14
14
  import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
15
15
  import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
16
16
  import { mergeUsage } from "../server/responses/empty-completion-guard";
17
+ import { markSidecarContextOverflow, sidecarLoopTerminalEvent, upstreamErrorCode, type SidecarLoopFailure } from "../server/responses/context-overflow";
17
18
  import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
18
19
  import { runAnthropicWebSearch } from "./anthropic-executor";
19
20
  import { runXaiWebSearch, type XaiSearchOptions } from "./xai-executor";
@@ -23,8 +24,9 @@ import type { WebSearchBackendId } from "./index";
23
24
  import { pacingHeaderDeadline } from "./header-deadline";
24
25
  import { redactSecretString } from "../lib/redact";
25
26
  import { readBoundedResponseBody } from "../lib/bounded-body";
26
- import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
27
+ import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, isSameTargetRetryHold, SendBudgetExhaustedError } from "../lib/upstream-retry";
27
28
  import { rateLimitRetryDelayMs } from "../providers/key-failover";
29
+ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
28
30
  import {
29
31
  isTranslatorBudgetExceededError,
30
32
  TRANSLATOR_MAX_TURN_BYTES,
@@ -33,6 +35,7 @@ import {
33
35
  import { formatWebSearchResults } from "./format-result";
34
36
  import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError, WebSearchAdapterEventError } from "./progress-stream";
35
37
  import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
38
+ import { newSidecarSearchCellId, rememberSidecarSearch, restoreSidecarSearchResults } from "./sidecar-search-replay";
36
39
 
37
40
  const SSE_HEADERS = {
38
41
  "Content-Type": "text/event-stream",
@@ -284,12 +287,21 @@ function jsonError(status: number, message: string, event?: Extract<AdapterEvent
284
287
  /** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
285
288
  * non-200 jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
286
289
  class LoopError extends Error {
287
- constructor(readonly status: number, message: string, readonly event?: Extract<AdapterEvent, { type: "error" }>) {
290
+ constructor(
291
+ readonly status: number,
292
+ message: string,
293
+ readonly event?: Extract<AdapterEvent, { type: "error" }>,
294
+ readonly detail?: Pick<SidecarLoopFailure, "upstreamCode" | "nonReplayable">,
295
+ ) {
288
296
  super(message);
289
297
  this.name = "LoopError";
290
298
  }
291
299
  }
292
300
 
301
+ function loopFailure(e: LoopError): SidecarLoopFailure {
302
+ return { status: e.status, message: e.message, ...(e.event ? { event: e.event } : {}), ...e.detail };
303
+ }
304
+
293
305
  /**
294
306
  * Dependencies for one web-search loop iteration: parsed request, active adapter,
295
307
  * incoming metadata, and the configured search executor.
@@ -417,6 +429,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
417
429
  ? Math.floor(deps.stallTimeoutSec * 1000)
418
430
  : 300_000;
419
431
 
432
+ // Put recorded results back into replayed search pairs before the first iteration (#7269).
433
+ restoreSidecarSearchResults(parsed);
420
434
  const messages: OcxMessage[] = [...parsed.context.messages];
421
435
  const replaySnapshots = new Set<AnthropicReplaySnapshot>();
422
436
  const loopT0 = Date.now();
@@ -491,9 +505,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
491
505
  // through the adapter formatter or the generic transport error, which could expose its
492
506
  // raw message. A parent/client cancellation still owns the request lifecycle as 499.
493
507
  if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
494
- throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
508
+ throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`, undefined,
509
+ { nonReplayable: isNonReplayableResponse(prepared.response) });
495
510
  }
496
511
  let formatted = "";
512
+ let upstreamCode: string | undefined;
497
513
  if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
498
514
  try {
499
515
  formatted = prepared.responseAdapter.formatErrorBody(
@@ -503,8 +519,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
503
519
  ).trim();
504
520
  } catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
505
521
  }
522
+ if (body.displaySafe && !body.truncated) upstreamCode = upstreamErrorCode(body.text);
506
523
  const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
507
- throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
524
+ throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`, undefined,
525
+ { upstreamCode, nonReplayable: isNonReplayableResponse(prepared.response) });
508
526
  }
509
527
  };
510
528
 
@@ -652,7 +670,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
652
670
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
653
671
  // rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
654
672
  while (
655
- prepared.response.status === 429
673
+ prepared.response.status === 429 && !isSameTargetRetryHold(prepared.response)
656
674
  && rateLimitRetryPolicy !== null
657
675
  && rateLimitRetries < rateLimitRetryPolicy.attempts
658
676
  ) {
@@ -736,7 +754,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
736
754
  throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
737
755
  }
738
756
  if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
739
- if (error instanceof LoopError) throw error;
757
+ if (error instanceof LoopError || error instanceof RequestPacingQueueOverloadError) throw error;
740
758
  throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
741
759
  } finally {
742
760
  headerDeadline.clear();
@@ -834,7 +852,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
834
852
  if (terminal.code === "translation_buffer_limit") {
835
853
  throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
836
854
  }
837
- throw new LoopError(502, terminal.message);
855
+ // Keep an adapter's own overflow verdict; every other terminal stays a generic 502.
856
+ throw new LoopError(502, terminal.message, terminal.code === "context_length_exceeded" ? terminal : undefined);
838
857
  }
839
858
  deps.thinkingRecovery?.observeIteration(events);
840
859
  return { ...scanEventsForWebSearch(events), streamedPassthroughCount, events, anthropicReplay };
@@ -849,6 +868,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
849
868
  async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = [], appendAssistant = true): AsyncGenerator<AdapterEvent> {
850
869
  const results: { query: string; outcome: SidecarOutcome }[] = [];
851
870
  let beganCell = false;
871
+ const cellItemId = newSidecarSearchCellId();
852
872
  if (call.queries.length === 0) {
853
873
  // The model called web_search with neither query nor queries — count it against the budget
854
874
  // (loop-bounding) exactly as the old empty-query placeholder did, but emit no cell.
@@ -870,7 +890,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
870
890
  // Real sidecar search. Open the cell once, before the first real query runs.
871
891
  if (!beganCell) {
872
892
  beganCell = true;
873
- yield { type: "web_search_call_begin", id: call.id };
893
+ yield { type: "web_search_call_begin", id: call.id, itemId: cellItemId };
874
894
  }
875
895
  // F5: the anthropic sidecar authenticates with its own stored OAuth — it never touches the
876
896
  // ChatGPT forward headers and must NOT record a Codex/OpenAI pool outcome.
@@ -945,12 +965,15 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
945
965
  });
946
966
  // One aggregated tool result. isError only when EVERY query failed (a partial success is usable).
947
967
  const allFailed = results.every(r => !!r.outcome.error);
968
+ const resultText = formatWebSearchResults(results, !!parsed._structuredOutput);
948
969
  messages.push({
949
970
  role: "toolResult", toolCallId: call.id, toolName: WEB_SEARCH_TOOL_NAME,
950
- content: formatWebSearchResults(results, !!parsed._structuredOutput),
971
+ content: resultText,
951
972
  isError: allFailed, timestamp: now,
952
973
  });
953
974
  if (beganCell) {
975
+ // The client sees only the cell; keep the result so its replay can carry it (#7269).
976
+ rememberSidecarSearch(parsed, cellItemId, { id: call.id, arguments: callArgs }, resultText);
954
977
  // The cell is "completed" if any query produced a usable result, else "failed". `queries`
955
978
  // carries every attempted query so Codex renders the native plural label.
956
979
  const anySuccess = results.some(r => !r.outcome.error);
@@ -966,7 +989,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
966
989
  }
967
990
  }
968
991
  yield {
969
- type: "web_search_call_end", id: call.id,
992
+ type: "web_search_call_end", id: call.id, itemId: cellItemId,
970
993
  queries: call.queries,
971
994
  status: anySuccess ? "completed" : "failed",
972
995
  ...(sources.length > 0 ? { sources } : {}),
@@ -982,7 +1005,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
982
1005
  firstPrepared = await prepareIterationDrained(false);
983
1006
  } catch (e) {
984
1007
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
985
- if (e instanceof LoopError) return jsonError(e.status, e.message, e.event);
1008
+ if (e instanceof LoopError) return markSidecarContextOverflow(jsonError(e.status, e.message, e.event), loopFailure(e));
986
1009
  if (e instanceof SendBudgetExhaustedError) return jsonError(429, e.message,
987
1010
  { type: "error", message: e.message, errorType: "rate_limit_error", code: e.code, retryable: false });
988
1011
  throw e;
@@ -1137,10 +1160,13 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
1137
1160
  } else {
1138
1161
  if (e instanceof SendBudgetExhaustedError) yield { type: "error", status: 429,
1139
1162
  errorType: "rate_limit_error", code: e.code, message: e.message, retryable: false };
1140
- else yield e instanceof LoopError && e.event ? e.event
1163
+ // A later iteration's overflow ends the turn as the main path's terminal
1164
+ // context_length_exceeded, so Codex compacts instead of resending (#7316).
1165
+ else yield (e instanceof LoopError ? sidecarLoopTerminalEvent(loopFailure(e)) : undefined)
1166
+ ?? (e instanceof LoopError && e.event ? e.event
1141
1167
  : e instanceof LoopError && deps.thinkingRecovery
1142
1168
  ? { type: "error", status: e.status, errorType: "upstream_error", message: e.message }
1143
- : { type: "error", message: e instanceof Error ? e.message : String(e) };
1169
+ : { type: "error", message: e instanceof Error ? e.message : String(e) });
1144
1170
  }
1145
1171
  return;
1146
1172
  }
@@ -308,9 +308,12 @@ export async function* parseStreamWithProgress(
308
308
  }
309
309
  if (event.type === "error") {
310
310
  if (event.usage) options.onErrorUsage?.(event.usage);
311
+ // An adapter's own overflow verdict keeps its typed event so the sidecar can end the
312
+ // turn as context_length_exceeded (#7316); other errors keep the plain-message shape.
313
+ const preserve = options.preserveAdapterError || event.code === "context_length_exceeded";
311
314
  fail(event.code === "translation_buffer_limit"
312
315
  ? new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES)
313
- : options.preserveAdapterError ? new WebSearchAdapterEventError(event, replaySafe) : new Error(event.message));
316
+ : preserve ? new WebSearchAdapterEventError(event, replaySafe) : new Error(event.message));
314
317
  return;
315
318
  }
316
319
  replaySafe &&= event.type === "heartbeat" && event.replayUnsafe !== true;
@@ -43,6 +43,7 @@ import {
43
43
  type TranslatorBudget,
44
44
  } from "../lib/translator-budget";
45
45
  import { jsonUtf8Bytes } from "../lib/json-byte-size";
46
+ import { newSidecarSearchCellId, rememberSidecarSearch, restoreSidecarSearchResults } from "./sidecar-search-replay";
46
47
 
47
48
  export interface RunTurnWebSearchDeps {
48
49
  parsed: OcxParsedRequest;
@@ -77,6 +78,8 @@ export interface RunTurnWebSearchDeps {
77
78
  * active: same request plus the synthetic tool so the model can ask to search.
78
79
  */
79
80
  export function runTurnWebSearchInitialParsed(parsed: OcxParsedRequest): OcxParsedRequest {
81
+ // Runs before the first dispatch: put recorded results back into replayed search pairs (#7269).
82
+ restoreSidecarSearchResults(parsed);
80
83
  return {
81
84
  ...parsed,
82
85
  context: {
@@ -296,6 +299,7 @@ export async function* runTurnWebSearchLoop(
296
299
  ): AsyncGenerator<AdapterEvent> {
297
300
  const results: { query: string; outcome: SidecarOutcome }[] = [];
298
301
  let beganCell = false;
302
+ const cellItemId = newSidecarSearchCellId();
299
303
  if (call.queries.length === 0) {
300
304
  searchesExecuted++;
301
305
  results.push({ query: "", outcome: { text: "", sources: [], error: "the model called web_search with an empty query" } });
@@ -318,7 +322,7 @@ export async function* runTurnWebSearchLoop(
318
322
  } else {
319
323
  if (!beganCell) {
320
324
  beganCell = true;
321
- yield { type: "web_search_call_begin", id: call.id };
325
+ yield { type: "web_search_call_begin", id: call.id, itemId: cellItemId };
322
326
  }
323
327
  if (abortSignal?.aborted) {
324
328
  yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
@@ -358,12 +362,14 @@ export async function* runTurnWebSearchLoop(
358
362
  timestamp: now,
359
363
  });
360
364
  const allFailed = results.every(r => !!r.outcome.error);
365
+ const resultText = formatWebSearchResults(results, !!parsed._structuredOutput);
361
366
  appendMessage({
362
367
  role: "toolResult", toolCallId: call.id, toolName: WEB_SEARCH_TOOL_NAME,
363
- content: formatWebSearchResults(results, !!parsed._structuredOutput),
368
+ content: resultText,
364
369
  isError: allFailed, timestamp: now,
365
370
  });
366
371
  if (beganCell) {
372
+ rememberSidecarSearch(parsed, cellItemId, { id: call.id, arguments: callArgs }, resultText);
367
373
  const anySuccess = results.some(r => !r.outcome.error);
368
374
  const sources: { url: string; title?: string }[] = [];
369
375
  const seenSrc = new Set<string>();
@@ -375,7 +381,7 @@ export async function* runTurnWebSearchLoop(
375
381
  }
376
382
  }
377
383
  yield {
378
- type: "web_search_call_end", id: call.id,
384
+ type: "web_search_call_end", id: call.id, itemId: cellItemId,
379
385
  queries: call.queries,
380
386
  status: anySuccess ? "completed" : "failed",
381
387
  ...(sources.length > 0 ? { sources } : {}),