@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BF38heuV.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DMiI18Kv.css">
19
+ <script type="module" crossorigin src="/assets/index-D4OKgUV4.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-Be1sDpWw.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iislee/opencodex",
3
- "version": "2.31.0",
3
+ "version": "2.35.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -42,6 +42,8 @@
42
42
  "typecheck": "bun x tsc --noEmit",
43
43
  "audit:high": "bun audit --audit-level=high && cd gui && bun audit --audit-level=high",
44
44
  "privacy:scan": "bun scripts/privacy-scan.ts",
45
+ "skill:surface": "bun scripts/generate-ocx-skill-surface.ts",
46
+ "skill:surface:check": "bun scripts/generate-ocx-skill-surface.ts --check",
45
47
  "generate:model-metadata": "bun scripts/generate-model-metadata.ts",
46
48
  "build:gui": "cd gui && bun install --frozen-lockfile && bun run build && cd .. && bun run prepare:package",
47
49
  "prepare:package": "bun scripts/prepare-package.ts",
@@ -14,7 +14,7 @@ import type {
14
14
  OcxToolResultMessage,
15
15
  OcxUsage,
16
16
  } from "../types";
17
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
17
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
18
18
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
19
19
  import { parseDataUrl } from "./image";
20
20
  import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
@@ -27,6 +27,7 @@ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
27
27
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
28
28
  import { decodeServerSentEvents } from "../lib/sse-decoder";
29
29
  import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
30
+ import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort";
30
31
 
31
32
  /** Map a user content part to an Anthropic content block (text or image source). */
32
33
  function toAnthropicContentPart(p: OcxContentPart): unknown {
@@ -36,6 +37,7 @@ function toAnthropicContentPart(p: OcxContentPart): unknown {
36
37
  ? { type: "image", source: { type: "base64", media_type: data.mediaType, data: data.base64 } }
37
38
  : { type: "image", source: { type: "url", url: p.imageUrl } };
38
39
  }
40
+ if (p.type === "video") return { type: "text", text: "[video]" };
39
41
  return { type: "text", text: p.text };
40
42
  }
41
43
 
@@ -264,6 +266,39 @@ export function formatAnthropicErrorBody(status: number, _headers: Headers, payl
264
266
  return redactSecretString(detail).slice(0, 400);
265
267
  }
266
268
 
269
+ function isAnthropicRecord(value: unknown): value is Record<string, unknown> {
270
+ return value !== null && typeof value === "object" && !Array.isArray(value);
271
+ }
272
+
273
+ function anthropicStructuralValueType(value: unknown): string {
274
+ if (value === null) return "null";
275
+ return Array.isArray(value) ? "array" : typeof value;
276
+ }
277
+
278
+ interface InvalidAnthropicShapeDiagnostic {
279
+ reason: "content_not_array" | "content_block_not_object";
280
+ blockIndex?: number;
281
+ valueType: string;
282
+ }
283
+
284
+ /**
285
+ * Structured refusal for a malformed buffered response body, shaped like the google adapter's
286
+ * `invalidGoogleShapeEvent` so an operator reading a log can tell which rung failed: the content
287
+ * container, or one block inside an otherwise well-formed container.
288
+ */
289
+ function invalidAnthropicShapeEvent(
290
+ diagnostic: InvalidAnthropicShapeDiagnostic,
291
+ ): Extract<AdapterEvent, { type: "error" }> {
292
+ const at = diagnostic.blockIndex !== undefined ? `; blockIndex=${diagnostic.blockIndex}` : "";
293
+ const subject = diagnostic.reason === "content_not_array" ? "content" : "content block";
294
+ return {
295
+ type: "error",
296
+ message: `anthropic response contained invalid ${subject} (${diagnostic.reason}${at}; valueType=${diagnostic.valueType})`,
297
+ status: 502,
298
+ errorType: "upstream_error",
299
+ };
300
+ }
301
+
267
302
  function extractAnthropicErrorDetail(parsed: unknown): string | undefined {
268
303
  if (typeof parsed === "string") return parsed.trim() || undefined;
269
304
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
@@ -469,10 +504,10 @@ function claudeFamilyVersion(modelId: string): { family: string; major: number;
469
504
  // Find the segment that actually starts with `claude-`, rather than assuming it is either
470
505
  // the first (breaks `anthropic/claude-sonnet-5`) or the last (breaks `claude-sonnet-5/variant`,
471
506
  // where the slash carries a vendor suffix rather than a routing prefix).
472
- const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(modelId);
507
+ const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:[.-](\d{1,2}))?(?!\d)/i.exec(modelId);
473
508
  if (!match) return undefined;
474
509
  return {
475
- family: match[1]!,
510
+ family: match[1]!.toLowerCase(),
476
511
  major: Number(match[2]),
477
512
  minor: match[3] === undefined ? 0 : Number(match[3]),
478
513
  };
@@ -518,6 +553,18 @@ function adaptiveEffort(effort: string): string {
518
553
  return effort === "minimal" ? "low" : effort;
519
554
  }
520
555
 
556
+ function defaultReasoningEffort(provider: OcxProviderConfig, modelId: string): string | undefined {
557
+ const value = modelRecordValue(provider.modelDefaultReasoningEfforts, modelId);
558
+ if (typeof value !== "string") return undefined;
559
+ const trimmed = value.trim();
560
+ // `__omit__` means "send no reasoning field", not "an effort literally named
561
+ // __omit__". Without this the sentinel reached the wire as
562
+ // `output_config.effort: "__omit__"` on adaptive models, and enabled budget
563
+ // thinking on the rest — the opposite of what it asks for (#2432).
564
+ if (!trimmed || isReasoningEffortOmitted(trimmed)) return undefined;
565
+ return trimmed;
566
+ }
567
+
521
568
  function usageFromAnthropic(usage: Record<string, number> | undefined): OcxUsage | undefined {
522
569
  if (!usage) return undefined;
523
570
  const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
@@ -796,11 +843,8 @@ function messagesToAnthropicFormat(
796
843
 
797
844
  function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (name: string) => string }): unknown[] | undefined {
798
845
  if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
799
- const allowed = isAllowedToolChoice(parsed.options.toolChoice)
800
- ? new Set(parsed.options.toolChoice.allowedTools)
801
- : undefined;
802
- const tools = allowed
803
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
846
+ const tools = isAllowedToolChoice(parsed.options.toolChoice)
847
+ ? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
804
848
  : parsed.context.tools;
805
849
  if (tools.length === 0) return undefined;
806
850
  const converted = tools.map(t => ({
@@ -929,18 +973,20 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
929
973
  // anyway, and thinking shares the caller's `max_tokens` — which truncates a small-budget
930
974
  // request before it can emit its stop sequence (#545). Say "disabled" out loud where the
931
975
  // model both defaults to thinking and accepts being told not to.
932
- if (parsed.options.reasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
976
+ const effectiveReasoning = parsed.options.reasoning ?? defaultReasoningEffort(provider, parsed.modelId);
977
+ if (effectiveReasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
933
978
  body.thinking = { type: "disabled" };
934
- } else if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
979
+ } else if (typeof effectiveReasoning === "string" && effectiveReasoning !== "none") {
935
980
  if (usesAdaptiveThinking(parsed.modelId)) {
936
981
  // Adaptive-thinking models replace the token budget with an effort knob and reject
937
982
  // `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible
938
983
  // output, so high effort needs the same total-token headroom as budget thinking or a
939
984
  // default 8192-token request can spend everything on thought and return empty text.
940
985
  body.thinking = { type: "adaptive" };
941
- body.output_config = { effort: adaptiveEffort(parsed.options.reasoning) };
986
+ const effort = adaptiveEffort(effectiveReasoning);
987
+ body.output_config = { effort };
942
988
  const explicitMaxOut = parsed.options.maxOutputTokens;
943
- const wantBudget = reasoningBudget(parsed.options.reasoning);
989
+ const wantBudget = reasoningBudget(effort);
944
990
  const floor = wantBudget + OUTPUT_HEADROOM;
945
991
  // Preserve explicit caller limits as-is; for omitted limits use the adaptive ceiling
946
992
  // so effort=max (budget=32k) still leaves OUTPUT_HEADROOM tokens for visible output.
@@ -953,7 +999,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
953
999
  // 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
954
1000
  // always exceeds the budget within a model-safe ceiling, reserving room for visible output.
955
1001
  const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
956
- const wantBudget = reasoningBudget(parsed.options.reasoning);
1002
+ const wantBudget = reasoningBudget(effectiveReasoning);
957
1003
  const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM));
958
1004
  const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
959
1005
  body.max_tokens = maxTokens;
@@ -1267,12 +1313,55 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1267
1313
  },
1268
1314
 
1269
1315
  async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1270
- const json = await response.json() as Record<string, unknown>;
1316
+ const parsed: unknown = await response.json();
1317
+ // `response.json()` resolves a body of `null` to `null` without throwing, so the cast below
1318
+ // used to reach `json.content` on it — the #1219 defect at the buffered body root. The
1319
+ // streaming parser has skipped a non-record frame since #1240, but a buffered body has no
1320
+ // next frame to recover into, so this fails closed instead.
1321
+ if (!isAnthropicRecord(parsed)) {
1322
+ return [{
1323
+ type: "error",
1324
+ message: `anthropic response was not a JSON object (${anthropicStructuralValueType(parsed)})`,
1325
+ status: 502,
1326
+ errorType: "upstream_error",
1327
+ }];
1328
+ }
1329
+ const json = parsed;
1271
1330
  const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
1272
1331
  budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
1273
1332
  try {
1274
1333
  const events: AdapterEvent[] = [];
1275
- const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
1334
+ // Same retain-then-return tail every other terminal branch in this function uses; naming it
1335
+ // keeps the two shape guards below from drifting out of step with the accounting.
1336
+ const finishWithEvents = (batch: AdapterEvent[]): AdapterEvent[] => {
1337
+ retainTranslatedEventBatch(batch, budget);
1338
+ return batch;
1339
+ };
1340
+ const rawContent: unknown = json.content;
1341
+ // `content` is claimed model output inside a well-formed response, so it is governed by the
1342
+ // #1332 nested-shape rule (fail closed) rather than #1240's root-frame padding rule (skip).
1343
+ // Absence stays legal in both encodings. A present non-array was silently accepted and is
1344
+ // the dangerous case: a string is iterable, so `for (const block of "text")` walked it one
1345
+ // CHARACTER at a time, read `undefined` from every `block.type`, and completed the turn as a
1346
+ // successful empty response — the #2231 failure mode on a different adapter.
1347
+ if (rawContent !== undefined && rawContent !== null && !Array.isArray(rawContent)) {
1348
+ return finishWithEvents([invalidAnthropicShapeEvent({
1349
+ reason: "content_not_array",
1350
+ valueType: anthropicStructuralValueType(rawContent),
1351
+ })]);
1352
+ }
1353
+ if (Array.isArray(rawContent)) {
1354
+ for (let blockIndex = 0; blockIndex < rawContent.length; blockIndex++) {
1355
+ if (!isAnthropicRecord(rawContent[blockIndex])) {
1356
+ return finishWithEvents([invalidAnthropicShapeEvent({
1357
+ reason: "content_block_not_object",
1358
+ blockIndex,
1359
+ valueType: anthropicStructuralValueType(rawContent[blockIndex]),
1360
+ })]);
1361
+ }
1362
+ }
1363
+ }
1364
+ const content = rawContent as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
1276
1365
  if (content) {
1277
1366
  for (const block of content) {
1278
1367
  if (block.type === "text" && block.text) {
@@ -37,6 +37,21 @@ export interface ProviderAdapter {
37
37
  */
38
38
  buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): AdapterRequest | Promise<AdapterRequest>;
39
39
 
40
+ /**
41
+ * Decide, BEFORE any request is built or sent, that this turn has nothing to ask upstream.
42
+ *
43
+ * Returning a reason short-circuits the turn to a locally constructed completed response: no
44
+ * `buildRequest`, no send, no token estimate, and no empty-completion retry. That last part is
45
+ * why this cannot be expressed as an outputless `done` from `parseStream`: the empty-completion
46
+ * guard treats a terminal with no content as a failed turn and re-invokes the identical request,
47
+ * so an adapter that "successfully returned nothing" would be retried into the very loop it was
48
+ * trying to end.
49
+ *
50
+ * Only for turns whose input already contains the answer — see the Kiro adapter, where replayed
51
+ * history ending in a delivered final answer has nothing left to complete.
52
+ */
53
+ localTerminal?(parsed: OcxParsedRequest): AdapterLocalTerminal | undefined;
54
+
40
55
  fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
41
56
 
42
57
  /**
@@ -70,10 +85,12 @@ export interface AdapterRequest {
70
85
  body: string;
71
86
  /** Final upstream wire names of custom tools lowered to functions while building this request. */
72
87
  convertedRoutedCustomToolNames?: ReadonlySet<string>;
88
+ /** Native custom-tool wire names authorized for representation-only response repair. */
89
+ routedCustomToolRepairNames?: ReadonlySet<string>;
73
90
  /** Client tool-search names actually lowered to upstream function calls for this request. */
74
91
  convertedRoutedToolSearchNames?: ReadonlySet<string>;
75
92
  /** Upstream-only aliases for namespace tools flattened in this request. */
76
- convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string }>;
93
+ convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string; kind: "function" | "custom" }>;
77
94
  /** Releases observation of a serialized request body after its final fetch attempt settles. */
78
95
  releaseBodyObservation?: () => void;
79
96
  /** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
@@ -117,3 +134,14 @@ export interface AdapterFetchContext {
117
134
  /** Custom fetch executor to use for physical upstream network requests (defaults to globalThis.fetch). */
118
135
  executor?: typeof globalThis.fetch;
119
136
  }
137
+
138
+ /**
139
+ * An adapter's decision that a turn needs no upstream inference at all.
140
+ *
141
+ * `reason` is diagnostic only. It is never sent to the client and never logged as request
142
+ * content: it names the code path for a maintainer reading a request log, so it must stay a
143
+ * fixed identifier rather than anything derived from the conversation.
144
+ */
145
+ export interface AdapterLocalTerminal {
146
+ reason: string;
147
+ }
@@ -3,10 +3,11 @@ import { execFile as execFileCallback } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import { opendir } from "node:fs/promises";
5
5
  import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
6
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
6
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
7
7
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
8
8
  import type { TranslatorBudget } from "../lib/translator-budget";
9
9
  import { readBoundedResponseBody } from "../lib/bounded-body";
10
+ import { debugDroppedFrame } from "../lib/debug";
10
11
  import { configuredReasoningEfforts } from "../reasoning-effort";
11
12
  import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
12
13
  import { identifyRoutedModel } from "./identity";
@@ -143,7 +144,8 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
143
144
  if (typeof message.content === "string") content.push({ type: "text", text: message.content });
144
145
  else for (const part of message.content) {
145
146
  if (part.type === "text") content.push({ type: "text", text: part.text });
146
- else content.push(wireImagePart(part.imageUrl));
147
+ else if (part.type === "image") content.push(wireImagePart(part.imageUrl));
148
+ else content.push({ type: "text", text: "[video]" });
147
149
  }
148
150
  out.push({ role: "user", content });
149
151
  }
@@ -156,8 +158,7 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
156
158
  if (choice === "none") return [];
157
159
  const tools = parsed.context.tools ?? [];
158
160
  if (isAllowedToolChoice(choice)) {
159
- const allowed = new Set(choice.allowedTools);
160
- return tools.filter(tool => toolAllowedByChoice(tool, allowed, tools));
161
+ return tools.filter(toolChoiceToolPredicate(choice, tools));
161
162
  }
162
163
  if (choice && typeof choice !== "string") {
163
164
  const selected = resolveToolChoiceWireName(tools, choice.name);
@@ -366,7 +367,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
366
367
  let newline = buffer.indexOf("\n");
367
368
  while (newline >= 0) {
368
369
  const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
369
- if (line) { try { yield JSON.parse(stripEventFrame(line)) as Record<string, unknown>; } catch { /* ignore non-events */ } }
370
+ if (line) yield* decodeEventLine(line);
370
371
  newline = buffer.indexOf("\n");
371
372
  }
372
373
  const residualBytes = encoder.encode(buffer).byteLength;
@@ -377,7 +378,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
377
378
  if (done) break;
378
379
  }
379
380
  const final = buffer.trim();
380
- if (final) { try { yield JSON.parse(stripEventFrame(final)) as Record<string, unknown>; } catch { /* ignore */ } }
381
+ if (final) yield* decodeEventLine(final);
381
382
  } finally {
382
383
  budget.releaseRetained(bufferBytes, { kind: "live_transient" });
383
384
  try { await reader.cancel(); } catch { /* already closed */ }
@@ -385,6 +386,41 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
385
386
  }
386
387
  }
387
388
 
389
+ /**
390
+ * Yield one NDJSON line as an event record, or nothing.
391
+ *
392
+ * `JSON.parse("null")` returns `null` instead of throwing, so the `try/catch` around the parse
393
+ * cannot see it and the `event.type` read in parseStream crashed the turn — the #1219 defect, on
394
+ * the one streaming transport the #1240 audit did not cover because it is NDJSON rather than SSE.
395
+ *
396
+ * A frame that does not parse to a record is padding, not an event: drop it and continue exactly
397
+ * as an unparseable line is already dropped, so a stream whose only frames are junk ends in the
398
+ * same single terminal `done` as an empty body. Skipping is what preserves an answer whose deltas
399
+ * have already arrived — the observed #1219 case is `null` padding BETWEEN content deltas, where
400
+ * terminating would discard a complete response (#1240).
401
+ *
402
+ * Note this deliberately makes a junk-only stream a quiet `[done]` where it previously threw. That
403
+ * throw was an unguarded type assumption, not a designed failure signal, and `[done]` is already
404
+ * what an empty body, a blank-line-only body and an unparseable-only body all produce here. The
405
+ * broader question — whether this adapter should report *any* no-valid-event stream as a failure
406
+ * rather than an empty success — is pre-existing, applies to all four of those inputs equally, and
407
+ * is deliberately not decided by this change.
408
+ */
409
+ function* decodeEventLine(line: string): Generator<Record<string, unknown>> {
410
+ let parsed: unknown;
411
+ try {
412
+ parsed = JSON.parse(stripEventFrame(line));
413
+ } catch {
414
+ debugDroppedFrame("command-code", line);
415
+ return;
416
+ }
417
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
418
+ debugDroppedFrame("command-code", line);
419
+ return;
420
+ }
421
+ yield parsed as Record<string, unknown>;
422
+ }
423
+
388
424
  /** The endpoint is newline-delimited JSON; defensively strip an SSE `data:` frame if the gateway ever switches shapes. */
389
425
  function stripEventFrame(line: string): string {
390
426
  return line.startsWith("data:") ? line.slice("data:".length).trim() : line;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Reversible single-line codec for Cursor composite tool-call ids.
3
+ *
4
+ * Cursor's wire delivers tool-call ids that can be two identifiers glued with a
5
+ * literal newline ("call-<uuid>-<n>\nfc_<uuid>_<n>"). OpenCodex forwards ids
6
+ * verbatim, so that newline leaked into Responses-visible `call_id` values,
7
+ * where line-oriented clients (logging, splitting, validation) break. The codec
8
+ * encodes only ids containing CR/LF into a versioned single-line form and
9
+ * decodes both that form and legacy raw multi-line ids back to the exact
10
+ * upstream bytes before anything is serialized toward Cursor.
11
+ */
12
+
13
+ const CALL_ID_PREFIX = "ocxc1_";
14
+
15
+ /** True when the id needs encoding to survive line-oriented consumers. */
16
+ function needsEncoding(id: string): boolean {
17
+ return id.includes("\n") || id.includes("\r");
18
+ }
19
+
20
+ /** Encode a Cursor wire call id into a single-line Responses-safe id. */
21
+ export function encodeCursorCallId(id: string): string {
22
+ if (!needsEncoding(id)) return id;
23
+ return CALL_ID_PREFIX + Buffer.from(id, "utf8").toString("base64url");
24
+ }
25
+
26
+ /**
27
+ * Decode a Responses-visible call id back to the exact Cursor wire id.
28
+ * Non-encoded ids (including legacy raw multi-line ids replayed by older
29
+ * clients) pass through unchanged; a malformed encoded payload also passes
30
+ * through rather than corrupting pairing.
31
+ */
32
+ export function decodeCursorCallId(id: string): string {
33
+ if (!id.startsWith(CALL_ID_PREFIX)) return id;
34
+ const payload = id.slice(CALL_ID_PREFIX.length);
35
+ if (payload.length === 0) return id;
36
+ try {
37
+ const decoded = Buffer.from(payload, "base64url").toString("utf8");
38
+ // Round-trip guard: only trust payloads our encoder could have produced.
39
+ if (Buffer.from(decoded, "utf8").toString("base64url") !== payload) return id;
40
+ return decoded;
41
+ } catch {
42
+ return id;
43
+ }
44
+ }