@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -4,6 +4,7 @@ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxP
4
4
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
5
5
  import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
6
6
  import { collectResponsesToolGroups } from "../responses/tool-groups";
7
+ import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
7
8
  import { decodeServerSentEvents } from "../lib/sse-decoder";
8
9
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
9
10
  import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
@@ -32,7 +33,10 @@ export const FORWARD_HEADERS = [
32
33
  "x-responsesapi-include-timing-metrics",
33
34
  ];
34
35
 
35
- export function sanitizeReasoningInputContent(body: unknown): unknown {
36
+ export function sanitizeReasoningInputContent(
37
+ body: unknown,
38
+ opts?: { preserveRawReasoningContent?: boolean },
39
+ ): unknown {
36
40
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
37
41
  const raw = body as Record<string, unknown>;
38
42
  if (!Array.isArray(raw.input)) return body;
@@ -47,13 +51,23 @@ export function sanitizeReasoningInputContent(body: unknown): unknown {
47
51
  // backend cannot decrypt them and would reject the request. Strip regardless of content shape.
48
52
  const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
49
53
  if (!hasRawContent && !hasOcxEnvelope) return item;
50
- changed = true;
54
+ if (hasOcxEnvelope) {
55
+ changed = true;
56
+ const next: Record<string, unknown> = { ...rec };
57
+ delete next.encrypted_content;
58
+ if (!opts?.preserveRawReasoningContent) next.content = [];
59
+ return next;
60
+ }
51
61
  // Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
52
62
  // native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
53
63
  // `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
54
- const next: Record<string, unknown> = { ...rec, content: [] };
55
- if (hasOcxEnvelope) delete next.encrypted_content;
56
- return next;
64
+ // DeepSeek's Responses API instead ACCEPTS plaintext reasoning replay (its compatibility
65
+ // guide merges reasoning items into the adjacent assistant message), so providers flagged
66
+ // `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
67
+ // continuations after tool calls (issue #875 family).
68
+ if (opts?.preserveRawReasoningContent) return item;
69
+ changed = true;
70
+ return { ...rec, content: [] };
57
71
  });
58
72
 
59
73
  return changed ? { ...raw, input } : body;
@@ -148,18 +162,6 @@ function scrubOcxCompactionItems(body: unknown): unknown {
148
162
  return changed ? { ...body, input } : body;
149
163
  }
150
164
 
151
- /**
152
- * Hosted (OpenAI-executed) tool types that specific native slugs reject at request time. Codex
153
- * attaches these for app skills (e.g. `image_generation` for imagegen) regardless of the target
154
- * model, and the passthrough path forwards the raw body untouched — so a slug that doesn't support
155
- * the tool 400s (`Tool 'image_generation' is not supported with gpt-5.3-codex-spark.`). Each entry
156
- * maps a model-slug matcher to the hosted tool types that must be stripped before forwarding.
157
- * Extend this when another native slug rejects a hosted tool (e.g. `code_interpreter`).
158
- */
159
- const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
160
- { match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
161
- ];
162
-
163
165
  /**
164
166
  * Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark).
165
167
  * codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's
@@ -471,6 +473,33 @@ function toolOutputText(output: unknown): string {
471
473
  * reasoning chain is intact and must be preserved.
472
474
  * Runs on every forward request; with intact pairs it returns the original reference.
473
475
  */
476
+ /**
477
+ * Backfill `queries` on a replayed single-query `web_search_call`.
478
+ *
479
+ * `webSearchAction()` in the bridge now emits both keys, but that only helps items
480
+ * created after the fix. A conversation that already recorded
481
+ * `{type:"search", query:"..."}` replays that stored item on every subsequent turn, and
482
+ * DeepSeek's native Responses parser requires `queries` — so upgrading alone leaves
483
+ * those threads permanently 400ing with `missing field 'queries'` (#930).
484
+ *
485
+ * Runs on every Responses request, on both `input` items and the `action` nested inside
486
+ * them. Returns the original reference when nothing needs repair, so the common path
487
+ * allocates nothing.
488
+ */
489
+ function backfillWebSearchQueries(body: unknown): unknown {
490
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
491
+ let changed = false;
492
+ const input = body.input.map(item => {
493
+ if (!isPlainObject(item) || item.type !== "web_search_call") return item;
494
+ const action = item.action;
495
+ if (!isPlainObject(action) || action.type !== "search") return item;
496
+ if (typeof action.query !== "string" || Array.isArray(action.queries)) return item;
497
+ changed = true;
498
+ return { ...item, action: { ...action, queries: [action.query] } };
499
+ });
500
+ return changed ? { ...body, input } : body;
501
+ }
502
+
474
503
  function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknown {
475
504
  if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
476
505
  const input = body.input;
@@ -608,6 +637,128 @@ function declaresImageGenClientTool(tool: unknown): boolean {
608
637
  return isImageGenClientName(tool.name);
609
638
  }
610
639
 
640
+ /** Rewrite client image-gen selectors to the hosted tool without widening caller restrictions. */
641
+ function preferHostedImageGenToolChoice(toolChoice: unknown): unknown {
642
+ if (!isPlainObject(toolChoice)) return toolChoice;
643
+ if ((toolChoice.type === "function" || toolChoice.type === "custom") && typeof toolChoice.name === "string") {
644
+ return isImageGenClientName(toolChoice.name) ? { type: HOSTED_IMAGE_GENERATION_TOOL } : toolChoice;
645
+ }
646
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
647
+ const hasHostedImageTool = toolChoice.tools.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
648
+ let changed = false;
649
+ let addedHostedImageTool = false;
650
+ const tools: unknown[] = [];
651
+ for (const tool of toolChoice.tools) {
652
+ const isClientImageTool = isPlainObject(tool)
653
+ && (tool.type === "function" || tool.type === "custom")
654
+ && typeof tool.name === "string"
655
+ && isImageGenClientName(tool.name);
656
+ if (!isClientImageTool) {
657
+ tools.push(tool);
658
+ continue;
659
+ }
660
+ changed = true;
661
+ if (!hasHostedImageTool && !addedHostedImageTool) {
662
+ tools.push({ type: HOSTED_IMAGE_GENERATION_TOOL });
663
+ addedHostedImageTool = true;
664
+ }
665
+ }
666
+ return changed ? { ...toolChoice, tools } : toolChoice;
667
+ }
668
+
669
+ /**
670
+ * Some Responses-compatible gateways reserve the hosted image namespace even when the request
671
+ * does not explicitly declare `image_generation`. For an explicitly configured model, remove only
672
+ * colliding client declarations so the gateway's hosted tool can take precedence.
673
+ */
674
+ function preferConfiguredHostedTools(
675
+ body: unknown,
676
+ provider: OcxProviderConfig,
677
+ modelId: string,
678
+ selectedModelId?: string,
679
+ ): unknown {
680
+ // A virtual model's advertised id takes precedence over its resolved wire-model id.
681
+ // Read own properties only: a routed model id of `constructor`/`toString` would
682
+ // otherwise resolve to an inherited Object.prototype function and throw on the
683
+ // membership test below, failing the request before it is dispatched.
684
+ const preferenceMap = provider.modelPreferHostedTools;
685
+ const ownPreference = (key: string | undefined): string[] | undefined => {
686
+ if (!key || !preferenceMap || !Object.prototype.hasOwnProperty.call(preferenceMap, key)) return undefined;
687
+ const entry = preferenceMap[key];
688
+ return Array.isArray(entry) ? entry : undefined;
689
+ };
690
+ const preferredTools = ownPreference(selectedModelId) ?? ownPreference(modelId);
691
+ if (!preferredTools?.includes(HOSTED_IMAGE_GENERATION_TOOL) || !isPlainObject(body)) return body;
692
+
693
+ const stripGroup = (tools: unknown[]): unknown[] => {
694
+ const filtered = tools.filter(tool => !declaresImageGenClientTool(tool));
695
+ return filtered.length === tools.length ? tools : filtered;
696
+ };
697
+
698
+ let changed = false;
699
+ let tools = body.tools;
700
+ let strippedTopLevelImageGenTool = false;
701
+ if (Array.isArray(body.tools)) {
702
+ tools = stripGroup(body.tools);
703
+ strippedTopLevelImageGenTool = tools !== body.tools;
704
+ changed ||= strippedTopLevelImageGenTool;
705
+ }
706
+
707
+ let input = body.input;
708
+ const strippedAdditionalToolsIndices = new Set<number>();
709
+ if (Array.isArray(body.input)) {
710
+ let nestedChanged = false;
711
+ const mappedInput = body.input.map((item, index) => {
712
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
713
+ const nestedTools = stripGroup(item.tools);
714
+ if (nestedTools === item.tools) return item;
715
+ strippedAdditionalToolsIndices.add(index);
716
+ nestedChanged = true;
717
+ return { ...item, tools: nestedTools };
718
+ });
719
+ if (nestedChanged) {
720
+ input = mappedInput;
721
+ changed = true;
722
+ }
723
+ }
724
+
725
+ const hasToolChoice = Object.hasOwn(body, "tool_choice");
726
+ const toolChoice = hasToolChoice ? preferHostedImageGenToolChoice(body.tool_choice) : body.tool_choice;
727
+ const toolChoiceChanged = hasToolChoice && toolChoice !== body.tool_choice;
728
+ const hasHostedImageGenTool = (toolGroup: unknown): boolean => Array.isArray(toolGroup)
729
+ && toolGroup.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
730
+ const hasHostedImageGenDeclaration = hasHostedImageGenTool(tools)
731
+ || (Array.isArray(input) && input.some(item => isPlainObject(item)
732
+ && item.type === "additional_tools"
733
+ && hasHostedImageGenTool(item.tools)));
734
+ if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
735
+ if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
736
+ tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
737
+ } else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
738
+ // Restore into the FIRST stripped container only. Tool declarations are
739
+ // request-scoped, not container-scoped — the containers are separate carriers for
740
+ // one tool set, so a single hosted declaration covers the request. An earlier
741
+ // revision restored into every stripped container and put `image_generation` on
742
+ // the wire twice; review caught it.
743
+ const firstStripped = Math.min(...strippedAdditionalToolsIndices);
744
+ input = input.map((item, index) => index === firstStripped
745
+ && isPlainObject(item)
746
+ && Array.isArray(item.tools)
747
+ ? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
748
+ : item);
749
+ }
750
+ }
751
+ changed ||= toolChoiceChanged;
752
+ if (!changed) return body;
753
+ const next: Record<string, unknown> = {
754
+ ...body,
755
+ ...(Array.isArray(body.tools) ? { tools } : {}),
756
+ ...(Array.isArray(body.input) ? { input } : {}),
757
+ };
758
+ if (toolChoiceChanged) next.tool_choice = toolChoice;
759
+ return next;
760
+ }
761
+
611
762
  /**
612
763
  * Lower one complete Codex image-gen namespace to public Responses function tools.
613
764
  *
@@ -866,13 +1017,9 @@ function normalizeImageGenClientTools(body: unknown): unknown {
866
1017
  function stripUnsupportedHostedTools(body: unknown): unknown {
867
1018
  if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
868
1019
  const model = typeof body.model === "string" ? body.model : "";
869
- const unsupported = UNSUPPORTED_HOSTED_TOOLS.filter(e => e.match(model));
870
- if (unsupported.length === 0) return body;
871
-
872
1020
  const tools = body.tools.filter(t => {
873
1021
  const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
874
- if (!type) return true;
875
- return !unsupported.some(e => e.tools.has(type));
1022
+ return !type || !isHostedToolUnsupportedForModel(model, type);
876
1023
  });
877
1024
  return tools.length === body.tools.length ? body : { ...body, tools };
878
1025
  }
@@ -1012,19 +1159,30 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1012
1159
  }
1013
1160
  if (forward) {
1014
1161
  outBody = stripUnsupportedForwardParams(outBody);
1162
+ } else {
1163
+ outBody = preferConfiguredHostedTools(
1164
+ outBody,
1165
+ provider,
1166
+ parsed.modelId,
1167
+ parsed._openAiVirtualSelectedModelId,
1168
+ );
1169
+ outBody = normalizeImageGenClientTools(outBody);
1015
1170
  }
1016
- else outBody = normalizeImageGenClientTools(outBody);
1017
1171
  if (forward || parsed._previousResponseInputExpanded === true) {
1018
1172
  outBody = repairOversizedReplayCallIds(outBody);
1019
1173
  }
1020
1174
  outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
1175
+ // Repair stored history from before the bridge emitted both keys: a conversation
1176
+ // that already recorded a single-query web_search_call replays it every turn, and
1177
+ // a strict parser rejects the whole request over it (#930).
1178
+ outBody = backfillWebSearchQueries(outBody);
1021
1179
  // Same predicate as the routedCompaction gate in handleResponses(): an
1022
1180
  // authMode check would let a noncanonical custom forward provider skip this
1023
1181
  // rewrite while the server still routes it as a summarizer turn (#422).
1024
1182
  if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
1025
1183
  outBody = buildRoutedCompactionBody(outBody);
1026
1184
  }
1027
- const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))));
1185
+ const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true })))))));
1028
1186
  const body = JSON.stringify(stripDisabledReasoningSummaries(
1029
1187
  normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
1030
1188
  provider,