@bitkyc08/opencodex 2.64.0 → 2.65.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 (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-DGLte6IR.js +50 -0
  5. package/gui/dist/assets/Tray-D4bA1vff.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-SggB6t3z.js → index-Bi1K37Wl.js} +12 -12
  8. package/gui/dist/assets/tray-data-BZXhB3qz.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-CpuDF3ci.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
@@ -45,6 +45,7 @@ import { messagesToChatFormat } from "./openai-chat/messages";
45
45
  import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
46
46
  import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
47
47
  import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
48
+ import { reconcileSerializedToolCallEvents, reconcileStructuredToolCall, reconcileStructuredToolCalls, SerializedToolCallContentBuffer } from "./openai-chat/serialized-tool-call-content";
48
49
 
49
50
  export { stripBracketedModelSuffix } from "./openai-chat/wire";
50
51
  export { buildOpenAIChatPassthroughRequest } from "./openai-chat/passthrough";
@@ -143,7 +144,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
143
144
  if (parsed.options.topP !== undefined && !modelInList(provider.noTopPModels, parsed.modelId)) {
144
145
  body.top_p = parsed.options.topP;
145
146
  }
146
- if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
147
+ if (parsed.options.stopSequences !== undefined && !modelInList(provider.noStopModels, parsed.modelId)) {
148
+ body.stop = parsed.options.stopSequences;
149
+ }
147
150
  const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
148
151
  const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
149
152
  const explicitReasoning = applyExplicitChatReasoningWirePolicy({
@@ -299,6 +302,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
299
302
  sawArgumentsString: boolean;
300
303
  }
301
304
  const pendingToolCalls: PendingToolCall[] = [];
305
+ const toolCallContent = new SerializedToolCallContentBuffer(budget);
306
+ const heldText = (): AdapterEvent[] => toolCallContent.drain([]);
302
307
  let toolCallSeq = 0;
303
308
  const closeToolCalls = (): PendingToolCall[] => {
304
309
  const calls = [...pendingToolCalls];
@@ -310,7 +315,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
310
315
  pendingToolCalls.length > 0 && pendingToolCalls.every(call => {
311
316
  if (call.name.trim().length === 0 || !call.sawArgumentsString || call.args.length === 0) return false;
312
317
  try {
313
- const parsed = JSON.parse(call.args) as unknown;
318
+ const parsed = JSON.parse(reconcileStructuredToolCall(call.name, toolNames.restore(call.name), call.args, toolCallContent.current()).argumentsText) as unknown;
314
319
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
315
320
  } catch {
316
321
  return false;
@@ -320,7 +325,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
320
325
  // stops the turn instead of emitting an unusable call. `closeToolCalls()` runs first,
321
326
  // so budget reservations are released for every pending call even on the early return.
322
327
  const flushToolCalls = function* (): Generator<AdapterEvent, "continue" | "terminate"> {
323
- for (const call of closeToolCalls()) {
328
+ const calls = closeToolCalls();
329
+ for (const call of calls) {
324
330
  // Ingest already proved `name` is a string; the typeof guard keeps this branch
325
331
  // total so a future ingest change cannot turn a malformed name into a throw.
326
332
  if (typeof call.name !== "string" || call.name.trim().length === 0) {
@@ -328,9 +334,14 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
328
334
  hadId: call.id.length > 0,
329
335
  argsBytes: call.argsBytes,
330
336
  });
331
- yield unnamedToolCallEvent(pendingUsage);
332
- return "terminate";
337
+ return yield* terminateWithError(unnamedToolCallEvent(pendingUsage));
333
338
  }
339
+ }
340
+ // Held markup is released only now, as one batch per response: the doubled-input repair needs every call.
341
+ const references = reconcileStructuredToolCalls(calls.map(call => ({ wireName: call.name, restoredName: toolNames.restore(call.name), argumentsText: call.args })), toolCallContent.current());
342
+ calls.forEach((call, index) => { call.args = references[index]!.argumentsText; });
343
+ yield* toolCallContent.drain(references);
344
+ for (const call of calls) {
334
345
  if (!call.id) call.id = `call_${++toolCallSeq}`;
335
346
  yield { type: "tool_call_start", id: call.id, name: toolNames.restore(call.name) };
336
347
  if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
@@ -342,6 +353,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
342
353
  event: Extract<AdapterEvent, { type: "error" }>,
343
354
  ): Generator<AdapterEvent, "terminate"> {
344
355
  closeToolCalls();
356
+ yield* heldText(); // Pending tools are not dispatched, so held text stays visible.
345
357
  yield event;
346
358
  return "terminate";
347
359
  };
@@ -360,7 +372,13 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
360
372
  // <think> blocks, which would otherwise render as the answer. Passthrough unless opted in.
361
373
  const inlineThink = createInlineThinkContentSplitter(provider.inlineThinkTagModels, lastRequestedModelId, budget);
362
374
  const emitContent = function* (events: AdapterEvent[]): Generator<AdapterEvent> {
363
- for (const event of events) { if (event.type === "text_delta") sawUserFacingOutput = true; yield event; }
375
+ for (const event of events) {
376
+ // Any other event keeps its place behind held text instead of overtaking it.
377
+ if (event.type !== "text_delta") { yield* toolCallContent.hold(event); continue; }
378
+ sawUserFacingOutput = true;
379
+ const released = toolCallContent.ingestStreaming(event.text);
380
+ yield* released.length > 0 ? released : [{ type: "heartbeat" } as AdapterEvent];
381
+ }
364
382
  };
365
383
 
366
384
  const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
@@ -381,8 +399,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
381
399
  parsed = JSON.parse(payload);
382
400
  } catch {
383
401
  tierMetadata?.markResponseUnparseable();
384
- yield { type: "error", message: "malformed upstream SSE data frame" };
385
- return "terminate";
402
+ return yield* terminateWithError({ type: "error", message: "malformed upstream SSE data frame" });
386
403
  }
387
404
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return "continue";
388
405
  const chunk = parsed as Record<string, unknown>;
@@ -423,11 +440,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
423
440
  if (detailSegments.length > 0) {
424
441
  for (const segment of detailSegments) {
425
442
  const reasoningDelta = reasoningDetailTracker.ingest(segment);
426
- if (reasoningDelta !== null) yield { type: "reasoning_raw_delta", text: reasoningDelta };
443
+ if (reasoningDelta !== null) yield* toolCallContent.hold({ type: "reasoning_raw_delta", text: reasoningDelta });
427
444
  }
428
445
  } else {
429
446
  const reasoningText = reasoningTextFrom(delta);
430
- if (reasoningText !== undefined) yield { type: "reasoning_raw_delta", text: reasoningText };
447
+ if (reasoningText !== undefined) yield* toolCallContent.hold({ type: "reasoning_raw_delta", text: reasoningText });
431
448
  }
432
449
  if (typeof delta.content === "string" && delta.content.length > 0) {
433
450
  yield* emitContent(inlineThink.feed(delta.content));
@@ -630,7 +647,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
630
647
  hadUsage: pendingUsage !== undefined,
631
648
  pendingToolCalls: pendingToolCalls.length,
632
649
  });
633
- yield { type: "error", message: "upstream stream ended mid tool call without a terminal signal — possible truncation" };
650
+ yield* terminateWithError({ type: "error", message: "upstream stream ended mid tool call without a terminal signal — possible truncation" });
634
651
  return;
635
652
  }
636
653
  if (!sawFinish && !sawUserFacingOutput) {
@@ -638,13 +655,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
638
655
  finishReason: finishReason ?? null,
639
656
  hadUsage: pendingUsage !== undefined,
640
657
  });
641
- yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
658
+ yield* terminateWithError({ type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" });
642
659
  return;
643
660
  }
644
661
  if ((yield* flushToolCalls()) === "terminate") return;
645
662
  const stopReason = stopReasonFor(finishReason);
646
663
  yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
647
664
  } catch (error) {
665
+ closeToolCalls();
666
+ yield* heldText();
648
667
  if (isTranslatorBudgetExceededError(error)
649
668
  || (error instanceof Error && (error.cause as { code?: unknown } | undefined)?.code === "translation_buffer_limit")) {
650
669
  yield {
@@ -662,6 +681,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
662
681
  budget.releaseRetained(bufferBytes, { kind: "live_transient" });
663
682
  reasoningDetailTracker.release();
664
683
  inlineThink.dispose();
684
+ toolCallContent.dispose();
665
685
  closeToolCalls();
666
686
  reader.releaseLock();
667
687
  }
@@ -748,7 +768,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
748
768
  if (segments.length > 0) reasoningText = segments.map(s => s.text).join("");
749
769
  }
750
770
  if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
771
+ const contentStart = events.length;
751
772
  if (typeof msg.content === "string") events.push(...splitInlineThinkContent(provider.inlineThinkTagModels, lastRequestedModelId, budget, msg.content));
773
+ const contentEnd = events.length;
774
+ const answerText = events.slice(contentStart).map(event => (event.type === "text_delta" ? event.text : "")).join("");
775
+ // Each call holds the delta event it emitted, so the batch repair sets its arguments later.
776
+ const structuredCalls: { wireName: string; restoredName: string; argumentsText: string; delta: Extract<AdapterEvent, { type: "tool_call_delta" }> }[] = [];
752
777
  const rawToolCalls = msg.tool_calls;
753
778
  if (rawToolCalls !== undefined && rawToolCalls !== null) {
754
779
  if (!Array.isArray(rawToolCalls)) {
@@ -771,11 +796,14 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
771
796
  logInvalidToolCalls("response", rawToolCalls);
772
797
  return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
773
798
  }
774
- events.push({ type: "tool_call_start", id, name: toolNames.restore(name) });
775
- events.push({ type: "tool_call_delta", arguments: args });
776
- events.push({ type: "tool_call_end" });
799
+ const delta: Extract<AdapterEvent, { type: "tool_call_delta" }> = { type: "tool_call_delta", arguments: args };
800
+ structuredCalls.push({ wireName: name, restoredName: toolNames.restore(name), argumentsText: args, delta });
801
+ events.push({ type: "tool_call_start", id, name: toolNames.restore(name) }, delta, { type: "tool_call_end" });
777
802
  }
778
803
  }
804
+ const references = reconcileStructuredToolCalls(structuredCalls, answerText);
805
+ structuredCalls.forEach((call, index) => { call.delta.arguments = references[index]!.argumentsText; });
806
+ reconcileSerializedToolCallEvents(events, contentStart, contentEnd, references, budget);
779
807
  const stopReason = stopReasonFor(choice.finish_reason);
780
808
  events.push({
781
809
  type: "done",
@@ -64,20 +64,27 @@ export function stripStatefulResponsesParams(body: unknown): unknown {
64
64
  }
65
65
 
66
66
  /**
67
- * Remove top-level parameters the ChatGPT backend (`authMode: "forward"`) rejects
68
- * with `{"detail":"Unsupported parameter: …"}` (strict allowlist). Codex CLI never
67
+ * Remove top-level parameters a forward destination rejects with
68
+ * `{"detail":"Unsupported parameter: …"}` (strict allowlist). Codex CLI never
69
69
  * sends these — it controls output length via `reasoning.effort` — but third-party
70
70
  * Responses API clients (GJC, SDK wrappers) include `max_output_tokens` per the
71
- * public spec. `metadata` is likewise absent from the allowlist. No-op when the
72
- * body carries neither field, keeping the common Codex path allocation-free.
71
+ * public spec. No-op when the body carries none of the dropped fields, keeping the
72
+ * common Codex path allocation-free.
73
+ *
74
+ * The canonical ChatGPT backend rejects `metadata`, so the forwarding path removes it on every
75
+ * forward route for compatibility.
76
+ * `max_output_tokens` does not: it is the caller's cost cap on the turn, and a
77
+ * self-hosted or third-party gateway may honour it — dropping it there silently
78
+ * removes the cap. Only the canonical ChatGPT backend rejects it outright, so only
79
+ * `canonical` callers strip it.
73
80
  */
74
- export function stripUnsupportedForwardParams(body: unknown): unknown {
81
+ export function stripUnsupportedForwardParams(body: unknown, canonical: boolean): unknown {
75
82
  if (!isPlainObject(body)) return body;
76
- const hasMot = Object.prototype.hasOwnProperty.call(body, "max_output_tokens");
77
- const hasMeta = Object.prototype.hasOwnProperty.call(body, "metadata");
78
- if (!hasMot && !hasMeta) return body;
79
- const { max_output_tokens: _mot, metadata: _meta, ...rest } = body;
80
- return rest;
83
+ const drop = canonical ? (["metadata", "max_output_tokens"] as const) : (["metadata"] as const);
84
+ if (!drop.some(key => Object.hasOwn(body, key))) return body;
85
+ const next: Record<string, unknown> = { ...body };
86
+ for (const key of drop) delete next[key];
87
+ return next;
81
88
  }
82
89
 
83
90
  /** Sampling controls the canonical ChatGPT backend rejects; other forward gateways accept them. */
@@ -1,4 +1,5 @@
1
1
  import { normalizeRoutedAgentMessages } from "../routed-agent-messages";
2
+ import { nameRoutedIdentity, repairIdentityInResponsesBody, stripRoutedIdentity } from "../identity";
2
3
  import { stripBracketedModelSuffix } from "../openai-chat";
3
4
  import { normalizeOpenCodeGoAdditionalTools } from "../opencode-go-additional-tools";
4
5
  import { isXaiResponsesDestination } from "../../providers/xai-transport";
@@ -33,7 +34,7 @@ import {
33
34
  createAdapterTierMetadata,
34
35
  } from "../../providers/fastwire";
35
36
  import { dropResponsesReasoningInputItems, mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
36
- import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
37
+ import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored, stripRejectedSamplingParams } from "./request-strips";
37
38
  import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
38
39
  import { isPlainObject } from "./internal";
39
40
  import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
@@ -268,6 +269,17 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
268
269
  if (!forward) outBody = normalizeRoutedAgentMessages(outBody, {
269
270
  allowStringContent: isXaiResponsesDestination(provider),
270
271
  });
272
+ // #5217: a sub-agent inherits the parent session's instruction block, so the identity
273
+ // sentence this proxy generated for the PARENT's model rides along to a worker running a
274
+ // different one. On a routed destination it is renamed to that destination — including the
275
+ // model-neutral catalog sentence, which this adapter never names itself; on a native/forward
276
+ // destination our sentence is dropped, because Codex's own identity wording (sent in the
277
+ // client's model_switch block) is the correct one there. Text the proxy did not generate —
278
+ // user turns, tool output, fenced code, provider-native blocks — is untouched.
279
+ outBody = repairIdentityInResponsesBody(
280
+ outBody,
281
+ forward ? stripRoutedIdentity : (text: string) => nameRoutedIdentity(text, parsed.modelId),
282
+ );
271
283
  outBody = mapRoutedResponsesReasoningEffort(outBody, provider, parsed.modelId);
272
284
  // stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
273
285
  // tier write so a force-fast/default decision can never mutate parsed._rawBody.
@@ -312,9 +324,11 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
312
324
  outBody = normalizeResponsesToolResultAdjacency(outBody);
313
325
  }
314
326
  if (forward) {
315
- outBody = stripUnsupportedForwardParams(outBody);
316
- // Only the canonical ChatGPT backend rejects the retired field; a self-hosted or
317
- // third-party forward gateway may still accept it, so this must not be widened.
327
+ // `metadata` is stripped on every forward route for compatibility with the canonical backend.
328
+ // `max_output_tokens` is stripped only when this provider is the canonical backend.
329
+ outBody = stripUnsupportedForwardParams(outBody, isCanonicalOpenAiForwardProvider(provider));
330
+ // Only the canonical ChatGPT backend rejects the canonical-only fields below; a self-hosted
331
+ // or third-party forward gateway may accept them, so this guard must not be widened.
318
332
  if (isCanonicalOpenAiForwardProvider(provider)) {
319
333
  outBody = stripCanonicalForwardSamplingParams(outBody);
320
334
  outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
@@ -323,6 +337,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
323
337
  outBody = normalizeCanonicalForwardContinuationEnvelope(outBody);
324
338
  }
325
339
  } else {
340
+ outBody = stripRejectedSamplingParams(outBody, provider, parsed.modelId);
326
341
  outBody = preferConfiguredHostedTools(
327
342
  outBody,
328
343
  provider,
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
3
3
  import { debugProviderDiagnostic } from "../../lib/debug";
4
+ import { modelInList } from "../../types";
4
5
  import { isPlainObject } from "./internal";
5
6
  import { activateDeferredTool } from "./tool-schema";
6
7
  import { stripOpenAiOnlyWebSearchFields } from "./web-search";
@@ -165,6 +166,30 @@ export function stripCanonicalOnlyTopLevelFields(body: unknown): unknown {
165
166
  return next;
166
167
  }
167
168
 
169
+ /**
170
+ * Sampling fields a model on the provider's `noStopModels` / `noPenaltyModels` list rejects on
171
+ * every wire. Claude inbound translates `stop_sequences` into a Responses `stop`
172
+ * (`src/claude/inbound.ts`), and a direct Responses caller can send penalties. Some listed models
173
+ * only have the Responses wire (xAI grok-4.20-multi-agent-0309 answers Chat Completions with 400),
174
+ * so the Chat adapter's omission cannot cover them. Returns the input unchanged when nothing is
175
+ * removed, so the caller-owned raw body is never mutated.
176
+ */
177
+ export function stripRejectedSamplingParams(
178
+ body: unknown,
179
+ provider: { noStopModels?: string[]; noPenaltyModels?: string[] },
180
+ modelId: string,
181
+ ): unknown {
182
+ if (!isPlainObject(body)) return body;
183
+ const dropStop = Object.hasOwn(body, "stop") && modelInList(provider.noStopModels, modelId);
184
+ const penalties = ["presence_penalty", "frequency_penalty"].filter(field => Object.hasOwn(body, field));
185
+ const dropPenalties = penalties.length > 0 && modelInList(provider.noPenaltyModels, modelId);
186
+ if (!dropStop && !dropPenalties) return body;
187
+ const next = { ...body };
188
+ if (dropStop) delete next.stop;
189
+ if (dropPenalties) for (const field of penalties) delete next[field];
190
+ return next;
191
+ }
192
+
168
193
  /**
169
194
  * When `store` is false, the upstream API does not persist response items. Any item ID
170
195
  * forwarded in `input` is then interpreted as a reference to a stored item that does not
@@ -68,6 +68,10 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
68
68
  * same-shaped successor to 1.2 on the same Zen wire, and an equality check would
69
69
  * have let a Codex-emitted `web_search` body reach the
70
70
  * gateway and come back 400 for every request the moment 1.3 was selected.
71
+ *
72
+ * This list gates the two Zen destinations only. Zen serves nothing but the Contributor
73
+ * tiers there, so the id is a proxy for "this gateway"; the direct Meta host below serves
74
+ * a non-Contributor default and is gated by destination instead.
71
75
  */
72
76
  const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
73
77
  "muse-spark-1.3-contributor",
@@ -76,10 +80,21 @@ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
76
80
  "muse-spark-1.2-contributor-free",
77
81
  ]);
78
82
 
83
+ /**
84
+ * Direct Meta Muse / Meta Model Responses. Its refusal is a gateway schema rule applied
85
+ * before inference, so it holds for every Muse model Meta serves — including the default
86
+ * `muse-spark-1.3`, which no Contributor-shaped list contains. Gating that host on model
87
+ * membership sent `search_content_types` through on every Codex `web_search` turn and 400ed
88
+ * the whole request. Same reading as the 64-char tool-name rewrite in
89
+ * `src/responses/muse-tool-name-alias.ts`, which is host-scoped and deliberately not
90
+ * model-gated for this reason.
91
+ */
92
+ const MUSE_SPARK_STRICT_ANY_MODEL_DESTINATION = "https://api.meta.ai/v1/responses";
93
+
79
94
  const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([
80
95
  "https://opencode.ai/zen/v1/responses",
81
96
  "https://opencode.ai/zen/go/v1/responses",
82
- "https://api.meta.ai/v1/responses",
97
+ MUSE_SPARK_STRICT_ANY_MODEL_DESTINATION,
83
98
  ]);
84
99
 
85
100
  const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [
@@ -94,7 +109,8 @@ const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [
94
109
  * malformed, credentialed, or parameterized destinations keep their original body
95
110
  * instead of assuming this gateway contract. Keep the rejected names together so a
96
111
  * newly identified field is a one-line compatibility update rather than another
97
- * bespoke rewrite.
112
+ * bespoke rewrite. The destination is the whole predicate on direct Meta; the Zen
113
+ * wires additionally require a known Contributor id.
98
114
  */
99
115
  export function stripMuseSparkUnsupportedWebSearchFields(
100
116
  body: unknown,
@@ -102,8 +118,6 @@ export function stripMuseSparkUnsupportedWebSearchFields(
102
118
  responseUrl: string,
103
119
  ): unknown {
104
120
  if (!isPlainObject(body)) return body;
105
- if (typeof modelId !== "string") return body;
106
- if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;
107
121
  let destination: string;
108
122
  try {
109
123
  const url = new URL(responseUrl);
@@ -113,6 +127,10 @@ export function stripMuseSparkUnsupportedWebSearchFields(
113
127
  return body;
114
128
  }
115
129
  if (!MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS.has(destination)) return body;
130
+ if (
131
+ destination !== MUSE_SPARK_STRICT_ANY_MODEL_DESTINATION
132
+ && (typeof modelId !== "string" || !MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase()))
133
+ ) return body;
116
134
 
117
135
  const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
118
136
  let changed = false;
@@ -3,6 +3,8 @@ import { clearCodingAgentBinaryCache, resolveProfileByBaseUrl, type CodingAgentP
3
3
  /** Official Qoder CLI profile. Region variants are separate profiles and credentials. */
4
4
  export interface QoderProfile extends CodingAgentProviderProfile {
5
5
  family: "qoder";
6
+ /** Required for this family: the CLI is driven with a personal access token. */
7
+ tokenEnv: string;
6
8
  }
7
9
 
8
10
  export const QODER_GLOBAL_PROFILE: QoderProfile = {
@@ -1,6 +1,7 @@
1
1
  import { createAnthropicAdapter } from "./anthropic";
2
2
  import { createAzureAdapter } from "./azure";
3
3
  import type { ProviderAdapter } from "./base";
4
+ import { createClaudeCliAdapter } from "./claude-cli/adapter";
4
5
  import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay";
5
6
  import { createCodeBuddyAdapter } from "./codebuddy/adapter";
6
7
  import { createQoderAdapter } from "./qoder/adapter";
@@ -138,6 +139,13 @@ export const ADAPTER_REGISTRY = {
138
139
  contractParent: "codebuddy",
139
140
  create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createQoderAdapter(provider),
140
141
  },
142
+ "claude-cli": {
143
+ // Claude Code speaks the same stream-json contract this repo already parses for CodeBuddy and
144
+ // Qoder, so the contract is inherited rather than restated. The family owns its args and env,
145
+ // and the CLI owns the credential: the adapter stores and injects none.
146
+ contractParent: "codebuddy",
147
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createClaudeCliAdapter(provider),
148
+ },
141
149
  } as const satisfies Record<string, AdapterDefinition>;
142
150
 
143
151
  export type AdapterId = keyof typeof ADAPTER_REGISTRY;
@@ -152,6 +152,7 @@ async function* replay(
152
152
 
153
153
  export async function preflightAdapterEvents(
154
154
  source: AsyncIterable<AdapterEvent>,
155
+ classifyFirstEvent?: (event: AdapterEvent) => Extract<AdapterEvent, { type: "error" }> | undefined,
155
156
  ): Promise<AdapterEventPreflight> {
156
157
  const iterator = source[Symbol.asyncIterator]();
157
158
  const buffered: AdapterEvent[] = [];
@@ -165,6 +166,12 @@ export async function preflightAdapterEvents(
165
166
  if (buffered.length > PREFLIGHT_HEARTBEAT_RETAIN_LIMIT) buffered.shift();
166
167
  continue;
167
168
  }
169
+ const classifiedError = replayUnsafe ? undefined : classifyFirstEvent?.(next.value);
170
+ if (classifiedError) {
171
+ buffered.push(classifiedError);
172
+ await iterator.return?.();
173
+ return { stream: replay(buffered, iterator), error: classifiedError, empty: false, replayUnsafe };
174
+ }
168
175
  buffered.push(next.value);
169
176
  if (next.value.type === "error") {
170
177
  await iterator.return?.();
@@ -45,6 +45,7 @@ import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolC
45
45
  import type { OutputItem, StringChunks } from "./internal";
46
46
  import { bridgeToResponsesSSE } from "./sse";
47
47
 
48
+ /** Build a buffered Responses result within a caller-owned or temporary translator budget. */
48
49
  export function buildResponseJSON(
49
50
  events: AdapterEvent[],
50
51
  modelId: string,
@@ -68,13 +69,14 @@ export function buildResponseJSON(
68
69
  }
69
70
  }
70
71
 
72
+ /** Fold adapter events into a Responses result while enforcing the requested tool boundary. */
71
73
  function buildResponseJSONWithBudget(
72
74
  events: AdapterEvent[],
73
75
  modelId: string,
74
76
  options?: {
75
77
  hideThinkingSummary?: boolean;
76
78
  toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
77
- /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
79
+ /** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
78
80
  declaredToolNames?: ReadonlySet<string>;
79
81
  /** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
80
82
  enforceDeclaredToolNames?: boolean;
@@ -443,9 +445,9 @@ function buildResponseJSONWithBudget(
443
445
  flushToolCall();
444
446
  const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
445
447
  if (
446
- options?.declaredToolNames
447
- && options.enforceDeclaredToolNames !== false
448
- && !options.declaredToolNames.has(effectiveName)
448
+ (options?.enforceDeclaredToolNames === true || options?.declaredToolNames != null)
449
+ && options?.enforceDeclaredToolNames !== false
450
+ && !options?.declaredToolNames?.has(effectiveName)
449
451
  ) {
450
452
  errorEvent = {
451
453
  type: "error",
package/src/bridge/sse.ts CHANGED
@@ -61,6 +61,7 @@ function responseError(status: number, type: string, message: string): OcxErrorP
61
61
 
62
62
  export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
63
63
 
64
+ /** Stream adapter events as Responses frames, applying tool authorization before relaying calls. */
64
65
  export function bridgeToResponsesSSE(
65
66
  events: AsyncIterable<AdapterEvent>,
66
67
  modelId: string,
@@ -92,13 +93,14 @@ export function bridgeToResponsesSSE(
92
93
  * from this callback instead of re-parsing the bridged SSE.
93
94
  */
94
95
  onUsage?: (usage: OcxUsage | undefined) => void;
95
- /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
96
+ /** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
96
97
  declaredToolNames?: ReadonlySet<string>;
97
98
  /**
98
99
  * Whether `declaredToolNames` is an authorization boundary this proxy enforces, or only the
99
100
  * catalog used to normalize provider-invented names back to declared ones.
100
101
  *
101
- * Defaults to enforcing. The chat and Anthropic inbound wires set it false: those specs make
102
+ * Defaults to enforcing when a catalog is supplied. Explicit true also fails closed when the
103
+ * catalog is absent. The chat and Anthropic inbound wires set it false: those specs make
102
104
  * the server relay a tool call and leave execution or refusal to the client's own runner, and
103
105
  * harnesses on them legitimately defer part of their catalog (#4735).
104
106
  *
@@ -1010,9 +1012,9 @@ export function bridgeToResponsesSSE(
1010
1012
  const mapped = toolNsMap?.get(effectiveName);
1011
1013
  const realName = mapped?.name ?? effectiveName;
1012
1014
  if (
1013
- options?.declaredToolNames
1014
- && options.enforceDeclaredToolNames !== false
1015
- && !options.declaredToolNames.has(effectiveName)
1015
+ (options?.enforceDeclaredToolNames === true || options?.declaredToolNames != null)
1016
+ && options?.enforceDeclaredToolNames !== false
1017
+ && !options?.declaredToolNames?.has(effectiveName)
1016
1018
  ) {
1017
1019
  const failure = responseError(
1018
1020
  502,