@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925

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-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.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-D5eN1TID.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
@@ -18,6 +18,7 @@ import { lookupReplayThoughtSignature } from "./thought-signature-replay";
18
18
  import { compactionItemToText, isCompactionItemType } from "./compaction";
19
19
  import { previousResponseReplayPrefixLength } from "./state";
20
20
  import { decodeReasoningEnvelope } from "./reasoning-envelope";
21
+ import { hasRoutedIdentity, nameRoutedIdentity } from "../adapters/identity";
21
22
  import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
22
23
  import { buildImageTool, extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
23
24
  import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
@@ -121,6 +122,33 @@ export function hasValidatedActiveReasoningEffort(options: Pick<OcxRequestOption
121
122
  }
122
123
 
123
124
 
125
+ /**
126
+ * Name this request's destination in an instruction text inherited from a stored session block.
127
+ *
128
+ * Returns the input unchanged when it carries no sentence of ours, which is every request that has
129
+ * not gone through a sub-agent spawn. The catalog block is model-neutral on disk (#5217) and some
130
+ * routed adapters build their own system text instead of calling `identifyRoutedModel`, so both the
131
+ * neutral line and a sentence naming an earlier model are handled here — request time is the first
132
+ * point where the destination model is known.
133
+ */
134
+ function nameDestinationText(text: string, modelId: string): string {
135
+ return hasRoutedIdentity(text) ? nameRoutedIdentity(text, modelId) : text;
136
+ }
137
+
138
+ function nameDestinationContent(
139
+ content: string | OcxContentPart[],
140
+ modelId: string,
141
+ ): string | OcxContentPart[] {
142
+ if (typeof content === "string") return nameDestinationText(content, modelId);
143
+ let changed = false;
144
+ const parts = content.map((part) => {
145
+ if (part.type !== "text" || !hasRoutedIdentity(part.text)) return part;
146
+ changed = true;
147
+ return { ...part, text: nameRoutedIdentity(part.text, modelId) };
148
+ });
149
+ return changed ? parts : content;
150
+ }
151
+
124
152
  export function parseRequest(
125
153
  body: unknown,
126
154
  parseOptions?: { replayCacheScope?: OcxReasoningReplayScopeRef },
@@ -168,7 +196,10 @@ export function parseRequest(
168
196
  let continuationConversationMessageIndex: number | undefined;
169
197
 
170
198
  if (typeof data.instructions === "string" && data.instructions.length > 0) {
171
- systemPrompt.push(data.instructions);
199
+ // #5217: this is the stored session instruction block. A sub-agent spawned on a DIFFERENT model
200
+ // receives the parent's copy verbatim, so the identity sentence inside it names the parent
201
+ // unless it is renamed here, where the destination model is known.
202
+ systemPrompt.push(nameDestinationText(data.instructions, data.model));
172
203
  }
173
204
 
174
205
  if (typeof data.input === "string") {
@@ -274,14 +305,25 @@ export function parseRequest(
274
305
  const flat = typeof text === "string"
275
306
  ? text
276
307
  : text.map(p => (p.type === "text" || p.type === "document" ? p.text : "")).join("");
277
- if (flat.length > 0) systemPrompt.push(flat);
308
+ // #5217: a system-role item is instruction text, exactly like `instructions` and a
309
+ // developer item, so it needs the same request-time naming — it lands in the system
310
+ // block verbatim, and Codex replays the parent's copy to a sub-agent on another model.
311
+ if (flat.length > 0) systemPrompt.push(nameDestinationText(flat, data.model));
278
312
  break;
279
313
  }
280
314
  case "user":
281
315
  case "developer": {
282
316
  pendingReasoning.length = 0;
283
317
  const content = inputContentParts(msg.content);
284
- messages.push({ role: msg.role, content, timestamp: now });
318
+ messages.push({
319
+ role: msg.role,
320
+ // #5217: Codex replays the PARENT session's instruction block as the worker's
321
+ // developer message, so a sub-agent on another model inherits an identity sentence
322
+ // naming the parent. Only this proxy's own sentence is rewritten, and only on a
323
+ // developer item; user turns are the caller's content and stay byte-identical.
324
+ content: msg.role === "developer" ? nameDestinationContent(content, data.model) : content,
325
+ timestamp: now,
326
+ });
285
327
  break;
286
328
  }
287
329
  case "assistant": {
package/src/router.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  import { registryModelIdKeys } from "./providers/registry/model-ids";
27
27
  import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
28
28
  import { cloneFastWire } from "./providers/fastwire";
29
+ import { fastSwitchOff } from "./providers/fast-opt-in";
29
30
  import {
30
31
  providerMatchesRegistryTransportWithStaticGuards,
31
32
  providerSupportsLiveModelDiscovery,
@@ -339,6 +340,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
339
340
  const noReasoningModels = staticPolicy.noReasoningModels;
340
341
  const noTemperatureModels = staticPolicy.noTemperatureModels;
341
342
  const noTopPModels = staticPolicy.noTopPModels;
343
+ const noStopModels = staticPolicy.noStopModels;
342
344
  const noPenaltyModels = staticPolicy.noPenaltyModels;
343
345
  const noJsonSchemaModels = staticPolicy.noJsonSchemaModels;
344
346
  const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels;
@@ -392,6 +394,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
392
394
  ...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined
393
395
  ? { supportsServiceTier: registryEntry.supportsServiceTier }
394
396
  : {}),
397
+ // An off Fast switch is a provider-wide denial on the runtime provider, so a Fast policy
398
+ // resolved without the provider name still refuses (providerFastSwitchOff).
399
+ ...(fastSwitchOff(provider, registryEntry) ? { supportsServiceTier: false } : {}),
395
400
  // Registry-only web-search capability: without this backfill a saved provider row reaches
396
401
  // the Responses adapter with the flag `undefined`, so the capability gate added in #2262
397
402
  // reads "unclassified" and forwards Codex's OpenAI-only `web_search` config fields. xAI
@@ -482,6 +487,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
482
487
  ...(noReasoningModels ? { noReasoningModels } : {}),
483
488
  ...(noTemperatureModels ? { noTemperatureModels } : {}),
484
489
  ...(noTopPModels ? { noTopPModels } : {}),
490
+ ...(noStopModels ? { noStopModels } : {}),
485
491
  ...(noPenaltyModels ? { noPenaltyModels } : {}),
486
492
  ...(noJsonSchemaModels ? { noJsonSchemaModels } : {}),
487
493
  ...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}),
@@ -744,6 +744,12 @@ export function providerManagementConfigError(
744
744
  // validation and then rejected by the seed comparison, so canonical OpenAI could never
745
745
  // set OR clear it — the value was admitted and then refused in the same request.
746
746
  delete canonicalCandidate.annotateEmptyToolOutputs;
747
+ // Canonical ChatGPT keeps WebSocket as the default, but an operator may
748
+ // select the existing HTTP/SSE path without changing its auth or endpoint.
749
+ if (raw.upstreamWebsocket !== undefined) {
750
+ if (raw.upstreamWebsocket !== false) return "provider openai upstreamWebsocket must be false or omitted";
751
+ delete canonicalCandidate.upstreamWebsocket;
752
+ }
747
753
  const canonical = seed && (options?.allowOperatorOverlays
748
754
  ? matchesCanonicalProviderSeed(canonicalCandidate, seed)
749
755
  : sameCanonicalProviderSeed(canonicalCandidate, seed));
@@ -950,6 +956,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
950
956
  mcpMaxResultBytes: "editor",
951
957
  modelAdapters: "editor",
952
958
  fastWire: "editor",
959
+ fastEnabled: "editor",
953
960
  baseUrl: "editor",
954
961
  responsesPath: "editor",
955
962
  chatCompletionsPath: "editor",
@@ -1042,6 +1049,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1042
1049
  noReasoningModels: "editor",
1043
1050
  noTemperatureModels: "editor",
1044
1051
  noTopPModels: "editor",
1052
+ noStopModels: "editor",
1045
1053
  noPenaltyModels: "editor",
1046
1054
  noStructuredOutputModels: "editor",
1047
1055
  noJsonSchemaModels: "editor",
@@ -328,7 +328,9 @@ async function handleChatCompletionsWithBudget(
328
328
  // This enrichment is optional for routed/non-main providers. If native main
329
329
  // is fenced, omit it and let auth-context reject only a final physical-main
330
330
  // selection while healthy pool/provider routes continue.
331
- if (tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) {
331
+ const isCanonicalPool = settledRoute && isCanonicalOpenAiForwardProvider(settledRoute.provider)
332
+ && settledRoute.codexAccountMode === "pool";
333
+ if (!isCanonicalPool && tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) {
332
334
  try {
333
335
  const { getMainAccountToken } = await import("../codex/main-account");
334
336
  const token = getMainAccountToken();
@@ -17,12 +17,14 @@ import { jsonUtf8Bytes } from "../lib/json-byte-size";
17
17
  import { sseFieldValue } from "../lib/sse-decoder";
18
18
  import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
19
19
  import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
20
+ import { createToolCallIdAllocator } from "../adapters/tool-call-id";
20
21
  import { AnthropicRequestError, DesktopModelMappingUnavailableError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
21
22
  import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
22
- import { resolveAlias, claudeCodeNativeAlias } from "../claude/alias";
23
+ import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
23
24
  import { recordDesktopRequest } from "../claude/desktop-health";
24
25
  import { stripOneMillionMarker } from "../claude/context-windows";
25
26
  import { captureClaudeInbound } from "../claude/inbound-debug";
27
+ import { claudeCodeForIngress } from "../claude/intercept/model-bindings";
26
28
  import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
27
29
  import {
28
30
  applyReplayRefusalClientHeaders,
@@ -85,6 +87,11 @@ import {
85
87
 
86
88
  type Rec = Record<string, unknown>;
87
89
 
90
+ /** Which listener a Claude Messages request arrived on. Only the intercept ingress honours bindings. */
91
+ export interface ClaudeIngressOptions {
92
+ claudeIntercept?: boolean;
93
+ }
94
+
88
95
  /**
89
96
  * Decode a Claude selector that may carry the fast marker.
90
97
  *
@@ -112,7 +119,8 @@ function decodeClaudeFastSelector(raw: string, cc?: OcxConfig["claudeCode"]): st
112
119
  function decodeFablePickerAlias(raw: string, cc?: OcxConfig["claudeCode"]): string {
113
120
  const decoded = resolveInboundModel(raw, cc);
114
121
  if (!decoded.startsWith("claude-fable-")) return raw;
115
- return claudeCodeNativeAlias(decoded) === raw ? decoded : raw;
122
+ // A picker value saved before the ocx-claude spelling keeps the native passthrough too.
123
+ return claudeCodeNativeAlias(decoded) === raw || legacyAliasForNative(decoded) === raw ? decoded : raw;
116
124
  }
117
125
 
118
126
  function isRec(v: unknown): v is Rec {
@@ -195,8 +203,9 @@ function wantsNativePassthrough(
195
203
  config: OcxConfig,
196
204
  requestPolicy: RequestPolicyView,
197
205
  model: unknown,
206
+ cc: OcxConfig["claudeCode"] = config.claudeCode,
198
207
  ): model is string {
199
- if (config.claudeCode?.nativePassthrough === false) return false;
208
+ if (cc?.nativePassthrough === false) return false;
200
209
  if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
201
210
  // Authorization and x-api-key both belong to the upstream on this branch. An exposed listener
202
211
  // therefore requires the dedicated admission header even though the routed Messages surface
@@ -207,7 +216,8 @@ function wantsNativePassthrough(
207
216
  }
208
217
  if (!hasAnthropicNativeCredential(req, config)) return false;
209
218
  // An alias or modelMap hit means the user asked for a ROUTED model: translate instead.
210
- return resolveInboundModel(model, config.claudeCode) === model;
219
+ // `cc` carries first-party intercept bindings for requests on the claude-intercept ingress.
220
+ return resolveInboundModel(model, cc) === model;
211
221
  }
212
222
 
213
223
  function shouldForwardNativeHeader(name: string, value: string, config: OcxConfig): boolean {
@@ -406,6 +416,44 @@ export function tapAnthropicSseForLog(
406
416
  });
407
417
  }
408
418
 
419
+ /**
420
+ * `tool_use.id` / `tool_result.tool_use_id` must match Anthropic's wire contract
421
+ * (`^[a-zA-Z0-9_-]+$`, <=64 chars). Third-party models mint other shapes — Devin's
422
+ * swe-2 emits `Bash:0#<hex>` — and a session history carrying them 400s the moment it
423
+ * is switched to a native Anthropic model ("messages.N.content.M.tool_use.id: String
424
+ * should match pattern"). The adapter path normalizes these via
425
+ * adapters/tool-call-id.ts (#1780); this passthrough bypasses that adapter, so the same
426
+ * allocator runs here. Stateless per request: conforming ids pass through byte-identical
427
+ * (prompt-cache keys untouched), rewritten ids keep call/result pairing stable.
428
+ * An empty id has no representable wire form, and forwarding `""` is what Anthropic
429
+ * rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
430
+ */
431
+ function sanitizePassthroughToolCallIds(messages: unknown[]): void {
432
+ const blocks: Rec[] = [];
433
+ for (const message of messages) {
434
+ if (!isRec(message) || !Array.isArray(message.content)) continue;
435
+ for (const block of message.content) if (isRec(block)) blocks.push(block);
436
+ }
437
+ const fieldOf = (block: Rec): "id" | "tool_use_id" | undefined => {
438
+ if (typeof block.type !== "string") return undefined;
439
+ if (block.type.endsWith("tool_use")) return "id";
440
+ if (block.type.endsWith("tool_result")) return "tool_use_id";
441
+ return undefined;
442
+ };
443
+ const callIds = createToolCallIdAllocator();
444
+ for (const block of blocks) {
445
+ const field = fieldOf(block);
446
+ if (field && typeof block[field] === "string") callIds.reserve(block[field] as string);
447
+ }
448
+ for (const block of blocks) {
449
+ const field = fieldOf(block);
450
+ if (!field || typeof block[field] !== "string") continue;
451
+ const wire = callIds.allocate(block[field] as string);
452
+ if (wire === undefined) throw new AnthropicRequestError(`${block.type} block has an empty ${field}`);
453
+ block[field] = wire;
454
+ }
455
+ }
456
+
409
457
  async function anthropicNativePassthrough(
410
458
  req: Request,
411
459
  config: OcxConfig,
@@ -435,6 +483,7 @@ async function anthropicNativePassthrough(
435
483
  if (Array.isArray(body.messages)) {
436
484
  await normalizeAnthropicImages(body.messages, { abortSignal: req.signal });
437
485
  enforceAnthropicImageLimits(body.messages);
486
+ sanitizePassthroughToolCallIds(body.messages);
438
487
  }
439
488
  const headers = new Headers();
440
489
  req.headers.forEach((value, name) => {
@@ -637,11 +686,12 @@ export async function handleClaudeMessages(
637
686
  logCtx: RequestLogContext,
638
687
  logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
639
688
  requestPolicy: RequestPolicyView = config,
689
+ ingress: ClaudeIngressOptions = {},
640
690
  ): Promise<Response> {
641
691
  const translatorBudget = createTranslatorBudget();
642
692
  try {
643
693
  return finalizeTranslatorBudgetResponse(
644
- await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy),
694
+ await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy, ingress),
645
695
  translatorBudget,
646
696
  );
647
697
  } catch (error) {
@@ -663,6 +713,7 @@ async function handleClaudeMessagesWithBudget(
663
713
  translatorBudget: TranslatorBudget,
664
714
  logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
665
715
  requestPolicy: RequestPolicyView = config,
716
+ ingress: ClaudeIngressOptions = {},
666
717
  ): Promise<Response> {
667
718
  logCtx.surface = "claude";
668
719
  const disabled = claudeInboundDisabled(config);
@@ -670,6 +721,8 @@ async function handleClaudeMessagesWithBudget(
670
721
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
671
722
  return disabled;
672
723
  }
724
+ // Model resolution reads this view; every other claudeCode setting keeps reading `config`.
725
+ const cc = claudeCodeForIngress(config.claudeCode, ingress.claudeIntercept === true);
673
726
 
674
727
  let anthropicBody: unknown;
675
728
  let internalBody: Rec;
@@ -698,7 +751,7 @@ async function handleClaudeMessagesWithBudget(
698
751
  }
699
752
  }
700
753
  if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
701
- anthropicBody.model = decodeFablePickerAlias(anthropicBody.model, config.claudeCode);
754
+ anthropicBody.model = decodeFablePickerAlias(anthropicBody.model, cc);
702
755
  }
703
756
  if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
704
757
  requestedModel = anthropicBody.model;
@@ -709,7 +762,7 @@ async function handleClaudeMessagesWithBudget(
709
762
  ({ fastRow, effortRow } = parseSyntheticRowId(
710
763
  requestedModel,
711
764
  config,
712
- () => decodeClaudeFastSelector(requestedModel, config.claudeCode),
765
+ () => decodeClaudeFastSelector(requestedModel, cc),
713
766
  ));
714
767
  if (effortRow) {
715
768
  anthropicBody.model = effortRow.baseId;
@@ -723,7 +776,7 @@ async function handleClaudeMessagesWithBudget(
723
776
  "messages",
724
777
  anthropicBody,
725
778
  isRec(anthropicBody) && typeof anthropicBody.model === "string"
726
- ? resolveInboundModel(anthropicBody.model, config.claudeCode)
779
+ ? resolveInboundModel(anthropicBody.model, cc)
727
780
  : undefined,
728
781
  req.headers.get("anthropic-beta") ?? undefined,
729
782
  );
@@ -744,7 +797,7 @@ async function handleClaudeMessagesWithBudget(
744
797
  // caller's body with the caller's credential and never runs the Anthropic adapter, so the
745
798
  // proxy-owned `speed` + beta (anthropic-speed wire) and its usage.speed observation would be
746
799
  // silently skipped. Translation reaches the adapter, which owns both.
747
- if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
800
+ if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model, cc)) {
748
801
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
749
802
  }
750
803
  // Capture source semantics before effort rewriting or translation drops fields.
@@ -780,7 +833,7 @@ async function handleClaudeMessagesWithBudget(
780
833
  };
781
834
  delete anthropicBody.thinking;
782
835
  }
783
- const translation = anthropicToResponsesTranslation(anthropicBody, config.claudeCode, translatorBudget);
836
+ const translation = anthropicToResponsesTranslation(anthropicBody, cc, translatorBudget);
784
837
  internalBody = translation.body;
785
838
  // The Anthropic translator builds its body from model/input/store/stream plus sampling
786
839
  // fields only, so the caller intent is applied to the TRANSLATED body rather than the
@@ -1212,9 +1265,11 @@ export async function handleClaudeCountTokens(
1212
1265
  req: Request,
1213
1266
  config: OcxConfig,
1214
1267
  requestPolicy: RequestPolicyView = config,
1268
+ ingress: ClaudeIngressOptions = {},
1215
1269
  ): Promise<Response> {
1216
1270
  const disabled = claudeInboundDisabled(config);
1217
1271
  if (disabled) return disabled;
1272
+ const cc = claudeCodeForIngress(config.claudeCode, ingress.claudeIntercept === true);
1218
1273
 
1219
1274
  let body: unknown;
1220
1275
  const translatorBudget = createTranslatorBudget();
@@ -1246,20 +1301,20 @@ export async function handleClaudeCountTokens(
1246
1301
  model = stripOneMillionMarker(countRoute);
1247
1302
  raw.model = model;
1248
1303
  }
1249
- model = decodeFablePickerAlias(model, config.claudeCode);
1304
+ model = decodeFablePickerAlias(model, cc);
1250
1305
  raw.model = model;
1251
1306
  // Fast-only: count_tokens never parsed an effort row, so it must not start. It returns a
1252
1307
  // token estimate and sends no tier, so only the IDENTITY is corrected - without this the
1253
1308
  // synthetic id reaches native passthrough as a model Anthropic has never heard of.
1254
1309
  const countFastRow = parseFastOnlyRowId(
1255
- config, () => decodeClaudeFastSelector(model, config.claudeCode),
1310
+ config, () => decodeClaudeFastSelector(model, cc),
1256
1311
  );
1257
1312
  if (countFastRow) {
1258
1313
  model = countFastRow.baseId;
1259
1314
  raw.model = model;
1260
1315
  }
1261
- captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
1262
- if (wantsNativePassthrough(req, config, requestPolicy, model)) {
1316
+ captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, cc), req.headers.get("anthropic-beta") ?? undefined);
1317
+ if (wantsNativePassthrough(req, config, requestPolicy, model, cc)) {
1263
1318
  return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
1264
1319
  }
1265
1320
  const inputTokens = estimateClaudeRequestTokens(raw, model);
@@ -0,0 +1,120 @@
1
+ import { ToolEnvelopeEchoFilter, stripToolEnvelopeEcho } from "../lib/tool-envelope-echo-filter";
2
+ import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
3
+
4
+ function record(value: unknown): value is Record<string, unknown> {
5
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+
8
+ function outputText(value: unknown, replacement?: (index: number, content: number, text: string) => string): void {
9
+ if (!record(value) || !Array.isArray(value.output)) return;
10
+ value.output.forEach((item: unknown, index: number) => {
11
+ if (!record(item) || !Array.isArray(item.content)) return;
12
+ item.content.forEach((part: unknown, content: number) => {
13
+ if (record(part) && part.type === "output_text" && typeof part.text === "string") {
14
+ part.text = replacement?.(index, content, part.text) ?? stripToolEnvelopeEcho(part.text);
15
+ }
16
+ });
17
+ });
18
+ }
19
+
20
+ /**
21
+ * The echo only appears when the model has seen a replayed tool envelope: a tool call or tool output
22
+ * in this request's input, or a stored conversation continued through previous_response_id (its history
23
+ * lives upstream and may hold tool results). A first turn with neither is never filtered, so a
24
+ * legitimate answer that starts a line with "[Tool Result]" reaches the client intact.
25
+ */
26
+ export function responsesRequestMayReplayToolOutput(rawBody: unknown): boolean {
27
+ if (!record(rawBody)) return false;
28
+ if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id !== "") return true;
29
+ if (!Array.isArray(rawBody.input)) return false;
30
+ // A tool call counts too: xAI's paired tool-result repair answers a dangling call with a synthetic
31
+ // output on the outbound request, so the model sees a tool result the caller never sent.
32
+ return rawBody.input.some(item => record(item) && typeof item.type === "string" && /_call(_output)?$/.test(item.type));
33
+ }
34
+
35
+ /** Completed non-streaming Responses bodies use the same line-aware marker rule. */
36
+ export function stripGrokUpstreamEnvelopeEchoFromResponsesJson(json: string): string {
37
+ try {
38
+ const value = JSON.parse(json) as unknown;
39
+ outputText(value);
40
+ return JSON.stringify(value);
41
+ } catch {
42
+ return json;
43
+ }
44
+ }
45
+
46
+ type TextState = {
47
+ filter: ToolEnvelopeEchoFilter;
48
+ delivered: string;
49
+ lastDeltaBlock: string;
50
+ lastDeltaEvent: Record<string, unknown>;
51
+ };
52
+
53
+ /** Keep xAI text deltas, done events, terminal snapshots and replay state in agreement. */
54
+ export function createGrokUpstreamEnvelopeEchoBlockRewrite(
55
+ onCompletedResponse?: (response: Record<string, unknown>) => void,
56
+ ): SseBlockRewrite {
57
+ const states = new Map<string, TextState>();
58
+ const key = (output: unknown, content: unknown): string =>
59
+ `${typeof output === "number" ? output : 0}:${typeof content === "number" ? content : 0}`;
60
+ const flush = (state: TextState): string[] => {
61
+ const suffix = state.filter.finish();
62
+ if (!suffix) return [];
63
+ state.delivered += suffix;
64
+ return [replaceSseDataPayload(state.lastDeltaBlock, JSON.stringify({ ...state.lastDeltaEvent, delta: suffix }))];
65
+ };
66
+ return (block) => {
67
+ const payload = sseDataPayload(block);
68
+ if (payload === null) return [block];
69
+ if (payload === "[DONE]") return [...[...states.values()].flatMap(flush), block];
70
+ let event: unknown;
71
+ try { event = JSON.parse(payload); } catch { return [block]; }
72
+ if (!record(event) || typeof event.type !== "string") return [block];
73
+ if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
74
+ const id = key(event.output_index, event.content_index);
75
+ let state = states.get(id);
76
+ if (!state) {
77
+ state = { filter: new ToolEnvelopeEchoFilter(), delivered: "", lastDeltaBlock: block, lastDeltaEvent: event };
78
+ states.set(id, state);
79
+ }
80
+ state.lastDeltaBlock = block;
81
+ state.lastDeltaEvent = event;
82
+ const delta = state.filter.feed(event.delta);
83
+ state.delivered += delta;
84
+ if (!delta) return [];
85
+ return [replaceSseDataPayload(block, JSON.stringify({ ...event, delta }))];
86
+ }
87
+ if (event.type === "response.output_text.done") {
88
+ const state = states.get(key(event.output_index, event.content_index));
89
+ if (!state) {
90
+ if (typeof event.text !== "string") return [block];
91
+ event.text = stripToolEnvelopeEcho(event.text);
92
+ return [replaceSseDataPayload(block, JSON.stringify(event))];
93
+ }
94
+ const pending = flush(state);
95
+ event.text = state.delivered;
96
+ return [...pending, replaceSseDataPayload(block, JSON.stringify(event))];
97
+ }
98
+ if (event.type === "response.output_item.done" && record(event.item)) {
99
+ const index = typeof event.output_index === "number" ? event.output_index : 0;
100
+ const pending: string[] = [];
101
+ if (Array.isArray(event.item.content)) {
102
+ event.item.content.forEach((part: unknown, content: number) => {
103
+ if (!record(part) || part.type !== "output_text" || typeof part.text !== "string") return;
104
+ const state = states.get(key(index, content));
105
+ if (state) { pending.push(...flush(state)); part.text = state.delivered; }
106
+ else part.text = stripToolEnvelopeEcho(part.text);
107
+ });
108
+ }
109
+ return [...pending, replaceSseDataPayload(block, JSON.stringify(event))];
110
+ }
111
+ if (event.type === "response.completed" && record(event.response)) {
112
+ const pending = [...states.values()].flatMap(flush);
113
+ outputText(event.response, (index, content, text) =>
114
+ states.get(key(index, content))?.delivered ?? stripToolEnvelopeEcho(text));
115
+ onCompletedResponse?.(event.response);
116
+ return [...pending, replaceSseDataPayload(block, JSON.stringify(event))];
117
+ }
118
+ return [block];
119
+ };
120
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Server } from "bun";
2
+ import type { PickerRouteInput } from "../../claude/intercept/picker-models";
2
3
  import {
3
4
  startClaudeIntercept,
4
5
  type ClaudeInterceptHandle,
@@ -18,6 +19,28 @@ export interface ClaudeInterceptLifecycle<T> {
18
19
  stop(): Promise<void>;
19
20
  }
20
21
 
22
+ /**
23
+ * Routes for Desktop's Code-tab picker: the same inputs `/api/sync` gives the gateway profile
24
+ * (src/server/management/config-routes.ts), read from the persisted config at call time. Dynamic
25
+ * imports keep the catalog and discovery off the synchronous startup path.
26
+ */
27
+ export async function loadPickerRoutesFromCatalog(): Promise<PickerRouteInput> {
28
+ const [{ loadConfig }, { fetchAllModels }, catalog] = await Promise.all([
29
+ import("../../config"),
30
+ import("../management-api"),
31
+ import("../../codex/catalog"),
32
+ ]);
33
+ const config = loadConfig();
34
+ const models = await fetchAllModels(config);
35
+ return {
36
+ nativeSlugs: [...catalog.desktopVisibleNativeSlugs(config)],
37
+ routedModels: catalog.filterCatalogVisibleModels(models, config)
38
+ .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })),
39
+ ...(config.claudeCode?.desktopProfile ? { profile: config.claudeCode.desktopProfile } : {}),
40
+ nativeContextCap: catalog.nativeContextLimits(config),
41
+ };
42
+ }
43
+
21
44
  export function createClaudeInterceptLifecycle<T>(): ClaudeInterceptLifecycle<T> {
22
45
  let listener: Server<T> | null = null;
23
46
  let pending: Promise<ClaudeInterceptHandle<T> | null> = Promise.resolve(null);
@@ -27,6 +50,7 @@ export function createClaudeInterceptLifecycle<T>(): ClaudeInterceptLifecycle<T>
27
50
  const dispatch = options.dispatch;
28
51
  pending = startClaudeIntercept<T>({
29
52
  ...options,
53
+ loadPickerRoutes: options.loadPickerRoutes ?? loadPickerRoutesFromCatalog,
30
54
  dispatch: (req, requestServer) => {
31
55
  listener ??= requestServer;
32
56
  return dispatch(req, requestServer);
@@ -1475,7 +1475,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1475
1475
  return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
1476
1476
  }
1477
1477
  return runAdmittedHttpTurn(req, policy, async () => withCors(
1478
- await handleClaudeCountTokens(req, config, policy),
1478
+ await handleClaudeCountTokens(req, config, policy, { claudeIntercept: ingress === "claude-intercept" }),
1479
1479
  req,
1480
1480
  policy,
1481
1481
  ));
@@ -1506,7 +1506,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1506
1506
  // pre-translation stream + native passthrough callbacks) — do not re-wrap the
1507
1507
  // translated Anthropic stream here.
1508
1508
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1509
- await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy),
1509
+ await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
1510
1510
  req,
1511
1511
  policy,
1512
1512
  ), { requestId, start, logCtx });
@@ -61,6 +61,7 @@ import {
61
61
  registerDefaultAppOwnedObservedBuffers,
62
62
  } from "../lib/app-owned-memory-stores";
63
63
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
64
+ import { startPackageRefresh, stopPackageRefresh } from "../update/refresh-scheduler";
64
65
  import { activateLab, labActivationRequired } from "../lib/lab-activation";
65
66
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
66
67
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
@@ -746,6 +747,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
746
747
  const nativeStop = server.stop.bind(server);
747
748
  const loopbackListenerRef = loopbackServer;
748
749
  const managementIngressRef = managementIngressServer;
750
+ let packageRefreshStopped = false;
749
751
  Object.defineProperty(server, "stop", {
750
752
  configurable: true,
751
753
  value: async (closeActiveConnections?: boolean): Promise<void> => {
@@ -754,6 +756,10 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
754
756
  // Disarm the package-tree restart timer before listener teardown: a queued
755
757
  // replacement callback must not call acceptSystemRestart() after stop() has
756
758
  // begun, or it would schedule a drain-and-restart on a stopped server.
759
+ if (!packageRefreshStopped) {
760
+ packageRefreshStopped = true;
761
+ stopPackageRefresh();
762
+ }
757
763
  packageTreeIntegrity.dispose();
758
764
  // The orchestration lives in `runListenerShutdown` so its two competing properties —
759
765
  // cleanup completes, failure propagates — are testable without a live socket.
@@ -874,6 +880,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
874
880
  });
875
881
  }
876
882
 
883
+ startPackageRefresh();
877
884
  return server;
878
885
  }
879
886