@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906

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 (260) 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-VGeQEZ_v.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 +54 -24
  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/identity.ts +8 -2
  26. package/src/adapters/kiro-calibration.ts +181 -0
  27. package/src/adapters/kiro.ts +135 -3
  28. package/src/adapters/openai-responses.ts +259 -29
  29. package/src/adapters/responses-code-mode.ts +59 -0
  30. package/src/adapters/tool-catalog-nudge.ts +1 -1
  31. package/src/adapters/xai-schema-analysis.ts +86 -0
  32. package/src/adapters/xai-tool-schema.ts +2 -87
  33. package/src/adapters/xai-web-search.ts +1 -1
  34. package/src/bridge.ts +47 -13
  35. package/src/chat/inbound.ts +11 -3
  36. package/src/claude/inbound-content-options.ts +60 -0
  37. package/src/claude/inbound-model-options.ts +142 -0
  38. package/src/claude/inbound-records.ts +7 -0
  39. package/src/claude/inbound.ts +10 -202
  40. package/src/claude/model-info.ts +45 -0
  41. package/src/cli/account-auth.ts +49 -9
  42. package/src/cli/account-extended.ts +7 -1
  43. package/src/cli/capabilities.ts +15 -4
  44. package/src/cli/claude.ts +232 -39
  45. package/src/cli/config-command.ts +9 -1
  46. package/src/cli/dispatch.ts +5 -1
  47. package/src/cli/doctor.ts +10 -0
  48. package/src/cli/effort.ts +372 -0
  49. package/src/cli/export-command.ts +3 -9
  50. package/src/cli/help.ts +1 -0
  51. package/src/cli/index.ts +13 -0
  52. package/src/cli/init.ts +4 -0
  53. package/src/cli/model-selection-guidance.ts +30 -0
  54. package/src/cli/models-runtime.ts +3 -2
  55. package/src/cli/models.ts +8 -3
  56. package/src/cli/observe.ts +3 -1
  57. package/src/cli/opencode.ts +4 -1
  58. package/src/cli/provider-runtime.ts +65 -0
  59. package/src/cli/provider.ts +8 -0
  60. package/src/cli/registry.ts +16 -2
  61. package/src/cli/runtime-api.ts +3 -1
  62. package/src/cli/star-prompt.ts +22 -6
  63. package/src/cli/status-probes.ts +168 -0
  64. package/src/cli/status.ts +5 -168
  65. package/src/clients/config-export/constants.ts +69 -0
  66. package/src/clients/config-export/contracts.ts +154 -0
  67. package/src/clients/config-export/dsh.ts +132 -0
  68. package/src/clients/config-export/fast-models.ts +29 -0
  69. package/src/clients/config-export/mcode.ts +83 -0
  70. package/src/clients/config-export/model-metadata.ts +108 -0
  71. package/src/clients/config-export/omp.ts +104 -0
  72. package/src/clients/config-export/zcode.ts +92 -0
  73. package/src/clients/config-export.ts +18 -710
  74. package/src/codex/account-lifecycle.ts +20 -3
  75. package/src/codex/account-usability.ts +2 -0
  76. package/src/codex/auth-api.ts +272 -32
  77. package/src/codex/auth-context.ts +328 -24
  78. package/src/codex/catalog/effort.ts +44 -5
  79. package/src/codex/catalog/metadata.ts +149 -14
  80. package/src/codex/catalog/native-models.ts +116 -4
  81. package/src/codex/catalog/parsing.ts +122 -8
  82. package/src/codex/catalog/provider-fetch.ts +154 -23
  83. package/src/codex/catalog/reserve.ts +52 -0
  84. package/src/codex/catalog/sync.ts +89 -16
  85. package/src/codex/catalog.ts +1 -1
  86. package/src/codex/convergence-types.ts +1 -0
  87. package/src/codex/convergence.ts +2 -0
  88. package/src/codex/data/upstream-models.json +169 -0
  89. package/src/codex/forward-transport-headers.ts +25 -0
  90. package/src/codex/inject.ts +99 -34
  91. package/src/codex/injected-marker.ts +30 -4
  92. package/src/codex/journal.ts +14 -0
  93. package/src/codex/legacy-config-keys.ts +68 -0
  94. package/src/codex/log-guard/inspect-schema.ts +137 -0
  95. package/src/codex/log-guard/inspect.ts +2 -134
  96. package/src/codex/loopback-target.ts +54 -0
  97. package/src/codex/main-account-cache.ts +63 -1
  98. package/src/codex/main-account-hard-lock.ts +52 -0
  99. package/src/codex/main-account.ts +3 -1
  100. package/src/codex/management-convergence.ts +3 -0
  101. package/src/codex/model-entitlements.ts +54 -4
  102. package/src/codex/project-config-warnings.ts +92 -2
  103. package/src/codex/prompt-layers/encoding.ts +80 -0
  104. package/src/codex/prompt-layers/paths.ts +54 -0
  105. package/src/codex/prompt-layers/revision.ts +55 -0
  106. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  107. package/src/codex/prompt-layers/toml-read.ts +181 -0
  108. package/src/codex/prompt-layers.ts +14 -520
  109. package/src/codex/quota-auto-refresh-state.ts +16 -0
  110. package/src/codex/quota-auto-refresh.ts +219 -0
  111. package/src/codex/quota-types.ts +51 -0
  112. package/src/codex/quota.ts +252 -93
  113. package/src/codex/reserve-availability.ts +177 -0
  114. package/src/codex/routing.ts +28 -9
  115. package/src/codex/shim.ts +53 -11
  116. package/src/codex/subagent-model-fallback.ts +23 -3
  117. package/src/combos/failover.ts +125 -7
  118. package/src/combos/identifiers.ts +89 -0
  119. package/src/combos/index.ts +4 -0
  120. package/src/combos/resolve.ts +80 -9
  121. package/src/combos/types.ts +20 -93
  122. package/src/config/subagent-models.ts +24 -0
  123. package/src/config.ts +156 -13
  124. package/src/generated/compatibility-version.json +474 -178
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/integrations/journal.ts +65 -4
  127. package/src/integrations/store.ts +5 -0
  128. package/src/lab/events/limits.ts +4 -0
  129. package/src/lib/destination-policy.ts +31 -2
  130. package/src/lib/errors.ts +39 -0
  131. package/src/lib/provider-outbound.ts +69 -3
  132. package/src/lib/proxy-env.ts +22 -0
  133. package/src/lib/redact-folding.ts +176 -0
  134. package/src/lib/redact.ts +2 -175
  135. package/src/lib/state-store-sweeper.ts +20 -6
  136. package/src/lib/token-estimate.ts +94 -27
  137. package/src/lib/windows-user-principal.ts +53 -5
  138. package/src/oauth/account-quota-rank.ts +40 -1
  139. package/src/oauth/anthropic-routing.ts +99 -3
  140. package/src/oauth/chatgpt-device.ts +187 -0
  141. package/src/oauth/chatgpt.ts +31 -4
  142. package/src/oauth/generic-account-failover.ts +36 -13
  143. package/src/oauth/index.ts +140 -29
  144. package/src/oauth/log.ts +3 -0
  145. package/src/oauth/login-cli.ts +5 -0
  146. package/src/oauth/meta-muse.ts +117 -15
  147. package/src/oauth/pool-settings-capability.ts +15 -4
  148. package/src/providers/api-keys.ts +8 -10
  149. package/src/providers/default-aliases.ts +39 -0
  150. package/src/providers/derive.ts +10 -2
  151. package/src/providers/fastwire.ts +36 -7
  152. package/src/providers/initial-model-selection-runtime.ts +90 -0
  153. package/src/providers/initial-model-selection.ts +120 -0
  154. package/src/providers/key-failover.ts +134 -54
  155. package/src/providers/key-store.ts +11 -1
  156. package/src/providers/label.ts +1 -1
  157. package/src/providers/model-discovery.ts +76 -0
  158. package/src/providers/model-rename-startup.ts +72 -8
  159. package/src/providers/muse-subscription-usage.ts +95 -0
  160. package/src/providers/openai-sidecar.ts +17 -5
  161. package/src/providers/openai-tiers-destination.ts +102 -0
  162. package/src/providers/openai-tiers.ts +2 -99
  163. package/src/providers/opencode-go-transport.ts +41 -0
  164. package/src/providers/quota-key-accounts.ts +141 -0
  165. package/src/providers/quota-types.ts +9 -0
  166. package/src/providers/quota.ts +625 -98
  167. package/src/providers/registry.ts +60 -17
  168. package/src/providers/xai-responses-opt-in.ts +31 -5
  169. package/src/quota/reset-activation.ts +81 -0
  170. package/src/quota/reset-detector.ts +305 -0
  171. package/src/quota/reset-notify-config.ts +162 -0
  172. package/src/quota/reset-observer.ts +125 -0
  173. package/src/quota/reset-poller.ts +160 -0
  174. package/src/quota/reset-seen-store.ts +385 -0
  175. package/src/quota/reset-sinks.ts +199 -0
  176. package/src/quota/window-mapping.ts +106 -0
  177. package/src/responses/apply-patch-envelope.ts +46 -0
  178. package/src/responses/code-mode-helper-compat.ts +39 -1
  179. package/src/responses/custom-tool-compat.ts +10 -4
  180. package/src/responses/hosted-tool-policy.ts +12 -4
  181. package/src/responses/parser-content.ts +133 -0
  182. package/src/responses/parser-text-format.ts +24 -0
  183. package/src/responses/parser-tools.ts +188 -0
  184. package/src/responses/parser.ts +3 -326
  185. package/src/responses/state.ts +124 -28
  186. package/src/router.ts +48 -13
  187. package/src/routing/analytics.ts +1 -0
  188. package/src/routing/capability.ts +17 -4
  189. package/src/server/auth-cors.ts +7 -1
  190. package/src/server/background-lifecycle.ts +23 -1
  191. package/src/server/chat-completions.ts +25 -3
  192. package/src/server/claude-messages.ts +62 -5
  193. package/src/server/effort-row.ts +1 -1
  194. package/src/server/fast-row.ts +295 -0
  195. package/src/server/gui-static.ts +30 -4
  196. package/src/server/index.ts +122 -28
  197. package/src/server/live.ts +18 -4
  198. package/src/server/management/agent-settings-routes.ts +2 -2
  199. package/src/server/management/combo-routes.ts +37 -9
  200. package/src/server/management/config-routes.ts +93 -2
  201. package/src/server/management/integration-routes.ts +108 -0
  202. package/src/server/management/model-routes.ts +13 -3
  203. package/src/server/management/model-rows.ts +20 -1
  204. package/src/server/management/native-integration-routes.ts +4 -1
  205. package/src/server/management/oauth-account-routes.ts +45 -10
  206. package/src/server/management/provider-routes.ts +34 -3
  207. package/src/server/management/quota-reset-routes.ts +57 -0
  208. package/src/server/management/route-registry.ts +7 -4
  209. package/src/server/management/shared.ts +19 -5
  210. package/src/server/management/system-routes.ts +3 -2
  211. package/src/server/management-api.ts +14 -2
  212. package/src/server/ports.ts +12 -2
  213. package/src/server/relay-eager.ts +38 -23
  214. package/src/server/relay.ts +4 -0
  215. package/src/server/request-log.ts +6 -0
  216. package/src/server/responses/codex-ws-correlation.ts +65 -0
  217. package/src/server/responses/codex-ws-exchange.ts +261 -0
  218. package/src/server/responses/codex-ws-metadata.ts +134 -0
  219. package/src/server/responses/codex-ws-pool.ts +162 -0
  220. package/src/server/responses/codex-ws-request.ts +87 -0
  221. package/src/server/responses/codex-ws-session.ts +93 -0
  222. package/src/server/responses/codex-ws-wire.ts +144 -0
  223. package/src/server/responses/collaboration.ts +41 -1
  224. package/src/server/responses/compact.ts +105 -12
  225. package/src/server/responses/core.ts +510 -57
  226. package/src/server/responses/empty-completion-guard.ts +4 -0
  227. package/src/server/responses/fetch-helpers.ts +10 -3
  228. package/src/server/responses/input-admission.ts +16 -9
  229. package/src/server/responses/responses-field-backfill.ts +1 -1
  230. package/src/server/responses/ws-upstream.ts +34 -318
  231. package/src/server/responses-custom-tool-repair.ts +20 -4
  232. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  233. package/src/server/safe-response-headers.ts +23 -0
  234. package/src/server/search.ts +9 -0
  235. package/src/server/subagent-models-startup.ts +27 -0
  236. package/src/server/system-env-shell.ts +238 -0
  237. package/src/server/system-env.ts +7 -234
  238. package/src/server/ws-bridge.ts +3 -25
  239. package/src/server/xai-responses-startup.ts +21 -0
  240. package/src/service-manager-probe.ts +1 -1
  241. package/src/service.ts +55 -16
  242. package/src/types/config.ts +108 -12
  243. package/src/types/provider.ts +36 -7
  244. package/src/types/request.ts +8 -0
  245. package/src/types/tools.ts +26 -1
  246. package/src/types.ts +2 -0
  247. package/src/update/notify.ts +8 -2
  248. package/src/usage/cost.ts +38 -28
  249. package/src/usage/expected-prices.ts +34 -15
  250. package/src/usage/log.ts +2 -0
  251. package/src/usage/model-identity.ts +26 -0
  252. package/src/usage/summary.ts +15 -1
  253. package/src/vision/describe.ts +6 -0
  254. package/src/vision/image-rewrite.ts +108 -0
  255. package/src/vision/index.ts +19 -306
  256. package/src/vision/plan.ts +205 -0
  257. package/src/web-search/executor.ts +6 -0
  258. package/src/web-search/index.ts +8 -1
  259. package/gui/dist/assets/index-B2YjLA-i.css +0 -1
  260. package/gui/dist/assets/index-aPup8CKb.js +0 -112
@@ -0,0 +1,59 @@
1
+ import { toolChoiceToolPredicate, type OcxParsedRequest, type OcxProviderConfig } from "../types";
2
+ import { isOpenAiOperatedResponsesDestination } from "../providers/openai-tiers";
3
+ import { CODE_MODE_RESULT_ECHO_SENTENCE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize";
4
+ import { isBareShellBridgeTool, isCodexCodeModeExecTool } from "./tool-catalog-nudge";
5
+
6
+ function record(value: unknown): value is Record<string, unknown> {
7
+ return !!value && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+
10
+ /** Inspect the whole result, not just its empty header: later text or media is real output. */
11
+ function textOnlyOutput(output: unknown): string | undefined {
12
+ if (typeof output === "string") return output;
13
+ if (!Array.isArray(output)) return undefined;
14
+ if (!output.every(part => record(part)
15
+ && ["text", "input_text", "output_text"].includes(String(part.type))
16
+ && typeof part.text === "string")) return undefined;
17
+ return output.map(part => part.text).join("\n");
18
+ }
19
+
20
+ function withExecInputGuidance(tool: unknown): unknown {
21
+ if (!record(tool) || tool.type !== "function" || tool.name !== "exec" || tool.namespace !== undefined) return tool;
22
+ if (!record(tool.parameters) || !record(tool.parameters.properties) || !record(tool.parameters.properties.input)) return tool;
23
+ return { ...tool, parameters: { ...tool.parameters, properties: {
24
+ ...tool.parameters.properties,
25
+ input: {
26
+ ...tool.parameters.properties.input,
27
+ description: `JavaScript source for unified exec; do not provide a bare shell command. ${CODE_MODE_RESULT_ECHO_SENTENCE}`,
28
+ },
29
+ } } };
30
+ }
31
+
32
+ /** Native routed Responses needs the same first-call/output contract as translated adapters. */
33
+ export function normalizeResponsesCodeMode(body: unknown, parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown {
34
+ if (!record(body) || parsed._compactionRequest || isOpenAiOperatedResponsesDestination(provider)) return body;
35
+ const visible = parsed.context.tools?.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
36
+ if (!visible?.some(isCodexCodeModeExecTool) || visible.some(isBareShellBridgeTool)) return body;
37
+ const instructions = typeof body.instructions === "string" ? body.instructions : "";
38
+ const input = Array.isArray(body.input) ? body.input : undefined;
39
+ const execCalls = new Set(input?.filter(item => record(item)
40
+ && (item.type === "function_call" || item.type === "custom_tool_call")
41
+ && item.name === "exec" && item.namespace === undefined && typeof item.call_id === "string")
42
+ .map(item => item.call_id));
43
+ return {
44
+ ...body,
45
+ instructions: instructions.includes(CODE_MODE_RESULT_ECHO_SENTENCE)
46
+ ? instructions : [instructions, CODE_MODE_RESULT_ECHO_SENTENCE].filter(Boolean).join("\n\n"),
47
+ ...(Array.isArray(body.tools) ? { tools: body.tools.map(withExecInputGuidance) } : {}),
48
+ ...(input ? { input: input.map(item => {
49
+ if (!record(item)) return item;
50
+ if (item.type === "additional_tools" && Array.isArray(item.tools)) {
51
+ return { ...item, tools: item.tools.map(withExecInputGuidance) };
52
+ }
53
+ if ((item.type !== "function_call_output" && item.type !== "custom_tool_call_output") || !execCalls.has(item.call_id)) return item;
54
+ const text = textOnlyOutput(item.output);
55
+ const normalized = text === undefined ? undefined : normalizeEmptyExecToolResultText(text, { toolName: "exec" });
56
+ return normalized === undefined ? item : { ...item, output: normalized };
57
+ }) } : {}),
58
+ };
59
+ }
@@ -121,7 +121,7 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
121
121
  "Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
122
122
  "Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
123
123
  verifiedCodeModeExecName
124
- ? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names. " + CODE_MODE_RESULT_ECHO_SENTENCE + " Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch` (no trailing `***` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated `*** Begin Patch ***` envelope is rejected by Codex before the file is touched."
124
+ ? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names. " + CODE_MODE_RESULT_ECHO_SENTENCE + " Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched."
125
125
  : "If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
126
126
  unavailableNeighborNames.length > 0
127
127
  ? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."
@@ -0,0 +1,86 @@
1
+ export function isSchemaObject(value: unknown): value is Record<string, unknown> {
2
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+
5
+ function decodeJsonPointerToken(token: string): string {
6
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
7
+ }
8
+
9
+ /** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */
10
+ export function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
11
+ if (ref === "#" || ref === "#/") return root;
12
+ if (!ref.startsWith("#/")) return undefined;
13
+ let current: unknown = root;
14
+ for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
15
+ if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined;
16
+ current = current[token];
17
+ }
18
+ return current;
19
+ }
20
+
21
+ /** Values a schema pins through `const`/`enum`, or undefined when it pins none. */
22
+ function xaiLiteralValues(schema: unknown): unknown[] | undefined {
23
+ if (!isSchemaObject(schema)) return undefined;
24
+ if (Object.hasOwn(schema, "const")) return [schema.const];
25
+ if (Array.isArray(schema.enum)) return schema.enum;
26
+ return undefined;
27
+ }
28
+
29
+ /** JSON type name for a literal, so it can be compared against a `type` keyword. */
30
+ function xaiJsonTypeOf(value: unknown): string {
31
+ if (value === null) return "null";
32
+ if (Array.isArray(value)) return "array";
33
+ if (typeof value === "string") return "string";
34
+ if (typeof value === "boolean") return "boolean";
35
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
36
+ return "object";
37
+ }
38
+
39
+ /** Types a schema declares, or undefined when it constrains none. */
40
+ function xaiDeclaredTypes(schema: unknown): Set<string> | undefined {
41
+ if (!isSchemaObject(schema)) return undefined;
42
+ const type = schema.type;
43
+ if (typeof type === "string") return new Set([type]);
44
+ if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]);
45
+ return undefined;
46
+ }
47
+
48
+ /** `integer` is a subset of `number`, so those two names overlap rather than exclude. */
49
+ function xaiTypesOverlap(left: string, right: string): boolean {
50
+ if (left === right) return true;
51
+ return (left === "integer" && right === "number") || (left === "number" && right === "integer");
52
+ }
53
+
54
+ /**
55
+ * Conservative mutual-exclusion test: true only when no instance can satisfy both schemas.
56
+ * Proof comes from disjoint literal sets or disjoint declared types; anything it cannot prove
57
+ * is reported as overlapping so the caller refuses the merge instead of widening the schema.
58
+ */
59
+ function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean {
60
+ const leftValues = xaiLiteralValues(left);
61
+ const rightValues = xaiLiteralValues(right);
62
+ if (leftValues && rightValues) {
63
+ const seen = new Set(rightValues.map(value => JSON.stringify(value)));
64
+ return leftValues.every(value => !seen.has(JSON.stringify(value)));
65
+ }
66
+ const leftTypes = xaiDeclaredTypes(left);
67
+ const rightTypes = xaiDeclaredTypes(right);
68
+ const literalsExcludedByTypes = (values: unknown[], types: Set<string>): boolean =>
69
+ values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type)));
70
+ if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes);
71
+ if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes);
72
+ if (leftTypes && rightTypes) {
73
+ return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType)));
74
+ }
75
+ return false;
76
+ }
77
+
78
+ /** Every pair provably disjoint, so a union over them accepts each instance exactly once. */
79
+ export function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean {
80
+ for (let i = 0; i < schemas.length; i += 1) {
81
+ for (let j = i + 1; j < schemas.length; j += 1) {
82
+ if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
@@ -1,8 +1,6 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
-
3
- function isSchemaObject(value: unknown): value is Record<string, unknown> {
4
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
5
- }
2
+ export { lookupLocalJsonPointer } from "./xai-schema-analysis";
3
+ import { isSchemaObject, lookupLocalJsonPointer, xaiSchemasArePairwiseDisjoint } from "./xai-schema-analysis";
6
4
 
7
5
  export function isXaiSchemaTarget(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
8
6
  try {
@@ -58,22 +56,6 @@ function createXaiSchemaBudget(): XaiSchemaBudget {
58
56
  return { remainingNodes: XAI_MAX_SCHEMA_NODES, remainingVariants: XAI_MAX_ROOT_VARIANTS };
59
57
  }
60
58
 
61
- function decodeJsonPointerToken(token: string): string {
62
- return token.replace(/~1/g, "/").replace(/~0/g, "~");
63
- }
64
-
65
- /** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */
66
- export function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
67
- if (ref === "#" || ref === "#/") return root;
68
- if (!ref.startsWith("#/")) return undefined;
69
- let current: unknown = root;
70
- for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
71
- if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined;
72
- current = current[token];
73
- }
74
- return current;
75
- }
76
-
77
59
  /** Resolve local `#/` `$ref`s. Unresolvable, cyclic, or over-budget refs return undefined. */
78
60
  function resolveXaiSchemaRefs(
79
61
  schema: unknown,
@@ -168,73 +150,6 @@ function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
168
150
  return serialized.every(value => value === serialized[0]);
169
151
  }
170
152
 
171
- /** Values a schema pins through `const`/`enum`, or undefined when it pins none. */
172
- function xaiLiteralValues(schema: unknown): unknown[] | undefined {
173
- if (!isSchemaObject(schema)) return undefined;
174
- if (Object.hasOwn(schema, "const")) return [schema.const];
175
- if (Array.isArray(schema.enum)) return schema.enum;
176
- return undefined;
177
- }
178
-
179
- /** JSON type name for a literal, so it can be compared against a `type` keyword. */
180
- function xaiJsonTypeOf(value: unknown): string {
181
- if (value === null) return "null";
182
- if (Array.isArray(value)) return "array";
183
- if (typeof value === "string") return "string";
184
- if (typeof value === "boolean") return "boolean";
185
- if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
186
- return "object";
187
- }
188
-
189
- /** Types a schema declares, or undefined when it constrains none. */
190
- function xaiDeclaredTypes(schema: unknown): Set<string> | undefined {
191
- if (!isSchemaObject(schema)) return undefined;
192
- const type = schema.type;
193
- if (typeof type === "string") return new Set([type]);
194
- if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]);
195
- return undefined;
196
- }
197
-
198
- /** `integer` is a subset of `number`, so those two names overlap rather than exclude. */
199
- function xaiTypesOverlap(left: string, right: string): boolean {
200
- if (left === right) return true;
201
- return (left === "integer" && right === "number") || (left === "number" && right === "integer");
202
- }
203
-
204
- /**
205
- * Conservative mutual-exclusion test: true only when no instance can satisfy both schemas.
206
- * Proof comes from disjoint literal sets or disjoint declared types; anything it cannot prove
207
- * is reported as overlapping so the caller refuses the merge instead of widening the schema.
208
- */
209
- function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean {
210
- const leftValues = xaiLiteralValues(left);
211
- const rightValues = xaiLiteralValues(right);
212
- if (leftValues && rightValues) {
213
- const seen = new Set(rightValues.map(value => JSON.stringify(value)));
214
- return leftValues.every(value => !seen.has(JSON.stringify(value)));
215
- }
216
- const leftTypes = xaiDeclaredTypes(left);
217
- const rightTypes = xaiDeclaredTypes(right);
218
- const literalsExcludedByTypes = (values: unknown[], types: Set<string>): boolean =>
219
- values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type)));
220
- if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes);
221
- if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes);
222
- if (leftTypes && rightTypes) {
223
- return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType)));
224
- }
225
- return false;
226
- }
227
-
228
- /** Every pair provably disjoint, so a union over them accepts each instance exactly once. */
229
- function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean {
230
- for (let i = 0; i < schemas.length; i += 1) {
231
- for (let j = i + 1; j < schemas.length; j += 1) {
232
- if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false;
233
- }
234
- }
235
- return true;
236
- }
237
-
238
153
  /** Deduplicate schemas by serialized shape, preserving first-seen order. */
239
154
  function uniqueXaiSchemas(values: unknown[]): unknown[] {
240
155
  const unique: unknown[] = [];
@@ -55,7 +55,7 @@ function normalizeToolGroup(tools: unknown[]): ToolGroupRewrite {
55
55
  // `search_context_size` 400s, while `user_location`, `search_content_types`, `filters` and
56
56
  // `enable_image_search` are all accepted. Deleting the accepted ones was a silent capability
57
57
  // loss, and it contradicted the sibling layer, whose own probe note already records
58
- // user_location/filters as accepted (tests/responses-routed-web-search-fields.test.ts).
58
+ // user_location/filters as accepted (tests/responses/responses-routed-web-search-fields.test.ts).
59
59
  delete next.external_web_access;
60
60
  delete next.search_context_size;
61
61
  if (enableImageSearch && !Object.hasOwn(next, "enable_image_search")) {
package/src/bridge.ts CHANGED
@@ -16,9 +16,9 @@ import {
16
16
  type OcxErrorPayload,
17
17
  } from "./lib/errors";
18
18
  import { redactSecretString } from "./lib/redact";
19
- import { repairFreeformToolInput } from "./responses/apply-patch-envelope";
19
+ import { mayBecomePatchEnvelope, repairFreeformToolInput } from "./responses/apply-patch-envelope";
20
20
  import { encodeCompactionSummary } from "./responses/compaction";
21
- import { compileCodeModeHelperInput } from "./responses/code-mode-helper-compat";
21
+ import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "./responses/code-mode-helper-compat";
22
22
  import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
23
23
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
24
24
  import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
@@ -33,7 +33,7 @@ import {
33
33
  stripCitationMarkers,
34
34
  type CitationMarkerFilter,
35
35
  } from "./responses/citation-markers";
36
- import { normalizeDeclaredToolName } from "./types";
36
+ import { declaresCodeModeExec, normalizeDeclaredToolName } from "./types";
37
37
  import { usageDisplayTotalTokens } from "./usage/totals";
38
38
  import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
39
39
  import {
@@ -59,6 +59,10 @@ function sseEvent(name: string, data: Record<string, unknown>): string {
59
59
  return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
60
60
  }
61
61
 
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return value !== null && typeof value === "object" && !Array.isArray(value);
64
+ }
65
+
62
66
  function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
63
67
  // input_tokens_details / output_tokens_details are ALWAYS emitted (zero defaults):
64
68
  // strict Responses clients deserialize them as required fields — grok-build's pinned
@@ -80,7 +84,24 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
80
84
  const inputTokens = usage.contextTotalTokens !== undefined
81
85
  ? Math.max(0, usage.contextTotalTokens - usage.outputTokens)
82
86
  : usage.inputTokens;
87
+ // openai/codex#41980 parity: unknown upstream usage fields (subscription metadata, future
88
+ // counters) pass through the rebuild. Normalized values stay authoritative for the known
89
+ // keys (they are derived from the same raw values, so this never disagrees with upstream).
90
+ const raw: Record<string, unknown> = usage.rawUsage ?? {};
91
+ // cache_write_tokens is a KNOWN key: it is emitted only from the validated normalized
92
+ // value below, never copied through raw (an unknown-shaped value must not leak into the
93
+ // normalized contract).
94
+ const rawInputDetails = isRecord(raw.input_tokens_details)
95
+ ? Object.fromEntries(Object.entries(raw.input_tokens_details as Record<string, unknown>)
96
+ .filter(([key]) => key !== "cache_write_tokens"))
97
+ : {} as Record<string, unknown>;
98
+ const rawOutputDetails = isRecord(raw.output_tokens_details)
99
+ ? raw.output_tokens_details as Record<string, unknown>
100
+ : {} as Record<string, unknown>;
83
101
  const out: Record<string, unknown> = {
102
+ ...Object.fromEntries(Object.entries(raw).filter(([key]) =>
103
+ key !== "input_tokens" && key !== "output_tokens" && key !== "total_tokens"
104
+ && key !== "input_tokens_details" && key !== "output_tokens_details")),
84
105
  input_tokens: inputTokens,
85
106
  output_tokens: usage.outputTokens,
86
107
  total_tokens: usage.contextTotalTokens !== undefined
@@ -90,18 +111,19 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
90
111
  // cached_tokens carries cache READS only, matching OpenAI semantics, and is always present
91
112
  // (zero default) for strict clients. Clamp to inputTokens so a provider's absolute
92
113
  // checkpoint can never report more cache reads than input.
93
- const inputDetails: Record<string, number> = {
114
+ const inputDetails: Record<string, unknown> = {
115
+ ...rawInputDetails,
94
116
  cached_tokens: Math.min(usage.cachedInputTokens ?? 0, inputTokens),
95
117
  };
96
118
  if (usage.cacheCreationInputTokens !== undefined) {
97
- const cacheRead = inputDetails.cached_tokens ?? 0;
119
+ const cacheRead = typeof inputDetails.cached_tokens === "number" ? inputDetails.cached_tokens : 0;
98
120
  inputDetails.cache_write_tokens = Math.min(
99
121
  usage.cacheCreationInputTokens,
100
122
  Math.max(0, inputTokens - cacheRead),
101
123
  );
102
124
  }
103
125
  out.input_tokens_details = inputDetails;
104
- out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
126
+ out.output_tokens_details = { ...rawOutputDetails, reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
105
127
  return out;
106
128
  }
107
129
 
@@ -255,9 +277,12 @@ export function bridgeToResponsesSSE(
255
277
  toolName: string,
256
278
  namespace?: string,
257
279
  codeModeHelperName?: string,
258
- ): string => codeModeHelperName
259
- ? compileCodeModeHelperInput(args, codeModeHelperName)
260
- : repairFreeformToolInput(args, toolName, namespace);
280
+ ): string => {
281
+ const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
282
+ return helper
283
+ ? compileCodeModeHelperInput(args, helper)
284
+ : repairFreeformToolInput(args, toolName, namespace);
285
+ };
261
286
  // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
262
287
  // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
263
288
  // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
@@ -1150,7 +1175,13 @@ export function bridgeToResponsesSSE(
1150
1175
  if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
1151
1176
  const full = freeformPartialInput(currentToolCall.args);
1152
1177
  const emitted = currentToolCall.inputEmitted ?? "";
1153
- if (full.startsWith(emitted) && full.length > emitted.length) {
1178
+ // Also hold a buffer that could still become a complete patch envelope:
1179
+ // at completion such a body is recompiled into an apply_patch helper call,
1180
+ // and streaming the envelope bytes first would be that same rewind.
1181
+ const mayCompile = declaresCodeModeExec(options?.declaredToolNames)
1182
+ && !currentToolCall.namespace
1183
+ && currentToolCall.name === "exec";
1184
+ if (!(mayCompile && mayBecomePatchEnvelope(full)) && full.startsWith(emitted) && full.length > emitted.length) {
1154
1185
  emit("response.custom_tool_call_input.delta", {
1155
1186
  item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1156
1187
  delta: full.slice(emitted.length),
@@ -1644,9 +1675,12 @@ function buildResponseJSONWithBudget(
1644
1675
  toolName: string,
1645
1676
  namespace?: string,
1646
1677
  codeModeHelperName?: string,
1647
- ): string => codeModeHelperName
1648
- ? compileCodeModeHelperInput(args, codeModeHelperName)
1649
- : repairFreeformToolInput(args, toolName, namespace);
1678
+ ): string => {
1679
+ const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
1680
+ return helper
1681
+ ? compileCodeModeHelperInput(args, helper)
1682
+ : repairFreeformToolInput(args, toolName, namespace);
1683
+ };
1650
1684
  const parseArgsObj = (args: string): Record<string, unknown> => {
1651
1685
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
1652
1686
  };
@@ -73,13 +73,18 @@ function userContentToBlocks(content: unknown): Rec[] {
73
73
  continue;
74
74
  }
75
75
  if (!isRec(raw)) continue;
76
- if ((raw.type === "text" || raw.type === "input_text") && typeof raw.text === "string") {
76
+ if ((raw.type === "text" || raw.type === "input_text" || raw.type === "output_text") && typeof raw.text === "string") {
77
77
  blocks.push({ type: "input_text", text: raw.text });
78
78
  continue;
79
79
  }
80
80
  const imageUrl = imageUrlFromPart(raw);
81
81
  if (imageUrl) {
82
- blocks.push({ type: "input_image", image_url: imageUrl });
82
+ const detail = isRec(raw.image_url) ? raw.image_url.detail : raw.detail;
83
+ blocks.push({
84
+ type: "input_image",
85
+ image_url: imageUrl,
86
+ ...(detail === "auto" || detail === "low" || detail === "high" ? { detail } : {}),
87
+ });
83
88
  continue;
84
89
  }
85
90
  const videoUrl = videoUrlFromPart(raw);
@@ -275,7 +280,10 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
275
280
  : typeof msg.tool_use_id === "string" ? msg.tool_use_id
276
281
  : "";
277
282
  if (!callId) throw new ChatCompletionsRequestError("tool messages require tool_call_id");
278
- const output = typeof msg.content === "string" ? msg.content : contentToText(msg.content);
283
+ const blocks = userContentToBlocks(msg.content);
284
+ const output = blocks.some(part => part.type === "input_image")
285
+ ? blocks.filter(part => part.type === "input_text" || part.type === "input_image")
286
+ : contentToText(msg.content);
279
287
  input.push({ type: "function_call_output", call_id: callId, output });
280
288
  break;
281
289
  }
@@ -0,0 +1,60 @@
1
+ import { isClaudeWebSearchToolName } from "./outbound";
2
+ import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
3
+
4
+ export function systemToInstructions(system: unknown): string | undefined {
5
+ if (typeof system === "string") return system.length > 0 ? system : undefined;
6
+ if (Array.isArray(system)) {
7
+ const parts: string[] = [];
8
+ for (const block of system) {
9
+ if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
10
+ }
11
+ return parts.length > 0 ? parts.join("\n\n") : undefined;
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ export function toolsToResponses(tools: unknown): Rec[] | undefined {
17
+ if (!Array.isArray(tools) || tools.length === 0) return undefined;
18
+ const out: Rec[] = [];
19
+ for (const raw of tools) {
20
+ if (!isRec(raw)) continue;
21
+ const type = typeof raw.type === "string" ? raw.type : "";
22
+ if (type.startsWith("web_search")) {
23
+ out.push({ type: "web_search" }); // hosted sidecar path
24
+ continue;
25
+ }
26
+ if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) {
27
+ out.push({
28
+ type: "function",
29
+ name: raw.name,
30
+ ...(typeof raw.description === "string" ? { description: raw.description } : {}),
31
+ parameters: raw.input_schema as Record<string, unknown>,
32
+ });
33
+ continue;
34
+ }
35
+ // Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop.
36
+ }
37
+ return out.length > 0 ? out : undefined;
38
+ }
39
+
40
+ export function toolChoiceToResponses(choice: unknown, body: Rec): void {
41
+ if (!isRec(choice)) return;
42
+ if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;
43
+ switch (choice.type) {
44
+ case "auto": body.tool_choice = "auto"; break;
45
+ case "none": body.tool_choice = "none"; break;
46
+ case "any": body.tool_choice = "required"; break;
47
+ case "tool":
48
+ if (typeof choice.name !== "string" || choice.name.length === 0) {
49
+ throw new AnthropicRequestError("tool_choice.tool requires a name");
50
+ }
51
+ // Anthropic represents hosted WebSearch as a named tool choice, while
52
+ // Responses requires the choice type to match the hosted declaration.
53
+ // Preserve forced-tool intent rather than weakening it to `auto`.
54
+ body.tool_choice = isClaudeWebSearchToolName(choice.name)
55
+ ? { type: "web_search" }
56
+ : { type: "function", name: choice.name };
57
+ break;
58
+ default: break;
59
+ }
60
+ }
@@ -0,0 +1,142 @@
1
+ import type { OcxClaudeCodeConfig } from "../types";
2
+ import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
3
+ import { resolveAlias } from "./alias";
4
+ import { stripOneMillionMarker } from "./context-windows";
5
+ import { resolveDesktop3pAlias } from "./desktop-3p";
6
+ import { isRec, type Rec } from "./inbound-records";
7
+
8
+ function isClaudeClassifierModel(model: string): boolean {
9
+ const stripped = model.replace(/-\d{8}$/, "");
10
+ return /^claude-opus-[45]/.test(stripped);
11
+ }
12
+
13
+ /**
14
+ * Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697).
15
+ *
16
+ * Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered
17
+ * `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so
18
+ * routing them crosses no boundary the operator did not ask for.
19
+ *
20
+ * Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the
21
+ * injected/default config slot, not the provider the live session actually selected, so it goes
22
+ * stale the moment the user changes the model picker -- and acting on it would silently move a
23
+ * classifier turn onto a provider with its own privacy and billing consequences. Live session
24
+ * affinity needs the request/session state this function does not have; it is tracked as
25
+ * follow-up work rather than approximated from static config.
26
+ */
27
+ function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined {
28
+ const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : "";
29
+ if (explicit.length > 0) return explicit;
30
+ if (Array.isArray(cc?.classifierFallbacks)) {
31
+ for (const candidate of cc.classifierFallbacks) {
32
+ if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ /** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */
39
+ export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string {
40
+ // Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a
41
+ // leaking build must not break alias decode (devlog 138 — the 1M signal is the
42
+ // anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i.
43
+ model = stripOneMillionMarker(model);
44
+ const aliased = resolveAlias(model);
45
+ if (aliased) return aliased;
46
+ // Desktop 3P aliases: claude-opus-4-{code} → provider/model route key
47
+ const desktop3p = resolveDesktop3pAlias(model);
48
+ if (desktop3p) {
49
+ // Native pseudo-provider returns bare slug; routed returns provider/model
50
+ const sep = desktop3p.indexOf("/");
51
+ if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1);
52
+ return desktop3p;
53
+ }
54
+ const map = cc?.modelMap ?? {};
55
+ const exact = map[model];
56
+ if (typeof exact === "string" && exact.length > 0) return exact;
57
+ const stripped = model.replace(/-\d{8}$/, "");
58
+ const dateless = map[stripped];
59
+ if (typeof dateless === "string" && dateless.length > 0) return dateless;
60
+
61
+ // Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as
62
+ // `claude-opus-5` carry no provider, so without this they fall through to defaultProvider --
63
+ // which may not speak Anthropic at all. Only an operator-declared target is used.
64
+ if (isClaudeClassifierModel(model)) {
65
+ const configured = configuredClassifierRoute(cc);
66
+ if (configured) return configured;
67
+ }
68
+ return model;
69
+ }
70
+
71
+ /** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */
72
+ export function effortForThinkingBudget(budget: number): string {
73
+ if (budget <= 4096) return "low";
74
+ if (budget <= 16384) return "medium";
75
+ return "high";
76
+ }
77
+
78
+ /**
79
+ * Adaptive-thinking wire (devlog 080): Claude Code /effort sends
80
+ * `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local
81
+ * capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it
82
+ * is a known Responses effort; unknown strings are dropped so downstream defaults win.
83
+ */
84
+ const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
85
+ export function effortFromOutputConfig(outputConfig: unknown): string | undefined {
86
+ if (!isRec(outputConfig)) return undefined;
87
+ const effort = outputConfig.effort;
88
+ return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
89
+ }
90
+
91
+ export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
92
+ if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
93
+ const format = outputConfig.format;
94
+ if (
95
+ format.type !== "json_schema"
96
+ || !isRec(format.schema)
97
+ || !isAnthropicOutputSchema(format.schema)
98
+ ) return undefined;
99
+ return { type: "json_schema", name: "response", schema: format.schema };
100
+ }
101
+
102
+ /**
103
+ * ocx-route directive (devlog 072): injected agent-definition bodies carry
104
+ * `<!-- ocx-route: <model> -->` because Claude Code 2.1.207 ignores custom
105
+ * gateway ids in agent frontmatter (live-proven fallback to sonnet). The body
106
+ * rides the subagent's system prompt, so the proxy re-routes here. Only the
107
+ * FIRST directive wins; the scan is bounded to the system field.
108
+ */
109
+ const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
110
+ const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
111
+
112
+ function systemText(body: unknown): string | null {
113
+ if (!isRec(body)) return null;
114
+ const system = body.system;
115
+ if (typeof system === "string") return system || null;
116
+ if (!Array.isArray(system)) return null;
117
+ const text = system
118
+ .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
119
+ .map(b => b.text as string)
120
+ .join("\n");
121
+ return text || null;
122
+ }
123
+
124
+ export function extractOcxRouteDirective(body: unknown): string | null {
125
+ const text = systemText(body);
126
+ if (!text) return null;
127
+ const match = OCX_ROUTE_RE.exec(text);
128
+ return match ? match[1]! : null;
129
+ }
130
+
131
+ /**
132
+ * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
133
+ * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
134
+ * exact generated-agent setting through the same trusted system-body channel as
135
+ * ocx-route so the inbound translator can restore `output_config.effort`.
136
+ */
137
+ export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
138
+ const text = systemText(body);
139
+ if (!text) return null;
140
+ const match = OCX_EFFORT_RE.exec(text);
141
+ return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
142
+ }
@@ -0,0 +1,7 @@
1
+ export class AnthropicRequestError extends Error {}
2
+
3
+ export type Rec = Record<string, unknown>;
4
+
5
+ export function isRec(v: unknown): v is Rec {
6
+ return !!v && typeof v === "object" && !Array.isArray(v);
7
+ }