@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -8,6 +8,15 @@
8
8
  //
9
9
  // This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
10
10
  // repair the one faithful reading, leave genuine patch content alone.
11
+ //
12
+ // One accepted exception, added 2026-09-05: a body that is ITSELF one complete
13
+ // operation-bearing envelope, submitted as the `exec` body. That shape is never valid
14
+ // JavaScript — `*** Begin Patch` fails to parse at the leading `**` — so it has zero
15
+ // executable readings and exactly one faithful one. `isCompletePatchEnvelope` recognizes
16
+ // it and the CALLERS retarget it to the apply_patch helper; the repair functions here are
17
+ // unchanged and still return every exec body byte-identical. JavaScript that merely
18
+ // mentions an envelope keeps a real executable reading and is left alone.
19
+ // See devlog/_plan/260905_apply_patch_envelope_gap.
11
20
 
12
21
  const PATCH_BEGIN = "*** Begin Patch";
13
22
  const PATCH_END = "*** End Patch";
@@ -43,6 +52,43 @@ export function normalizeApplyPatchDelimiters(text: string): string {
43
52
  return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
44
53
  }
45
54
 
55
+ /**
56
+ * True when a body is one COMPLETE top-level patch envelope carrying a real file
57
+ * operation.
58
+ *
59
+ * A code-mode `exec` body is JavaScript, and this shape is never valid JavaScript: a
60
+ * leading `*** Begin Patch` fails to parse at the `**`. So a body satisfying this
61
+ * predicate cannot be a program the caller meant to run, which is what makes reading it
62
+ * as an apply_patch call the one faithful reading rather than a guess.
63
+ *
64
+ * Deliberately strict, and reusing the same two checks `normalizeApplyPatchDelimiters`
65
+ * uses: a second, looser notion of "looks like a patch" is how a repair boundary drifts.
66
+ * JavaScript that merely CONTAINS an envelope in a string, template, or comment fails the
67
+ * anchored match and is left alone.
68
+ */
69
+ export function isCompletePatchEnvelope(text: string): boolean {
70
+ const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
71
+ if (!match) return false;
72
+ return PATCH_OPERATION_LINE.test(match[3] ?? "");
73
+ }
74
+
75
+ /**
76
+ * True when a PARTIAL buffer could still grow into a complete patch envelope.
77
+ *
78
+ * Streaming decides per delta, before the body is complete, while
79
+ * `isCompletePatchEnvelope` can only answer once it is. Without this the bridge would
80
+ * stream raw envelope bytes and then replace them with compiled helper JavaScript at
81
+ * completion — the rewind that path explicitly refuses to perform.
82
+ *
83
+ * Deliberately one-sided: it holds a buffer that MIGHT become an envelope and never
84
+ * claims one will. A held buffer that turns out to be something else still reaches the
85
+ * client in the authoritative completed item; only the live preview is skipped.
86
+ */
87
+ export function mayBecomePatchEnvelope(text: string): boolean {
88
+ if (text === "") return false;
89
+ return text.startsWith(PATCH_BEGIN) || PATCH_BEGIN.startsWith(text);
90
+ }
91
+
46
92
  /**
47
93
  * Repair freeform input before Codex sees it.
48
94
  *
@@ -1,4 +1,9 @@
1
- import { normalizeApplyPatchDelimiters } from "./apply-patch-envelope";
1
+ import {
2
+ isCompletePatchEnvelope,
3
+ normalizeApplyPatchDelimiters,
4
+ unwrapFreeformToolInput,
5
+ } from "./apply-patch-envelope";
6
+ import { declaresCodeModeExec } from "../types/tools";
2
7
 
3
8
  function isPlainObject(value: unknown): value is Record<string, unknown> {
4
9
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -51,3 +56,36 @@ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: str
51
56
  }
52
57
  return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
53
58
  }
59
+
60
+ /**
61
+ * Resolve the effective code-mode helper for one freeform call.
62
+ *
63
+ * `codeModeHelperName` already covers the NAME-based case: a provider emitted
64
+ * `apply_patch` under a declared `exec` catalog, so `normalizeDeclaredToolName` rewrote
65
+ * the name and recorded the original. That decision happens at tool-call start, before
66
+ * any arguments exist, so it cannot see a provider that got the NAME right and the BODY
67
+ * wrong.
68
+ *
69
+ * This adds that second case: the name is already `exec` so nothing was rewritten, but
70
+ * the body is a complete patch envelope and therefore cannot be the JavaScript that
71
+ * `exec` runs. Same inference the name-based path makes, drawn from the payload.
72
+ *
73
+ * Returns undefined for everything else, including JavaScript that merely mentions a
74
+ * patch envelope — that body is a real program and is forwarded byte-identical.
75
+ */
76
+ export function resolveCodeModeHelperName(
77
+ codeModeHelperName: string | undefined,
78
+ toolName: string,
79
+ argumentsText: unknown,
80
+ namespace?: string,
81
+ declaredNames?: ReadonlySet<string>,
82
+ ): string | undefined {
83
+ if (codeModeHelperName) return codeModeHelperName;
84
+ if (toolName !== "exec" || namespace !== undefined) return undefined;
85
+ // `exec` is a name, not a guarantee. Without a catalog that is genuinely code mode, a
86
+ // caller-defined `exec` could legitimately take patch text, and handing it generated
87
+ // `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
88
+ if (!declaresCodeModeExec(declaredNames)) return undefined;
89
+ if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
90
+ return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText)) ? "apply_patch" : undefined;
91
+ }
@@ -4,7 +4,7 @@ import {
4
4
  repairFreeformToolInput,
5
5
  unwrapFreeformToolInput,
6
6
  } from "./apply-patch-envelope";
7
- import { compileCodeModeHelperInput } from "./code-mode-helper-compat";
7
+ import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "./code-mode-helper-compat";
8
8
  import { collectResponsesToolGroups } from "./tool-groups";
9
9
 
10
10
  const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
@@ -281,17 +281,23 @@ export function restoreRoutedCustomCalls(
281
281
  ) {
282
282
  const sourceInput = item.type === "function_call" ? item.arguments : item.input;
283
283
  const aliased = targetName !== wireName;
284
+ const itemNamespace = typeof item.namespace === "string" ? item.namespace : undefined;
285
+ // Name-based alias first; otherwise let a raw patch envelope submitted as the `exec`
286
+ // body resolve to the same apply_patch helper (devlog/_plan/260905_apply_patch_envelope_gap).
287
+ const helper = aliased && sourceInput !== ""
288
+ ? item.name
289
+ : resolveCodeModeHelperName(undefined, targetName, sourceInput, itemNamespace, declaredNames);
284
290
  const restored: Record<string, unknown> = {
285
291
  ...item,
286
292
  type: "custom_tool_call",
287
293
  id: customToolItemId(item.id),
288
294
  name: aliased ? targetName : item.name,
289
- input: aliased && sourceInput !== ""
290
- ? compileCodeModeHelperInput(sourceInput, item.name)
295
+ input: helper
296
+ ? compileCodeModeHelperInput(sourceInput, helper)
291
297
  : repairFreeformToolInput(
292
298
  sourceInput,
293
299
  targetName,
294
- typeof item.namespace === "string" ? item.namespace : undefined,
300
+ itemNamespace,
295
301
  ),
296
302
  };
297
303
  delete restored.arguments;
@@ -1,9 +1,17 @@
1
- /** Hosted tools rejected by specific native model slugs. */
2
- const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
1
+ /** Hosted tools rejected by specific native model slugs or exact provider destinations. */
2
+ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
3
+ match: (model: string, baseUrl?: string) => boolean;
4
+ tools: ReadonlySet<string>;
5
+ }> = [
3
6
  { match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
7
+ {
8
+ match: (model, baseUrl) => model === "grok-4.6"
9
+ && baseUrl?.replace(/\/+$/, "") === "https://opencode.ai/zen/go/v1",
10
+ tools: new Set(["web_search", "web_search_preview"]),
11
+ },
4
12
  ];
5
13
 
6
14
  /** True when forwarding this hosted tool to the model would be rejected upstream. */
7
- export function isHostedToolUnsupportedForModel(modelId: string, tool: string): boolean {
8
- return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId) && entry.tools.has(tool));
15
+ export function isHostedToolUnsupportedForModel(modelId: string, tool: string, baseUrl?: string): boolean {
16
+ return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId, baseUrl) && entry.tools.has(tool));
9
17
  }
@@ -0,0 +1,133 @@
1
+ import type { OcxContentPart, OcxTextContent } from "../types";
2
+
3
+ export function isObj(v: unknown): v is Record<string, unknown> {
4
+ return typeof v === "object" && v !== null && !Array.isArray(v);
5
+ }
6
+
7
+ type InputBlock =
8
+ | { type: "input_text"; text: string }
9
+ | { type: "text"; text: string }
10
+ | { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
11
+ | { type: "input_video"; video_url?: string }
12
+ | { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
13
+
14
+ /** A usable reference string, or undefined. Empty strings and non-strings are not references. */
15
+ function nonEmptyString(value: unknown): string | undefined {
16
+ return typeof value === "string" && value.length > 0 ? value : undefined;
17
+ }
18
+
19
+ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
20
+ if (typeof blocks === "string") return blocks;
21
+ // The catch-all can also hand back a non-array `content` (an object, a number), which would
22
+ // throw at the loop below before any per-block guard runs.
23
+ if (!Array.isArray(blocks)) return [];
24
+ const parts: OcxContentPart[] = [];
25
+ for (const raw of blocks) {
26
+ // A malformed message item fails its strict schema and falls through to inputItemSchema's
27
+ // permissive catch-all, so blocks reaching here are NOT guaranteed to match the declared
28
+ // shape. Validate each field before use, as outputToToolResultContent already does.
29
+ if (!isObj(raw)) continue;
30
+ const block = raw as InputBlock;
31
+ if (block.type === "input_text" || block.type === "text") {
32
+ if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
33
+ } else if (block.type === "input_image") {
34
+ const b = block as { image_url?: string; file_id?: string; detail?: string };
35
+ const imageUrl = nonEmptyString(b.image_url);
36
+ const fileId = nonEmptyString(b.file_id);
37
+ const detail = nonEmptyString(b.detail);
38
+ if (imageUrl) {
39
+ // Preserve the image as a structured part — adapters send it as a native image block.
40
+ // NEVER inline the (often base64 data-URL) image_url as text: that explodes the token count.
41
+ parts.push({ type: "image", imageUrl, ...(detail ? { detail: normalizeImageDetail(detail) } : {}) });
42
+ } else if (fileId) {
43
+ parts.push({ type: "text", text: `[image: ${fileId}]` }); // file_id ref → no inline data
44
+ }
45
+ // No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
46
+ // the request never carried, which is worse than dropping malformed input.
47
+ } else if (block.type === "input_video") {
48
+ const videoUrl = nonEmptyString(block.video_url);
49
+ if (videoUrl) parts.push({ type: "video", videoUrl });
50
+ } else if (block.type === "input_file") {
51
+ const b = block as { file_id?: string; filename?: string; file_data?: string };
52
+ const fileId = nonEmptyString(b.file_id);
53
+ const fileData = nonEmptyString(b.file_data);
54
+ const filename = nonEmptyString(b.filename);
55
+ if (fileId) {
56
+ parts.push({ type: "text", text: `[file: ${fileId}]` });
57
+ } else if (fileData) {
58
+ // Inline file_data is often large base64. Preserve only its presence and name, never bytes.
59
+ parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
60
+ }
61
+ // A bare filename is not a file resource in the Responses schema, so omit it rather than
62
+ // fabricating a "[file: ...]" marker for an attachment that was never sent.
63
+ }
64
+ }
65
+ // Collapse to a plain string only for a single TEXT part; images must stay structured.
66
+ if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
67
+ return parts;
68
+ }
69
+
70
+ type OutputBlock = { type: "output_text"; text: string } | { type: "text"; text: string } | { type: "refusal"; refusal: string };
71
+
72
+ export function outputTextOf(blocks: unknown): OcxTextContent[] {
73
+ if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : [];
74
+ if (!Array.isArray(blocks)) return [];
75
+ const out: OcxTextContent[] = [];
76
+ for (const raw of blocks) {
77
+ // Same catch-all caveat as inputContentParts: validate before use.
78
+ if (!isObj(raw)) continue;
79
+ const b = raw as OutputBlock;
80
+ if (b.type === "output_text" || b.type === "text") {
81
+ if (typeof raw.text === "string") out.push({ type: "text", text: raw.text });
82
+ } else if (b.type === "refusal") {
83
+ if (typeof raw.refusal === "string") out.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /**
90
+ * Tool-call output content. Preserves images (e.g. Codex `view_image` returns
91
+ * `input_image` items): returns content parts when any image is present, else a plain joined string.
92
+ * Never inlines an image_url as text (that would explode the token count).
93
+ */
94
+ export function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] {
95
+ if (typeof output === "string") return output;
96
+ if (!Array.isArray(output)) return "";
97
+ const parts: OcxContentPart[] = [];
98
+ let hasImage = false;
99
+ for (const raw of output) {
100
+ if (!isObj(raw)) continue;
101
+ if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") {
102
+ if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
103
+ } else if (raw.type === "refusal" && typeof raw.refusal === "string") {
104
+ parts.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
105
+ } else if (raw.type === "input_image") {
106
+ const imageUrl = nonEmptyString(raw.image_url);
107
+ const fileId = nonEmptyString(raw.file_id);
108
+ if (imageUrl) {
109
+ parts.push({ type: "image", imageUrl, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) });
110
+ hasImage = true;
111
+ } else if (fileId) {
112
+ parts.push({ type: "text", text: `[image: ${fileId}]` });
113
+ }
114
+ } else if (raw.type === "encrypted_content") {
115
+ // codex-rs FunctionCallOutputContentItem::EncryptedContent — opaque to routed models.
116
+ parts.push({ type: "text", text: "[encrypted content omitted]" });
117
+ }
118
+ }
119
+ if (!hasImage) return parts.map(p => (p.type === "text" ? p.text : "")).join("");
120
+ return parts;
121
+ }
122
+
123
+ export function toolOutputContainsEncryptedContent(output: string | unknown[] | undefined): boolean {
124
+ return Array.isArray(output) && output.some(raw => isObj(raw) && raw.type === "encrypted_content");
125
+ }
126
+
127
+ /**
128
+ * codex-rs ImageDetail allows "original", but chat-completions providers only accept
129
+ * auto|low|high on image_url.detail — degrade "original" to "high" (the codex default).
130
+ */
131
+ function normalizeImageDetail(detail: string): string {
132
+ return detail === "original" ? "high" : detail;
133
+ }
@@ -0,0 +1,24 @@
1
+ import type { OcxRequestOptions } from "../types";
2
+ import { isObj } from "./parser-content";
3
+
4
+ /**
5
+ * The Responses `text.format` object when it requests structured output (json_schema or
6
+ * json_object), undefined otherwise. Acceptance is identical to the boolean detector this
7
+ * replaces; unknown or malformed formats are ignored, never rejected, so the native
8
+ * passthrough keeps forwarding whatever the caller sent via `_rawBody`.
9
+ */
10
+ export function parseTextFormat(text: unknown): OcxRequestOptions["textFormat"] {
11
+ if (!isObj(text)) return undefined;
12
+ const format = (text as { format?: unknown }).format;
13
+ if (!isObj(format)) return undefined;
14
+ const f = format as { type?: unknown; name?: unknown; description?: unknown; schema?: unknown; strict?: unknown };
15
+ if (f.type === "json_object") return { type: "json_object" };
16
+ if (f.type !== "json_schema") return undefined;
17
+ return {
18
+ type: "json_schema",
19
+ ...(typeof f.name === "string" ? { name: f.name } : {}),
20
+ ...(typeof f.description === "string" ? { description: f.description } : {}),
21
+ ...(isObj(f.schema) ? { schema: f.schema as Record<string, unknown> } : {}),
22
+ ...(typeof f.strict === "boolean" ? { strict: f.strict } : {}),
23
+ };
24
+ }
@@ -0,0 +1,188 @@
1
+ import type { OcxRequestOptions, OcxTool } from "../types";
2
+ import { isObj } from "./parser-content";
3
+ import { WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
4
+ import { buildImageTool, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
5
+ import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
6
+
7
+ export function mapToolChoice(value: unknown): OcxRequestOptions["toolChoice"] {
8
+ if (value === undefined || value === null) return undefined;
9
+ if (value === "auto" || value === "none" || value === "required") return value;
10
+ if (isObj(value) && "type" in value) {
11
+ const t = (value as { type: string }).type;
12
+ if ((t === "function" || t === "custom") && "name" in value) {
13
+ return { name: (value as { name: string }).name };
14
+ }
15
+ // Hosted image tool types (with or without a name) map to the synthetic image_gen wire name.
16
+ if (t === "image_generation" || t === "image_gen") {
17
+ return { name: IMAGE_GEN_TOOL_NAME };
18
+ }
19
+ if (t === "allowed_tools" && Array.isArray(value.tools)) {
20
+ const names = value.tools
21
+ .map(allowedToolName)
22
+ .filter((name): name is string => Boolean(name));
23
+ return names.length > 0
24
+ ? { allowedTools: [...new Set(names)], mode: value.mode === "required" ? "required" : "auto" }
25
+ : "none";
26
+ }
27
+ return "auto";
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ function allowedToolName(tool: unknown): string | undefined {
33
+ if (!isObj(tool)) return undefined;
34
+ if (typeof tool.name === "string" && tool.name.length > 0) return tool.name;
35
+ if (tool.type === "web_search" || tool.type === "web_search_preview") return WEB_SEARCH_TOOL_NAME;
36
+ if (tool.type === "image_generation" || tool.type === "image_gen") return IMAGE_GEN_TOOL_NAME;
37
+ if (tool.type === "tool_search") return "tool_search";
38
+ return undefined;
39
+ }
40
+
41
+ export function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
42
+ if (!tools) return undefined;
43
+ const out: OcxTool[] = [];
44
+ const normalizeParameters = (raw: unknown): Record<string, unknown> => {
45
+ if (isObj(raw) && raw.type === "object") return raw;
46
+ return { ...(isObj(raw) ? raw : {}), type: "object" };
47
+ };
48
+ const pushFn = (t: Record<string, unknown>, namespace?: string) => {
49
+ // Hosted image_generation already installed the synthetic root tool. A later
50
+ // ordinary root `image_gen` must not create a second un-namespaced identity.
51
+ if (
52
+ !namespace
53
+ && t.name === IMAGE_GEN_TOOL_NAME
54
+ && out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration)
55
+ ) {
56
+ return;
57
+ }
58
+ const tool: OcxTool = {
59
+ name: t.name as string,
60
+ description: (t.description as string) ?? "",
61
+ parameters: normalizeParameters(t.parameters),
62
+ };
63
+ if (t.strict !== undefined) tool.strict = t.strict as boolean;
64
+ if (namespace) tool.namespace = namespace;
65
+ out.push(tool);
66
+ };
67
+ const pushCustom = (t: Record<string, unknown>, namespace?: string) => {
68
+ // Hosted image_generation already installed the synthetic root tool. A later
69
+ // root custom `image_gen` would collide on the same wire name with a different
70
+ // `freeform` flag and throw `ambiguous tool catalog`.
71
+ if (
72
+ !namespace
73
+ && t.name === IMAGE_GEN_TOOL_NAME
74
+ && out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration)
75
+ ) {
76
+ return;
77
+ }
78
+ // Freeform custom tools are lowered to a single string `input` because chat models cannot
79
+ // emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the
80
+ // tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches
81
+ // routed models that the nested helper name is itself a callable top-level tool.
82
+ const inputDescription = t.name === "apply_patch"
83
+ ? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope."
84
+ : "Raw freeform input for this tool.";
85
+ const tool: OcxTool = {
86
+ name: t.name as string,
87
+ description: (t.description as string) ?? "",
88
+ parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] },
89
+ freeform: true,
90
+ };
91
+ if (namespace) tool.namespace = namespace;
92
+ out.push(tool);
93
+ };
94
+ for (const t of tools) {
95
+ if (!isObj(t)) continue;
96
+ if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) {
97
+ pushFn(t.function as Record<string, unknown>);
98
+ continue;
99
+ }
100
+ if (t.type === "function" && typeof t.name === "string") {
101
+ pushFn(t);
102
+ } else if (t.type === "namespace" && Array.isArray(t.tools)) {
103
+ // Codex 0.147 groups its ordinary client tools under the reserved `functions` namespace,
104
+ // including freeform custom tools such as code-mode `exec`. Those children are still
105
+ // top-level Responses tools, so flatten them without a namespace. Other namespace groups
106
+ // are MCP-style and keep their namespace for round-trip routing.
107
+ const builtinFunctions = t.name === "functions";
108
+ const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined;
109
+ for (const inner of t.tools as unknown[]) {
110
+ if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns);
111
+ else if (isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner, ns);
112
+ }
113
+ }
114
+ else if (t.type === "custom" && typeof t.name === "string") {
115
+ pushCustom(t);
116
+ }
117
+ else if (t.type === "tool_search") {
118
+ // Client-executed tool discovery — the gateway to deferred tools (subagents, extra MCP tools).
119
+ // Expose as a function so chat models can call it; the bridge relays it as a tool_search_call.
120
+ out.push({
121
+ name: "tool_search",
122
+ description: toolSearchDescription(t),
123
+ parameters: normalizeParameters(toolSearchParameters(t)),
124
+ toolSearch: true,
125
+ });
126
+ }
127
+ else if (t.type === "image_generation" || t.type === "image_gen") {
128
+ // Keep Codex's image_gen visible to routed chat models. The hosted OpenAI tool
129
+ // cannot execute on Grok; the model still has to see a callable image_gen so
130
+ // Codex's client-side /v1/images request can fire and be relayed to xAI.
131
+ // Identity is the un-namespaced synthetic root (`imageGeneration: true`), not
132
+ // the bare name: a namespaced ordinary `image_gen` must not suppress it.
133
+ const synthetic = buildImageTool();
134
+ // Every un-namespaced `image_gen` collides on one wire name, so removing only
135
+ // the first leaves a second root behind and the catalog stays ambiguous.
136
+ // Drop all root collisions, keep namespaced entries, then insert exactly one
137
+ // synthetic root — at the earliest colliding position so declaration order is
138
+ // preserved for models that read the catalog positionally.
139
+ let insertAt = -1;
140
+ for (let i = out.length - 1; i >= 0; i -= 1) {
141
+ const tool = out[i]!;
142
+ if (tool.name !== IMAGE_GEN_TOOL_NAME || tool.namespace) continue;
143
+ out.splice(i, 1);
144
+ insertAt = i;
145
+ }
146
+ if (insertAt >= 0) out.splice(insertAt, 0, synthetic);
147
+ else out.push(synthetic);
148
+ }
149
+ else if (typeof t.name === "string" && t.type !== "web_search" && t.type !== "image_generation") {
150
+ // Any OTHER named tool (e.g. a native/computer-use tool type opencodex doesn't explicitly
151
+ // model) is client-executed — pass it through as a function so the routed model can read and
152
+ // call it naturally; the bridge relays its call as a function_call. Previously such tools were
153
+ // silently dropped, so the model never saw them.
154
+ pushFn(t);
155
+ }
156
+ // Hosted web_search is still dropped here — the web-search sidecar re-injects it.
157
+ }
158
+ return out.length > 0 ? out : undefined;
159
+ }
160
+
161
+ /**
162
+ * Namespace a custom tool was declared under, by its bare name.
163
+ *
164
+ * A `custom_tool_call` echoed back by the client carries only the bare name — the bridge
165
+ * emits `{"type":"custom_tool_call","name":"exec"}` even when the tool was declared as
166
+ * `mcp__functions__exec`. Without this lookup the namespace is lost on the return trip,
167
+ * and the adapters replay history through `namespacedToolName(namespace, name)`, which
168
+ * then produces a bare `exec` the provider may not have. Ordinary `function_call` items
169
+ * do not need this: they carry `namespace` on the wire.
170
+ */
171
+ export function customToolNamespaces(tools: unknown): Map<string, string> {
172
+ const out = new Map<string, string>();
173
+ if (!Array.isArray(tools)) return out;
174
+ for (const spec of tools) {
175
+ if (!isObj(spec) || spec.type !== "namespace" || !Array.isArray(spec.tools)) continue;
176
+ const namespace = typeof spec.name === "string" ? spec.name : undefined;
177
+ // Codex 0.147 groups ordinary client tools under the reserved `functions` namespace and
178
+ // buildTools deliberately flattens those without a namespace. Mirror that here, or the
179
+ // reconstruction would invent a namespace the request never advertised.
180
+ if (!namespace || namespace === "functions") continue;
181
+ for (const inner of spec.tools) {
182
+ if (!isObj(inner) || inner.type !== "custom" || typeof inner.name !== "string") continue;
183
+ // Ambiguous bare names are already rejected upstream, so first declaration wins.
184
+ if (!out.has(inner.name)) out.set(inner.name, namespace);
185
+ }
186
+ }
187
+ return out;
188
+ }