@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
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 {
@@ -277,9 +277,12 @@ export function bridgeToResponsesSSE(
277
277
  toolName: string,
278
278
  namespace?: string,
279
279
  codeModeHelperName?: string,
280
- ): string => codeModeHelperName
281
- ? compileCodeModeHelperInput(args, codeModeHelperName)
282
- : 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
+ };
283
286
  // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
284
287
  // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
285
288
  // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
@@ -1172,7 +1175,13 @@ export function bridgeToResponsesSSE(
1172
1175
  if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
1173
1176
  const full = freeformPartialInput(currentToolCall.args);
1174
1177
  const emitted = currentToolCall.inputEmitted ?? "";
1175
- 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) {
1176
1185
  emit("response.custom_tool_call_input.delta", {
1177
1186
  item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1178
1187
  delta: full.slice(emitted.length),
@@ -1666,9 +1675,12 @@ function buildResponseJSONWithBudget(
1666
1675
  toolName: string,
1667
1676
  namespace?: string,
1668
1677
  codeModeHelperName?: string,
1669
- ): string => codeModeHelperName
1670
- ? compileCodeModeHelperInput(args, codeModeHelperName)
1671
- : 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
+ };
1672
1684
  const parseArgsObj = (args: string): Record<string, unknown> => {
1673
1685
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
1674
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
+ }
@@ -10,130 +10,24 @@
10
10
  * - top_k is accepted and silently dropped (no Responses equivalent, CCR parity).
11
11
  */
12
12
  import type { OcxClaudeCodeConfig } from "../types";
13
- import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
14
- import { resolveAlias } from "./alias";
15
- import { stripOneMillionMarker } from "./context-windows";
16
- import { resolveDesktop3pAlias } from "./desktop-3p";
17
- import { isClaudeWebSearchToolName } from "./outbound";
18
13
  import { createHash } from "node:crypto";
19
14
 
20
- export class AnthropicRequestError extends Error {}
15
+ export { AnthropicRequestError } from "./inbound-records";
16
+ export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, extractOcxRouteDirective, extractOcxEffortDirective } from "./inbound-model-options";
17
+ import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
18
+ import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options";
19
+ import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options";
21
20
 
22
- type Rec = Record<string, unknown>;
23
21
 
24
- function isRec(v: unknown): v is Rec {
25
- return !!v && typeof v === "object" && !Array.isArray(v);
26
- }
27
-
28
- function isClaudeClassifierModel(model: string): boolean {
29
- const stripped = model.replace(/-\d{8}$/, "");
30
- return /^claude-opus-[45]/.test(stripped);
31
- }
32
-
33
- /**
34
- * Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697).
35
- *
36
- * Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered
37
- * `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so
38
- * routing them crosses no boundary the operator did not ask for.
39
- *
40
- * Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the
41
- * injected/default config slot, not the provider the live session actually selected, so it goes
42
- * stale the moment the user changes the model picker -- and acting on it would silently move a
43
- * classifier turn onto a provider with its own privacy and billing consequences. Live session
44
- * affinity needs the request/session state this function does not have; it is tracked as
45
- * follow-up work rather than approximated from static config.
46
- */
47
- function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined {
48
- const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : "";
49
- if (explicit.length > 0) return explicit;
50
- if (Array.isArray(cc?.classifierFallbacks)) {
51
- for (const candidate of cc.classifierFallbacks) {
52
- if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
53
- }
54
- }
55
- return undefined;
56
- }
57
-
58
- /** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */
59
- export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string {
60
- // Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a
61
- // leaking build must not break alias decode (devlog 138 — the 1M signal is the
62
- // anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i.
63
- model = stripOneMillionMarker(model);
64
- const aliased = resolveAlias(model);
65
- if (aliased) return aliased;
66
- // Desktop 3P aliases: claude-opus-4-{code} → provider/model route key
67
- const desktop3p = resolveDesktop3pAlias(model);
68
- if (desktop3p) {
69
- // Native pseudo-provider returns bare slug; routed returns provider/model
70
- const sep = desktop3p.indexOf("/");
71
- if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1);
72
- return desktop3p;
73
- }
74
- const map = cc?.modelMap ?? {};
75
- const exact = map[model];
76
- if (typeof exact === "string" && exact.length > 0) return exact;
77
- const stripped = model.replace(/-\d{8}$/, "");
78
- const dateless = map[stripped];
79
- if (typeof dateless === "string" && dateless.length > 0) return dateless;
80
-
81
- // Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as
82
- // `claude-opus-5` carry no provider, so without this they fall through to defaultProvider --
83
- // which may not speak Anthropic at all. Only an operator-declared target is used.
84
- if (isClaudeClassifierModel(model)) {
85
- const configured = configuredClassifierRoute(cc);
86
- if (configured) return configured;
87
- }
88
- return model;
89
- }
90
-
91
- /** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */
92
- export function effortForThinkingBudget(budget: number): string {
93
- if (budget <= 4096) return "low";
94
- if (budget <= 16384) return "medium";
95
- return "high";
96
- }
97
-
98
- /**
99
- * Adaptive-thinking wire (devlog 080): Claude Code /effort sends
100
- * `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local
101
- * capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it
102
- * is a known Responses effort; unknown strings are dropped so downstream defaults win.
103
- */
104
- const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
105
- export function effortFromOutputConfig(outputConfig: unknown): string | undefined {
106
- if (!isRec(outputConfig)) return undefined;
107
- const effort = outputConfig.effort;
108
- return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
109
- }
110
-
111
- function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
112
- if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
113
- const format = outputConfig.format;
114
- if (
115
- format.type !== "json_schema"
116
- || !isRec(format.schema)
117
- || !isAnthropicOutputSchema(format.schema)
118
- ) return undefined;
119
- return { type: "json_schema", name: "response", schema: format.schema };
120
- }
121
-
122
- function systemToInstructions(system: unknown): string | undefined {
123
- if (typeof system === "string") return system.length > 0 ? system : undefined;
124
- if (Array.isArray(system)) {
125
- const parts: string[] = [];
126
- for (const block of system) {
127
- if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
128
- }
129
- return parts.length > 0 ? parts.join("\n\n") : undefined;
130
- }
131
- return undefined;
132
- }
133
22
 
134
23
  function imageBlockToInputImage(block: Rec): Rec | null {
135
24
  const source = block.source;
136
25
  if (!isRec(source)) return null;
26
+ if (source.type === "file") {
27
+ throw new AnthropicRequestError(
28
+ "File-backed images require native Anthropic passthrough; use base64 or URL images on translated routes.",
29
+ );
30
+ }
137
31
  if (source.type === "base64" && typeof source.data === "string") {
138
32
  const media = typeof source.media_type === "string" ? source.media_type : "image/png";
139
33
  return { type: "input_image", image_url: `data:${media};base64,${source.data}` };
@@ -194,47 +88,6 @@ export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "block
194
88
  .filter(name => name.length > 0))];
195
89
  }
196
90
 
197
- /**
198
- * ocx-route directive (devlog 072): injected agent-definition bodies carry
199
- * `<!-- ocx-route: <model> -->` because Claude Code 2.1.207 ignores custom
200
- * gateway ids in agent frontmatter (live-proven fallback to sonnet). The body
201
- * rides the subagent's system prompt, so the proxy re-routes here. Only the
202
- * FIRST directive wins; the scan is bounded to the system field.
203
- */
204
- const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
205
- const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
206
-
207
- function systemText(body: unknown): string | null {
208
- if (!isRec(body)) return null;
209
- const system = body.system;
210
- if (typeof system === "string") return system || null;
211
- if (!Array.isArray(system)) return null;
212
- const text = system
213
- .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
214
- .map(b => b.text as string)
215
- .join("\n");
216
- return text || null;
217
- }
218
-
219
- export function extractOcxRouteDirective(body: unknown): string | null {
220
- const text = systemText(body);
221
- if (!text) return null;
222
- const match = OCX_ROUTE_RE.exec(text);
223
- return match ? match[1]! : null;
224
- }
225
-
226
- /**
227
- * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
228
- * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
229
- * exact generated-agent setting through the same trusted system-body channel as
230
- * ocx-route so the inbound translator can restore `output_config.effort`.
231
- */
232
- export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
233
- const text = systemText(body);
234
- if (!text) return null;
235
- const match = OCX_EFFORT_RE.exec(text);
236
- return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
237
- }
238
91
 
239
92
  /** Injected-skill payloads below this size are never stubbed (not worth it). */
240
93
  const SKILL_ELISION_MIN_CHARS = 10_000;
@@ -391,51 +244,6 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void {
391
244
  flush();
392
245
  }
393
246
 
394
- function toolsToResponses(tools: unknown): Rec[] | undefined {
395
- if (!Array.isArray(tools) || tools.length === 0) return undefined;
396
- const out: Rec[] = [];
397
- for (const raw of tools) {
398
- if (!isRec(raw)) continue;
399
- const type = typeof raw.type === "string" ? raw.type : "";
400
- if (type.startsWith("web_search")) {
401
- out.push({ type: "web_search" }); // hosted sidecar path
402
- continue;
403
- }
404
- if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) {
405
- out.push({
406
- type: "function",
407
- name: raw.name,
408
- ...(typeof raw.description === "string" ? { description: raw.description } : {}),
409
- parameters: raw.input_schema as Record<string, unknown>,
410
- });
411
- continue;
412
- }
413
- // Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop.
414
- }
415
- return out.length > 0 ? out : undefined;
416
- }
417
-
418
- function toolChoiceToResponses(choice: unknown, body: Rec): void {
419
- if (!isRec(choice)) return;
420
- if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;
421
- switch (choice.type) {
422
- case "auto": body.tool_choice = "auto"; break;
423
- case "none": body.tool_choice = "none"; break;
424
- case "any": body.tool_choice = "required"; break;
425
- case "tool":
426
- if (typeof choice.name !== "string" || choice.name.length === 0) {
427
- throw new AnthropicRequestError("tool_choice.tool requires a name");
428
- }
429
- // Anthropic represents hosted WebSearch as a named tool choice, while
430
- // Responses requires the choice type to match the hosted declaration.
431
- // Preserve forced-tool intent rather than weakening it to `auto`.
432
- body.tool_choice = isClaudeWebSearchToolName(choice.name)
433
- ? { type: "web_search" }
434
- : { type: "function", name: choice.name };
435
- break;
436
- default: break;
437
- }
438
- }
439
247
 
440
248
  /** Recursive canonical JSON (keys sorted at every depth) — stable cache-cohort input. */
441
249
  function canonicalJson(value: unknown): string {
@@ -111,9 +111,32 @@ export function buildAnthropicModelInfos(
111
111
  aliasForRoute: (provider: string, modelId: string) => string = desktop3pAlias,
112
112
  nativeContextCap?: NativeContextLimitsInput,
113
113
  fastMode?: boolean,
114
+ // Presence is the feature gate: the caller passes undefined when `fastRows` is off, so a
115
+ // default install publishes nothing. The predicate answers ELIGIBILITY, not enablement.
116
+ fastRows?: (model: CatalogModel | { provider: string; id: string }) => boolean,
114
117
  ): AnthropicModelInfo[] {
115
118
  const out: AnthropicModelInfo[] = [];
116
119
  const seen = new Set<string>();
120
+ // Every id the loops below will really emit, computed BEFORE either runs. `seen` alone is
121
+ // not enough: it grows as they run, so whether a synthetic id collided with a real one
122
+ // would depend on iteration order. With both `foo` and a real `foo--fast` in the roster,
123
+ // the synthetic id for `foo` IS the real model's id, and whichever ran first would win it.
124
+ const realDiscoveryIds = new Set<string>([
125
+ ...nativeSlugs.map(slug => (
126
+ idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug)
127
+ )),
128
+ ...routedModels.map(m => {
129
+ // The same asymmetry the routed loop applies: readable uses the LISTED id, so a
130
+ // fastMode-rewritten Cursor row is counted under the id it is really published as,
131
+ // while Desktop 3P hashes the RAW id.
132
+ const listed = fastMode === true && m.provider === "cursor" && idStyle === "readable"
133
+ ? cursorFastIdFor(m.id) ?? m.id
134
+ : m.id;
135
+ return idStyle === "readable"
136
+ ? claudeCodeAlias(m.provider, listed)
137
+ : aliasForRoute(m.provider, m.id);
138
+ }),
139
+ ]);
117
140
  // [1m] picker variant (devlog 260712 B1): Claude Code accounts exactly 1M for ids
118
141
  // carrying the marker (2.1.207 binary: /\[1m\]/i → 1e6, compaction preserved), so
119
142
  // ONLY models with an authoritative >=1M window get a second selectable row —
@@ -140,6 +163,21 @@ export function buildAnthropicModelInfos(
140
163
  : ONE_MILLION;
141
164
  out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: advertised });
142
165
  };
166
+ /**
167
+ * Publish a Fast sibling beside a row, following `push1mVariant` rather than the
168
+ * `fastMode` rewrite below: `fastMode` is a global switch with no per-request choice, so
169
+ * it REPLACES the listed id, while a selector has to leave the default pickable beside it.
170
+ *
171
+ * Because it only ADDS a row, it is safe for the Desktop 3P hashed style too — the
172
+ * exclusion `fastMode` needs exists because rewriting a hash strands a saved selection.
173
+ */
174
+ const pushFastVariant = (base: AnthropicModelInfo) => {
175
+ const id = `${base.id}--fast`;
176
+ // A real model always wins its own id, whatever the iteration order.
177
+ if (realDiscoveryIds.has(id) || seen.has(id)) return;
178
+ seen.add(id);
179
+ out.push({ ...base, id, display_name: `${base.display_name} · Fast` });
180
+ };
143
181
  for (const slug of nativeSlugs) {
144
182
  const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
145
183
  if (seen.has(id)) continue;
@@ -151,6 +189,9 @@ export function buildAnthropicModelInfos(
151
189
  const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeMaxInput ?? nativeWindow);
152
190
  out.push(info);
153
191
  push1mVariant(info, nativeWindow, nativeMaxInput);
192
+ // Natives too, not only routed rows: gpt-5.6-sol is the flagship Fast model, and
193
+ // omitting it would leave this surface without the model the feature exists for.
194
+ if (fastRows?.({ provider: "native", id: slug }) === true) pushFastVariant(info);
154
195
  }
155
196
  for (const m of routedModels) {
156
197
  // Global Fast has no toggle on this surface, so the fast identity is what gets listed —
@@ -180,6 +221,10 @@ export function buildAnthropicModelInfos(
180
221
  // Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
181
222
  // routes — only a genuine >=1M window earns the variant row there.
182
223
  push1mVariant(info, m.contextWindow, routedMaxInput);
224
+ // The whole model is passed, not a (provider, id) pair: a combo row lives in its own
225
+ // namespace with no config.providers entry, so the caller classifies it from the
226
+ // aggregated supportsServiceTier the row already carries.
227
+ if (fastRows?.(m) === true) pushFastVariant(info);
183
228
  }
184
229
  return out;
185
230
  }