@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
@@ -54,7 +54,7 @@ import {
54
54
  DEEPSEEK_VISION_PREVIEW_MODEL,
55
55
  COMMAND_CODE_MIMO_CONTEXT_WINDOWS,
56
56
  COMMAND_CODE_MODEL_INPUT_MODALITIES,
57
- OPENCODE_FREE_DEEPSEEK_MODELS,
57
+ OPENCODE_ZEN_FREE_DEEPSEEK_MODELS,
58
58
  OPENCODE_ZEN_TEXT_ONLY_MODELS,
59
59
  OPENCODE_ZEN_IMAGE_MODELS,
60
60
  deepseekThinkingEffortsFor,
@@ -1020,7 +1020,12 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1020
1020
  },
1021
1021
  },
1022
1022
  // FREEZE 2026-07-10: codestral-latest is unconfirmed behind auth. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
1023
- { id: "mistral", label: "Mistral", baseUrl: "https://api.mistral.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://console.mistral.ai/api-keys", defaultModel: "codestral-latest" },
1023
+ {
1024
+ id: "mistral", label: "Mistral", baseUrl: "https://api.mistral.ai/v1", adapter: "openai-chat", authKind: "key",
1025
+ dashboardUrl: "https://console.mistral.ai/api-keys", defaultModel: "codestral-latest", promptCacheKey: true,
1026
+ // Native text-only input enrolls this model in the existing vision sidecar.
1027
+ modelInputModalities: { "zai-glm-5-3": ["text"] },
1028
+ },
1024
1029
  {
1025
1030
  id: "minimax", label: "MiniMax — Coding Plan", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key",
1026
1031
  dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
@@ -1089,20 +1094,20 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1089
1094
  },
1090
1095
  {
1091
1096
  id: "opencode-zen", label: "opencode zen", baseUrl: "https://opencode.ai/zen/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://opencode.ai/auth",
1092
- // Same opencode.ai/zen/v1 gateway as `opencode-free` (keyed tier): DeepSeek thinking mode
1097
+ // Keyed opencode.ai/zen/v1 gateway: DeepSeek thinking mode
1093
1098
  // requires the assistant's original reasoning_content to be replayed on tool-call
1094
1099
  // continuations, or the gateway answers HTTP 400 (issues #950/#994). Mirror the DeepSeek
1095
1100
  // reasoning + thinking metadata so `opencode-zen/deepseek-v4-flash-free` — and the other
1096
1101
  // Zen DeepSeek thinking models — never serialize a bare tool-call turn.
1097
- note: "Keyed OpenCode Zen gateway. Free models on this tier are often short-window rate-limited at roughly 15-20 requests/minute (community-measured; OpenCode does not publish RPM). Zen may return generic 429s without Retry-After / X-RateLimit headers; when Retry-After is omitted, opencodex adds a synthetic backoff hint (upstream Retry-After still wins). Distinct from the keyless opencode-free desktop quota (~200 Big Pickle/free-model requests per 5 hours). Docs: https://opencode.ai/docs/zen/. Free-model prompts may be retained for training — do not send confidential material.",
1102
+ note: "Keyed OpenCode Zen gateway. Free models on this tier are often short-window rate-limited at roughly 15-20 requests/minute (community-measured; OpenCode does not publish RPM). Zen may return generic 429s without Retry-After / X-RateLimit headers; when Retry-After is omitted, opencodex adds a synthetic backoff hint (upstream Retry-After still wins). Docs: https://opencode.ai/docs/zen/. Free-model prompts may be retained for training — do not send confidential material.",
1098
1103
  modelReasoningEfforts: Object.fromEntries(
1099
- [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekThinkingEffortsFor(id)]),
1104
+ [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_ZEN_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekThinkingEffortsFor(id)]),
1100
1105
  ),
1101
1106
  modelReasoningEffortMap: Object.fromEntries(
1102
- [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekReasoningMapFor(id)]),
1107
+ [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_ZEN_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekReasoningMapFor(id)]),
1103
1108
  ),
1104
- preserveReasoningContentModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
1105
- // Same Zen gateway as opencode-free: the DeepSeek vision preview id
1109
+ preserveReasoningContentModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_ZEN_FREE_DEEPSEEK_MODELS],
1110
+ // The DeepSeek vision preview id
1106
1111
  // (merges into deepseek-v4-flash later).
1107
1112
  modelContextWindows: {
1108
1113
  [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
@@ -1114,7 +1119,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1114
1119
  noVisionModels: [...OPENCODE_ZEN_TEXT_ONLY_MODELS, ...DEEPSEEK_GATEWAY_THINKING_MODELS],
1115
1120
  // Same DeepSeek routes as the Go preset above, behind the same vendor, so they carry
1116
1121
  // the same json_schema rejection (#1338 / #1415).
1117
- noJsonSchemaModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
1122
+ noJsonSchemaModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_ZEN_FREE_DEEPSEEK_MODELS],
1118
1123
  // Muse Spark on Zen can sit silent during prolonged reasoning and close without a protocol terminal.
1119
1124
  modelResponsesTerminalRepair: {
1120
1125
  "muse-spark-1.2-contributor-free": { graceMs: 5_000 },
@@ -1266,50 +1271,6 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1266
1271
  models: ["glm-5.3-flash", "claude-sonnet-5-5", "gpt-6.1-sol", "deepseek-v4-flash"],
1267
1272
  note: "OpenAI-compatible relay for Claude, GPT, GLM and DeepSeek chat models. The seeded models were verified with chat, streaming and tool calls; deepseek-v4.1-flash is listed upstream but currently answers 503 and is left out. An ordinary key only reaches its own routing group's models; a Smart key reaches all seeded models. Image and video models are not served through this Chat preset. Upstream latency varies widely (a few seconds to about 30 s) with occasional transient 503s.",
1268
1273
  },
1269
- {
1270
- id: "opencode-free",
1271
- label: "OpenCode Free",
1272
- adapter: "openai-chat",
1273
- baseUrl: "https://opencode.ai/zen/v1",
1274
- authKind: "key",
1275
- keyOptional: true,
1276
- featured: true,
1277
- liveModels: true,
1278
- note: "No key needed, but OpenCode now gates this tier to its own client: Zen refuses any request that arrives without an x-opencode-session header (error type MissingSessionID, \"OpenCode's free tier can only be used in OpenCode\"). opencodex does not mint that header or claim an OpenCode client identity, because no upstream contract authorizes a third-party agent to present itself as OpenCode. Until OpenCode publishes a third-party integration path for the keyless tier, use the keyed opencode-zen provider instead (https://opencode.ai/auth). Quota figures for when the tier admitted a request: OpenCode advertises about 200 Big Pickle/free-model requests per 5 hours, and the same Zen gateway can short-window rate-limit free models at roughly 15-20 requests/minute, and may return generic 429s without Retry-After (opencodex synthesizes backoff only when that header is omitted). Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
1279
- dashboardUrl: "https://opencode.ai",
1280
- staticHeaders: {
1281
- // Zen answers a bare runtime User-Agent (Bun/x.y.z) more aggressively than a client
1282
- // that identifies itself, which is what the 429 in #2067 traced to. The value is
1283
- // deliberately unversioned: a pinned "opencode-cli/<version>" is a claim about an
1284
- // install we do not have and goes stale on the vendor's schedule, not ours.
1285
- // Corroboration, not authority: OmniRoute — an independent open-source broker against
1286
- // the same Zen upstream — defaults to exactly this pair (userAgent "opencode", client
1287
- // "desktop") in open-sse/executors/opencode.ts, and got there by RETREATING from its
1288
- // own earlier "opencode-cli/1.0.0" pin. An operator can still override either value
1289
- // through the provider headers API; user headers win case-insensitively at route time.
1290
- "User-Agent": "opencode",
1291
- "x-opencode-client": "desktop",
1292
- },
1293
- modelReasoningEfforts: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
1294
- modelReasoningEffortMap: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
1295
- preserveReasoningContentModels: OPENCODE_FREE_DEEPSEEK_MODELS,
1296
- // The DeepSeek vision preview id is preemptive metadata for when Zen starts
1297
- // serving it (merges into v4-flash later).
1298
- modelContextWindows: {
1299
- [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
1300
- },
1301
- modelInputModalities: {
1302
- [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
1303
- ...Object.fromEntries(OPENCODE_ZEN_IMAGE_MODELS.map(id => [id, ["text", "image"] as string[]])),
1304
- },
1305
- // Same Zen roster behind the same base URL, so it carries the same measured
1306
- // text-only list rather than only its DeepSeek member (#1043).
1307
- noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
1308
- // Same reasoning: the free tier is the same Zen roster, so its DeepSeek members get
1309
- // the keyed tier's json_schema treatment and its reasoning contract rather than a
1310
- // narrower table that silently falls behind whenever the keyed one is updated.
1311
- noJsonSchemaModels: [...DEEPSEEK_GATEWAY_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
1312
- },
1313
1274
  // Xiaomi retires mimo-v2.5 and mimo-v2.5-pro on 2026-10-21 with no redirect
1314
1275
  // (https://mimo.mi.com/docs/en-US/updates/deprecate), so the first-party presets default to V2.6.
1315
1276
  // Saved defaults are not rewritten; V2.5 stays listed until it stops answering.
@@ -477,7 +477,7 @@ export const COMMAND_CODE_MODEL_INPUT_MODALITIES: Record<string, ["text"] | ["te
477
477
  ...Object.fromEntries(COMMAND_CODE_IMAGE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
478
478
  ...Object.fromEntries(COMMAND_CODE_TEXT_ONLY_MODELS.map(id => [id, ["text"] as ["text"]])),
479
479
  };
480
- export const OPENCODE_FREE_DEEPSEEK_MODELS = ["deepseek-v4-flash-free"];
480
+ export const OPENCODE_ZEN_FREE_DEEPSEEK_MODELS = ["deepseek-v4-flash-free"];
481
481
  /*
482
482
  * Zen free models that reject `image_url` upstream (#1043, and the reproducible
483
483
  * half of #1024).
@@ -49,8 +49,7 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
49
49
  * was only ever true for a freshly seeded config: `providerConfigSeed` copies the block once
50
50
  * (`derive.ts`), `enrichProviderFromCatalog` fills it only when the whole block is absent, and
51
51
  * nothing merged it at request time. So an install that predates a header — or that saved any
52
- * header of its own — never received the new one, which is exactly what #2067 would have
53
- * shipped for every existing opencode-free user.
52
+ * header of its own — never received the new one.
54
53
  *
55
54
  * The comparison is case-insensitive on purpose. HTTP header names are case-insensitive, but a
56
55
  * plain object spread is not: merging a registry `User-Agent` over a user's `user-agent`
@@ -134,7 +134,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
134
134
  const repairLegacyMimoFreeAuth = providerName === "mimo-free"
135
135
  && staticModelCatalog
136
136
  && (provider.authMode === undefined || provider.authMode === "local");
137
- const explicitKeyOverride = providerUsesKeyAuthOverride(registryEntry, provider, resolvedApiKey);
137
+ const explicitKeyOverride = providerUsesKeyAuthOverride(registryEntry, provider);
138
138
  const canonicalAuthMode = explicitKeyOverride
139
139
  ? "key"
140
140
  : repairLegacyMimoFreeAuth
@@ -39,7 +39,7 @@
39
39
  * Scope is deliberately one entry. The Go gateway's `deepseek-v4.1-flash` was declared text-only
40
40
  * from jawcode metadata and was measured natively multimodal on 2026-09-19 (see the note at that
41
41
  * entry in `registry/entries-core.ts`). Its sibling `deepseek-v4-flash` still rejects images
42
- * upstream, and the sibling Zen tiers (`opencode-zen`, `opencode-free`) could not be probed at
42
+ * upstream, and the keyed Zen tier (`opencode-zen`) could not be probed at
43
43
  * all — an unverified tier is not evidence, so neither is touched here.
44
44
  */
45
45
  import { PROVIDER_REGISTRY, providerMatchesRegistryTransport } from "./registry";
@@ -3,6 +3,7 @@ import type { OcxProviderConfig } from "../types";
3
3
  import { isEgressTransparentExecutor, markEgressTransparentExecutor } from "../lib/provider-egress";
4
4
  import { configuredOutboundFetch } from "../lib/proxy-env";
5
5
  import { resolveGithubCopilotTransport } from "./github-copilot-transport";
6
+ import { resolveMistralTransport } from "./mistral-transport";
6
7
 
7
8
  export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
8
9
 
@@ -121,7 +122,7 @@ export function resolveProviderTransport(
121
122
  if (providerName === "github-copilot") {
122
123
  return resolveGithubCopilotTransport(provider, apiBaseUrl);
123
124
  }
124
- if (providerName !== "xai") return provider;
125
+ if (providerName !== "xai") return resolveMistralTransport(provider);
125
126
 
126
127
  const cacheKey = promptCacheKey?.trim();
127
128
  const affinity = cacheKey ? deriveXaiConvId(cacheKey) : undefined;
@@ -0,0 +1,46 @@
1
+ import type { OcxAudioContent } from "../types";
2
+
3
+ export type ChatAudioFormat = OcxAudioContent["format"];
4
+
5
+ const MIME_FORMATS: Readonly<Record<string, ChatAudioFormat>> = {
6
+ "audio/mp3": "mp3",
7
+ "audio/mpeg": "mp3",
8
+ "audio/wav": "wav",
9
+ "audio/wave": "wav",
10
+ "audio/x-wav": "wav",
11
+ };
12
+
13
+ function normalizedFormat(value: unknown): ChatAudioFormat | undefined {
14
+ if (typeof value !== "string") return undefined;
15
+ const format = value.trim().toLowerCase();
16
+ return format === "mp3" || format === "wav" ? format : undefined;
17
+ }
18
+
19
+ function validBase64(value: string): boolean {
20
+ return value.length > 0
21
+ && value.length % 4 === 0
22
+ && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
23
+ }
24
+
25
+ /**
26
+ * Parse the inline audio shape Codex puts in a Responses content block. Chat Completions
27
+ * needs bare base64 plus `wav`/`mp3`, while Codex supplies an RFC 2397 data URL. A raw
28
+ * base64 value with an explicit supported format is accepted as well; remote URLs are not.
29
+ */
30
+ export function audioContentFromRaw(audioUrl: unknown, declaredFormat?: unknown): OcxAudioContent | undefined {
31
+ if (typeof audioUrl !== "string" || audioUrl.length === 0) return undefined;
32
+ const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/.exec(audioUrl);
33
+ let data: string;
34
+ let format: ChatAudioFormat | undefined;
35
+ if (dataUrl) {
36
+ const declared = normalizedFormat(declaredFormat);
37
+ const mime = dataUrl[1].trim().toLowerCase();
38
+ format = MIME_FORMATS[mime] ?? (mime === "application/octet-stream" ? declared : undefined);
39
+ data = dataUrl[2].replace(/\s+/g, "");
40
+ } else {
41
+ format = normalizedFormat(declaredFormat);
42
+ data = audioUrl.replace(/\s+/g, "");
43
+ }
44
+ if (!format || !validBase64(data)) return undefined;
45
+ return { type: "audio", data, format, text: `[audio: ${format}]` };
46
+ }
@@ -1,4 +1,5 @@
1
1
  import { isInlineDocumentDataUrl } from "./inline-document";
2
+ import { audioContentFromRaw } from "./audio";
2
3
 
3
4
  /** Input kinds for which the normalized request has no lossless content carrier. */
4
5
  export type UntranslatedInputMedia = "audio" | "file";
@@ -10,23 +11,35 @@ function isRecord(value: unknown): value is RecordValue {
10
11
  }
11
12
 
12
13
  /**
13
- * Whether the position being scanned has a converter that builds a document part.
14
+ * Whether the position being scanned has a converter that builds a lossless media part.
14
15
  *
15
- * Only user content does. A tool output, a system or assistant message, and a Chat `developer`
16
- * message are all flattened to text by their converters, so exempting an attachment there would
17
- * turn today's explicit refusal into the silent drop this scanner exists to prevent.
16
+ * User content carries documents and inline audio; developer content carries documents only.
17
+ * Tool output carries inline audio through the Chat adapter's follow-up user message. System and
18
+ * assistant content have no lossless translated media carrier, so exempting an attachment there
19
+ * would turn today's explicit refusal into the silent drop this scanner exists to prevent.
18
20
  */
19
- type CarrierPosition = "user-content" | "flattened";
21
+ type CarrierPosition = "user-content" | "developer-content" | "tool-output" | "flattened";
20
22
 
21
- function mediaKind(value: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
23
+ function mediaKind(value: unknown, position: CarrierPosition, allowAudio: boolean): UntranslatedInputMedia | undefined {
22
24
  if (!isRecord(value)) return undefined;
23
- if (value.type === "input_audio" || value.type === "audio") return "audio";
25
+ if (value.type === "input_audio" || value.type === "audio") {
26
+ // A Chat content part has a nested input_audio object, while Codex's Responses
27
+ // block uses audio_url. Only the latter is carried by the translated Chat adapter; the
28
+ // Chat-shaped `audio` spelling has no parser path and must never pass the guard.
29
+ return value.type === "input_audio"
30
+ && allowAudio
31
+ && (position === "user-content" || position === "tool-output")
32
+ && audioContentFromRaw(value.audio_url, value.format)
33
+ ? undefined
34
+ : "audio";
35
+ }
24
36
  if (value.type === "input_file" || value.type === "file" || value.type === "document") {
25
37
  // A document that carries its own bytes has a lossless carrier in user content, so refusing
26
38
  // it there would reject the very request #5212 exists to preserve. A reference with no
27
39
  // payload has none anywhere: translated adapters cannot dereference a file_id or a remote
28
40
  // source.
29
- return position === "user-content" && carriesInlineDocumentBytes(value) ? undefined : "file";
41
+ return (position === "user-content" || position === "developer-content")
42
+ && carriesInlineDocumentBytes(value) ? undefined : "file";
30
43
  }
31
44
  // A file-id-only image is not pixels: translated adapters cannot dereference it.
32
45
  if (value.type === "input_image" && typeof value.file_id === "string" && value.file_id.length > 0
@@ -46,10 +59,10 @@ function carriesInlineDocumentBytes(value: RecordValue): boolean {
46
59
  && value.source.data.length > 0;
47
60
  }
48
61
 
49
- function contentMedia(content: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
62
+ function contentMedia(content: unknown, position: CarrierPosition, allowAudio: boolean): UntranslatedInputMedia | undefined {
50
63
  if (!Array.isArray(content)) return undefined;
51
64
  for (const part of content) {
52
- const kind = mediaKind(part, position);
65
+ const kind = mediaKind(part, position, allowAudio);
53
66
  if (kind) return kind;
54
67
  }
55
68
  return undefined;
@@ -60,20 +73,29 @@ function contentMedia(content: unknown, position: CarrierPosition): Untranslated
60
73
  * arguments, schema properties, or arbitrary nested objects. No payload is copied,
61
74
  * decoded, fetched or included in the returned value.
62
75
  */
63
- export function untranslatedResponsesInputMedia(body: unknown): UntranslatedInputMedia | undefined {
76
+ export function untranslatedResponsesInputMedia(
77
+ body: unknown,
78
+ options: { allowAudio?: boolean } = {},
79
+ ): UntranslatedInputMedia | undefined {
64
80
  if (!isRecord(body) || !Array.isArray(body.input)) return undefined;
65
81
  for (const item of body.input) {
66
82
  if (!isRecord(item)) continue;
67
- const direct = mediaKind(item, "flattened");
83
+ const direct = mediaKind(item, "flattened", options.allowAudio === true);
68
84
  if (direct) return direct;
69
85
  if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
70
- const kind = contentMedia(item.output, "flattened");
86
+ const kind = contentMedia(item.output, "tool-output", options.allowAudio === true);
71
87
  if (kind) return kind;
72
88
  } else if (item.type === "message" || item.type === undefined) {
73
89
  // `inputContentParts` runs for user and developer messages only; a system message is
74
90
  // flattened to text by the parser.
75
91
  const role = item.role;
76
- const kind = contentMedia(item.content, role === "user" || role === "developer" ? "user-content" : "flattened");
92
+ const kind = contentMedia(
93
+ item.content,
94
+ role === "user" ? "user-content"
95
+ : role === "developer" ? "developer-content"
96
+ : "flattened",
97
+ options.allowAudio === true,
98
+ );
77
99
  if (kind) return kind;
78
100
  }
79
101
  }
@@ -87,7 +109,7 @@ export function untranslatedChatInputMedia(body: unknown): UntranslatedInputMedi
87
109
  if (!isRecord(message)) continue;
88
110
  // Only the `user` branch of the Chat projection builds content blocks. `system`,
89
111
  // `developer`, `assistant` and `tool` all reduce their content to a string.
90
- const kind = contentMedia(message.content, message.role === "user" ? "user-content" : "flattened");
112
+ const kind = contentMedia(message.content, message.role === "user" ? "user-content" : "flattened", false);
91
113
  if (kind) return kind;
92
114
  }
93
115
  return undefined;
@@ -1,4 +1,5 @@
1
1
  import type { OcxContentPart, OcxTextContent } from "../types";
2
+ import { audioContentFromRaw } from "./audio";
2
3
  import { inlineDocumentFromDataUrl } from "./inline-document";
3
4
 
4
5
  export function isObj(v: unknown): v is Record<string, unknown> {
@@ -65,23 +66,15 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
65
66
  const processing = nonEmptyString((block as { processing?: string }).processing);
66
67
  if (videoUrl) parts.push({ type: "video", videoUrl, ...(processing ? { processing } : {}) });
67
68
  } else if (block.type === "input_audio") {
68
- // Upstream Codex sends input_audio with an audio_url (codex-rs
69
- // protocol/src/models.rs). The IR has no audio carrier and no adapter consumes
70
- // one, so this part used to vanish with no trace at all.
71
- //
72
- // This records PRESENCE only and is NOT audio support: never the payload, which
73
- // is large base64 and would explode the token count, and never the URL, which
74
- // can carry a signed token. This parser must stay non-throwing — the native
75
- // Responses passthrough also runs through parseRequest before the adapter
76
- // forwards _rawBody, so refusing here would regress legitimate raw passthrough.
77
- //
78
- // No adapter refuses audio at its wire today: by this point the part is already a
79
- // text marker, so downstream adapters see text and continue. A typed unsupported-
80
- // modality carrier that survives to final adapter dispatch is a separate, recorded
81
- // residual — do not describe this branch as a refusal.
82
69
  const b = block as { audio_url?: string; format?: string };
83
- const format = safeAudioFormat(b.format);
84
- if (nonEmptyString(b.audio_url)) {
70
+ // Keep bytes only when they have a Chat-compatible inline form. Unsupported or
71
+ // remote references retain a marker for text-only wires; the final adapter guard
72
+ // refuses them rather than allowing a silent drop.
73
+ const audio = audioContentFromRaw(b.audio_url, b.format);
74
+ if (audio) {
75
+ parts.push(audio);
76
+ } else if (nonEmptyString(b.audio_url)) {
77
+ const format = safeAudioFormat(b.format);
85
78
  parts.push({ type: "text", text: format ? `[audio: ${format}]` : "[audio]" });
86
79
  }
87
80
  } else if (block.type === "input_file") {
@@ -104,7 +97,7 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
104
97
  // fabricating a "[file: ...]" marker for an attachment that was never sent.
105
98
  }
106
99
  }
107
- // Collapse to a plain string only for a single TEXT part; images must stay structured.
100
+ // Collapse to a plain string only for a single TEXT part; media must stay structured.
108
101
  if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
109
102
  return parts;
110
103
  }
@@ -129,15 +122,15 @@ export function outputTextOf(blocks: unknown): OcxTextContent[] {
129
122
  }
130
123
 
131
124
  /**
132
- * Tool-call output content. Preserves images (e.g. Codex `view_image` returns
133
- * `input_image` items): returns content parts when any image is present, else a plain joined string.
134
- * Never inlines an image_url as text (that would explode the token count).
125
+ * Tool-call output content. Preserves images (e.g. Codex `view_image` returns)
126
+ * and inline audio: returns content parts when either is present, else a plain joined string.
127
+ * Never inlines an image_url or audio payload as text (that would explode the token count).
135
128
  */
136
129
  export function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] {
137
130
  if (typeof output === "string") return output;
138
131
  if (!Array.isArray(output)) return "";
139
132
  const parts: OcxContentPart[] = [];
140
- let hasImage = false;
133
+ let hasMedia = false;
141
134
  for (const raw of output) {
142
135
  if (!isObj(raw)) continue;
143
136
  if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") {
@@ -149,15 +142,17 @@ export function outputToToolResultContent(output: string | unknown[] | undefined
149
142
  const fileId = nonEmptyString(raw.file_id);
150
143
  if (imageUrl) {
151
144
  parts.push({ type: "image", imageUrl, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) });
152
- hasImage = true;
145
+ hasMedia = true;
153
146
  } else if (fileId) {
154
147
  parts.push({ type: "text", text: `[image: ${fileId}]` });
155
148
  }
156
149
  } else if (raw.type === "input_audio") {
157
- // Same presence-only contract as the user-content branch above: Codex returns
158
- // audio in tool output too, and it previously disappeared without trace.
159
- const format = safeAudioFormat(raw.format);
160
- if (nonEmptyString(raw.audio_url)) {
150
+ const audio = audioContentFromRaw(raw.audio_url, raw.format);
151
+ if (audio) {
152
+ parts.push(audio);
153
+ hasMedia = true;
154
+ } else if (nonEmptyString(raw.audio_url)) {
155
+ const format = safeAudioFormat(raw.format);
161
156
  parts.push({ type: "text", text: format ? `[audio: ${format}]` : "[audio]" });
162
157
  }
163
158
  } else if (raw.type === "encrypted_content") {
@@ -165,7 +160,7 @@ export function outputToToolResultContent(output: string | unknown[] | undefined
165
160
  parts.push({ type: "text", text: "[encrypted content omitted]" });
166
161
  }
167
162
  }
168
- if (!hasImage) return parts.map(p => (p.type === "text" ? p.text : "")).join("");
163
+ if (!hasMedia) return parts.map(p => (p.type === "text" ? p.text : "")).join("");
169
164
  return parts;
170
165
  }
171
166
 
@@ -22,6 +22,7 @@ import { previousResponseReplayPrefixLength } from "./state";
22
22
  import { decodeReasoningEnvelope } from "./reasoning-envelope";
23
23
  import { hasRoutedIdentity, nameRoutedIdentity } from "../adapters/identity";
24
24
  import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
25
+ import { rewriteReplayedWebSearchCells } from "./web-search-replay";
25
26
  import { buildImageTool, extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
26
27
  import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
27
28
 
@@ -232,6 +233,13 @@ export function parseRequest(
232
233
  // Built before the item loop: a custom_tool_call echoed back in `input` needs the
233
234
  // namespace from the request's own tool catalog to survive the round trip.
234
235
  const customToolNamespacesByName = customToolNamespaces(data.tools);
236
+ // Stash the hosted web_search config (if Codex enabled it) so the proxy can run searches via the
237
+ // sidecar for routed providers. buildTools still drops the hosted tool; the sidecar path
238
+ // re-injects a synthetic function tool only when it will actually handle the call. The same
239
+ // declaration decides how replayed search cells are rebuilt below.
240
+ const webSearch = extractHostedWebSearch(data.tools as unknown[] | undefined);
241
+ // With search declared, replayed hosted cells become web_search call/output pairs (#7269).
242
+ const webSearchReplay = webSearch && Array.isArray(data.input) ? rewriteReplayedWebSearchCells(data.input) : undefined;
235
243
  const systemPrompt: string[] = [];
236
244
  // Responses reasoning siblings belong to the following assistant, including across call items.
237
245
  // Keep them off the message list until that assistant arrives; turn boundaries clear the array.
@@ -274,9 +282,12 @@ export function parseRequest(
274
282
  if (data.previous_response_id) continuationConversationMessageIndex = messages.length;
275
283
  messages.push({ role: "user", content: data.input, timestamp: now });
276
284
  } else if (data.input) {
277
- for (let inputIndex = 0; inputIndex < data.input.length; inputIndex++) {
278
- const item = data.input[inputIndex];
279
- if (inputIndex > 0 && isAnthropicProgressMirror(item, anthropicProgressEnvelope(data.input[inputIndex - 1]))) continue;
285
+ const inputItems = (webSearchReplay?.items ?? data.input) as typeof data.input;
286
+ for (let itemIndex = 0; itemIndex < inputItems.length; itemIndex++) {
287
+ const item = inputItems[itemIndex];
288
+ // Boundaries are measured on the caller's input, before any replayed-search rewrite.
289
+ const inputIndex = webSearchReplay?.sourceIndexes[itemIndex] ?? itemIndex;
290
+ if (itemIndex > 0 && isAnthropicProgressMirror(item, anthropicProgressEnvelope(inputItems[itemIndex - 1]))) continue;
280
291
  const effectiveType = (item as { type?: string }).type ?? ("role" in item ? "message" : undefined);
281
292
  const itemRole = (item as { role?: string }).role;
282
293
  const externalTaskInput = effectiveType === "function_call_output" ? externalTaskInputContent(item) : undefined;
@@ -529,9 +540,9 @@ export function parseRequest(
529
540
  }
530
541
 
531
542
  if (effectiveType === "web_search_call") {
532
- // Replayed hosted web-search evidence has no paired result payload that routed providers can
533
- // consume. Keep it out of assistant-visible text: the old marker was useful as an internal
534
- // loop hint, but when no sidecar is available the model can echo it as a fake answer.
543
+ // Without a search declaration the cell stays out of routed history: a call to an undeclared
544
+ // tool, or assistant-visible marker text the model could echo as a fake answer, would be
545
+ // worse than the gap. With one, the cell was already rewritten into a web_search pair.
535
546
  pendingReasoning.length = 0;
536
547
  continue;
537
548
  }
@@ -689,10 +700,6 @@ export function parseRequest(
689
700
  if (data.service_tier !== undefined) options.serviceTier = data.service_tier;
690
701
  if (data.prompt_cache_key !== undefined) options.promptCacheKey = data.prompt_cache_key;
691
702
 
692
- // Stash the hosted web_search config (if Codex enabled it) so the proxy can run searches via the
693
- // gpt-mini sidecar for routed providers. buildTools still drops the hosted tool; the sidecar path
694
- // re-injects a synthetic function tool only when it will actually handle the call.
695
- const webSearch = extractHostedWebSearch(data.tools as unknown[] | undefined);
696
703
  const imageGen = extractHostedImageGeneration([
697
704
  ...(data.tools as unknown[] ?? []),
698
705
  ...loadedToolSpecs,
@@ -718,6 +725,7 @@ export function parseRequest(
718
725
  ? { _continuationConversationMessageIndex: continuationConversationMessageIndex }
719
726
  : {}),
720
727
  ...(webSearch ? { _webSearch: webSearch } : {}),
728
+ ...(webSearchReplay ? { _replayedWebSearchCells: webSearchReplay.cells } : {}),
721
729
  ...(imageGen ? { _imageGeneration: imageGen } : {}),
722
730
  ...(textFormat ? { _structuredOutput: true } : {}),
723
731
  ...(compactionRequest ? { _compactionRequest: true } : {}),