@bitkyc08/opencodex 2.56.0 → 2.58.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 (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,166 @@
1
+ import { createHash } from "node:crypto";
2
+ import { namespacedToolName, type OcxParsedRequest, type OcxTool } from "../../types";
3
+ import { frameAgentRouterMessages } from "../agentrouter";
4
+
5
+ const MAX_CHAT_TOOL_NAME_LENGTH = 64;
6
+ const ALIAS_HINT_CHARS = 16;
7
+ const RESERVED_ALIAS_PATTERN = /^ocx_[a-zA-Z0-9_-]{16}_[a-zA-Z0-9_-]{43}$/;
8
+ type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
9
+
10
+ export interface OpenAIChatToolNameRegistry {
11
+ alias(tool: ToolIdentity): string;
12
+ aliasWireName(wireName: string): string;
13
+ restore(wireName: string): string;
14
+ }
15
+
16
+ interface OpenAIChatToolNameScope {
17
+ messages(parsed: OcxParsedRequest, baseUrl: string, messages: readonly unknown[]): unknown;
18
+ registry(): OpenAIChatToolNameRegistry;
19
+ restore(wireName: string): string;
20
+ }
21
+
22
+ function identityKey(tool: ToolIdentity): string {
23
+ return JSON.stringify([tool.namespace ?? null, tool.name]);
24
+ }
25
+
26
+ function boundedAlias(tool: ToolIdentity, wireName: string): string {
27
+ const hint = wireName
28
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
29
+ .slice(-ALIAS_HINT_CHARS)
30
+ .padStart(ALIAS_HINT_CHARS, "_");
31
+ const key = identityKey(tool);
32
+ const digest = createHash("sha256")
33
+ .update(key)
34
+ .digest("base64url");
35
+ return `ocx_${hint}_${digest}`;
36
+ }
37
+
38
+ /** Catalog declarations plus structured calls retained in replay history. */
39
+ export function openAIChatToolNameIdentities(parsed: OcxParsedRequest): ToolIdentity[] {
40
+ const identities: ToolIdentity[] = [...(parsed.context.tools ?? [])];
41
+ for (const message of parsed.context.messages) {
42
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
43
+ for (const part of message.content) {
44
+ if (part.type !== "toolCall") continue;
45
+ identities.push({
46
+ name: part.name,
47
+ ...(part.namespace === undefined ? {} : { namespace: part.namespace }),
48
+ });
49
+ }
50
+ }
51
+ return identities;
52
+ }
53
+
54
+ /**
55
+ * One collision domain for a translated Chat Completions request.
56
+ *
57
+ * Namespaced names whose flattened spelling exceeds Chat Completions' 64-character function-name
58
+ * bound are rewritten. Ordinary names and bare names pass through byte-for-byte unless they occupy
59
+ * the reserved alias spelling; those are re-aliased so no declaration can shadow another identity's
60
+ * deterministic alias. Distinct identities sharing one flattened spelling each keep an identity
61
+ * alias, while replay rewriting leaves that ambiguous spelling untouched. Echoed aliases restore to
62
+ * the original flattened name consumed by the Responses bridge's existing namespace map.
63
+ */
64
+ export function createOpenAIChatToolNameRegistry(
65
+ tools: readonly ToolIdentity[] | undefined,
66
+ ): OpenAIChatToolNameRegistry {
67
+ const identities = new Map<string, ToolIdentity>();
68
+ for (const tool of tools ?? []) identities.set(identityKey(tool), tool);
69
+ const sortedIdentities = [...identities.entries()]
70
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
71
+
72
+ const aliasesByIdentity = new Map<string, string>();
73
+ const aliasesByWireName = new Map<string, string>();
74
+ const originalsByAlias = new Map<string, string>();
75
+ const wireOwners = new Map<string, string | null>();
76
+ for (const [key, tool] of sortedIdentities) {
77
+ const wireName = namespacedToolName(tool.namespace, tool.name);
78
+ const owner = wireOwners.get(wireName);
79
+ if (owner === undefined) wireOwners.set(wireName, key);
80
+ else if (owner !== key) wireOwners.set(wireName, null);
81
+ }
82
+
83
+ const aliasOwners = new Map<string, string>();
84
+ const wireClaims = new Map<string, { key: string; alias: string } | null>();
85
+ for (const [key, tool] of sortedIdentities) {
86
+ const wireName = namespacedToolName(tool.namespace, tool.name);
87
+ const candidate = (tool.namespace !== undefined && wireName.length > MAX_CHAT_TOOL_NAME_LENGTH)
88
+ || RESERVED_ALIAS_PATTERN.test(wireName)
89
+ || wireOwners.get(wireName) === null
90
+ ? boundedAlias(tool, wireName)
91
+ : wireName;
92
+ // A full SHA-256 collision is not safely attributable. Keep the later identity's native
93
+ // spelling instead of failing the request or stealing the first identity's restore entry.
94
+ const alias = aliasOwners.has(candidate) ? wireName : candidate;
95
+ aliasesByIdentity.set(key, alias);
96
+ if (!aliasOwners.has(alias)) aliasOwners.set(alias, key);
97
+ if (alias !== wireName) originalsByAlias.set(alias, wireName);
98
+
99
+ const claim = wireClaims.get(wireName);
100
+ if (claim === undefined) wireClaims.set(wireName, { key, alias });
101
+ else if (claim !== null && claim.key !== key) wireClaims.set(wireName, null);
102
+ }
103
+ for (const [wireName, claim] of wireClaims) {
104
+ if (claim !== null) aliasesByWireName.set(wireName, claim.alias);
105
+ }
106
+
107
+ return {
108
+ alias(tool: ToolIdentity): string {
109
+ const key = identityKey(tool);
110
+ const known = aliasesByIdentity.get(key);
111
+ if (known !== undefined) return known;
112
+ return namespacedToolName(tool.namespace, tool.name);
113
+ },
114
+ aliasWireName(wireName: string): string {
115
+ return aliasesByWireName.get(wireName) ?? wireName;
116
+ },
117
+ restore(wireName: string): string {
118
+ return originalsByAlias.get(wireName) ?? wireName;
119
+ },
120
+ };
121
+ }
122
+
123
+ export function restoreOpenAIChatToolName(
124
+ registry: OpenAIChatToolNameRegistry,
125
+ wireName: string,
126
+ ): string {
127
+ return registry.restore(wireName);
128
+ }
129
+
130
+ function isRecord(value: unknown): value is Record<string, unknown> {
131
+ return value !== null && typeof value === "object" && !Array.isArray(value);
132
+ }
133
+
134
+ /** Rewrite replayed assistant tool calls after the ordinary message converter has flattened them. */
135
+ export function aliasOpenAIChatMessageToolNames(
136
+ messages: readonly unknown[],
137
+ registry: OpenAIChatToolNameRegistry,
138
+ ): unknown[] {
139
+ return messages.map(message => {
140
+ if (!isRecord(message) || !Array.isArray(message.tool_calls)) return message;
141
+ let changed = false;
142
+ const toolCalls = message.tool_calls.map(toolCall => {
143
+ if (!isRecord(toolCall) || !isRecord(toolCall.function)
144
+ || typeof toolCall.function.name !== "string") return toolCall;
145
+ const name = registry.aliasWireName(toolCall.function.name);
146
+ if (name === toolCall.function.name) return toolCall;
147
+ changed = true;
148
+ return { ...toolCall, function: { ...toolCall.function, name } };
149
+ });
150
+ return changed ? { ...message, tool_calls: toolCalls } : message;
151
+ });
152
+ }
153
+
154
+ export function withOpenAIChatToolNames<T>(
155
+ build: (scope: OpenAIChatToolNameScope) => T,
156
+ ): T {
157
+ let registry = createOpenAIChatToolNameRegistry(undefined);
158
+ return build({
159
+ messages(parsed, baseUrl, messages): unknown {
160
+ registry = createOpenAIChatToolNameRegistry(openAIChatToolNameIdentities(parsed));
161
+ return frameAgentRouterMessages(baseUrl, aliasOpenAIChatMessageToolNames(messages, registry));
162
+ },
163
+ registry: () => registry,
164
+ restore: wireName => restoreOpenAIChatToolName(registry, wireName),
165
+ });
166
+ }
@@ -1,7 +1,8 @@
1
1
  import { isNativeOpenAIChatTarget } from "./wire";
2
+ import { createOpenAIChatToolNameRegistry, type OpenAIChatToolNameRegistry } from "./tool-name-registry";
2
3
  import { isXaiSchemaTarget, lookupLocalJsonPointer, normalizeXaiToolParameters } from "../xai-tool-schema";
3
4
  import { stripResponsesOnlyEncryptedMarker, stripUnicodePropertyPatterns } from "../responses-tool-schema";
4
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../../types";
5
+ import { isAllowedToolChoice, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../../types";
5
6
  import type { OcxParsedRequest, OcxProviderConfig } from "../../types";
6
7
 
7
8
  const ZEN_SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
@@ -409,7 +410,11 @@ function normalizeMoonshotToolParameters(parameters: unknown): Record<string, un
409
410
  return isXaiObjectSchema(normalized) ? normalized : rooted;
410
411
  }
411
412
 
412
- export function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
413
+ export function toolsToChatFormat(
414
+ parsed: OcxParsedRequest,
415
+ provider: OcxProviderConfig,
416
+ registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(parsed.context.tools),
417
+ ): unknown[] | undefined {
413
418
  if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
414
419
  const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
415
420
  if (tools.length === 0) return undefined;
@@ -427,7 +432,7 @@ export function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProvide
427
432
  return [{
428
433
  type: "function",
429
434
  function: {
430
- name: namespacedToolName(t.namespace, t.name),
435
+ name: registry.alias(t),
431
436
  ...(t.description ? { description: t.description } : {}),
432
437
  parameters,
433
438
  ...(t.strict !== undefined ? { strict: t.strict } : {}),
@@ -437,8 +442,12 @@ export function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProvide
437
442
  return formatted.length > 0 ? formatted : undefined;
438
443
  }
439
444
 
440
- export function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
441
- const base = toolsToChatFormat(parsed, provider);
445
+ export function toolsToChatFormatForProvider(
446
+ parsed: OcxParsedRequest,
447
+ provider: OcxProviderConfig,
448
+ registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(parsed.context.tools),
449
+ ): unknown[] | undefined {
450
+ const base = toolsToChatFormat(parsed, provider, registry);
442
451
  const azureChat = isAzureOpenAiChatTarget(provider);
443
452
  const zenChat = shouldSanitizeZenToolParameters(provider);
444
453
  if (!base || (!zenChat && !azureChat)) return base;
@@ -463,15 +472,24 @@ export function toolChoiceToChatFormat(
463
472
  tc: OcxParsedRequest["options"]["toolChoice"],
464
473
  tools: OcxParsedRequest["context"]["tools"],
465
474
  provider: OcxProviderConfig,
475
+ registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(tools),
466
476
  ): unknown {
467
477
  if (!tc) return undefined;
468
478
  if (isAllowedToolChoice(tc)) {
469
479
  if (tc.mode === "required" && tc.allowedTools.length === 1 && isNativeOpenAIChatTarget(provider)) {
470
- return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.allowedTools[0]) } };
480
+ return {
481
+ type: "function",
482
+ function: { name: registry.aliasWireName(resolveToolChoiceWireName(tools, tc.allowedTools[0])) },
483
+ };
471
484
  }
472
485
  return tc.mode === "required" ? "required" : "auto";
473
486
  }
474
487
  if (tc === "auto" || tc === "none" || tc === "required") return tc;
475
- if ("name" in tc) return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.name) } };
488
+ if ("name" in tc) {
489
+ return {
490
+ type: "function",
491
+ function: { name: registry.aliasWireName(resolveToolChoiceWireName(tools, tc.name)) },
492
+ };
493
+ }
476
494
  return undefined;
477
495
  }
@@ -6,7 +6,6 @@ import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
6
6
  import { debugProviderDiagnostic } from "../lib/debug";
7
7
  import { sseFieldValue } from "../lib/sse-decoder";
8
8
  import { isDebugEnabled } from "../lib/debug-settings";
9
- import { frameAgentRouterMessages } from "./agentrouter";
10
9
  import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
11
10
  import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../providers/vercel-gateway-routing";
12
11
  import { fastPolicyForModel } from "../providers/service-tier";
@@ -39,6 +38,7 @@ import {
39
38
  upstreamErrorEvent,
40
39
  } from "./openai-chat/errors";
41
40
  import { messagesToChatFormat } from "./openai-chat/messages";
41
+ import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
42
42
  import { isNativeOpenAIChatTarget, openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
43
43
  import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
44
44
 
@@ -88,7 +88,7 @@ function canSerializeOpenAIChatServiceTier(
88
88
 
89
89
  export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
90
90
  let lastRequestedModelId: string | undefined;
91
- return {
91
+ return withOpenAIChatToolNames(toolNames => ({
92
92
  name: "openai-chat",
93
93
 
94
94
  formatErrorBody: formatOpenAIChatErrorBody,
@@ -96,10 +96,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
96
96
  buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta) {
97
97
  lastRequestedModelId = parsed.modelId;
98
98
  const { url, headers, hasCredential } = openAIChatTransport(provider);
99
- const messages = frameAgentRouterMessages(provider.baseUrl, messagesToChatFormat(parsed, provider));
99
+ const messages = toolNames.messages(parsed, provider.baseUrl, messagesToChatFormat(parsed, provider));
100
100
  const finish = (): AdapterRequest => {
101
- const tools = toolsToChatFormatForProvider(parsed, provider);
102
- const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
101
+ const tools = toolsToChatFormatForProvider(parsed, provider, toolNames.registry());
102
+ const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider, toolNames.registry());
103
103
 
104
104
  const body: Record<string, unknown> = {
105
105
  model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
@@ -365,7 +365,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
365
365
  return "terminate";
366
366
  }
367
367
  if (!call.id) call.id = `call_${++toolCallSeq}`;
368
- yield { type: "tool_call_start", id: call.id, name: call.name };
368
+ yield { type: "tool_call_start", id: call.id, name: toolNames.restore(call.name) };
369
369
  if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
370
370
  yield { type: "tool_call_end" };
371
371
  }
@@ -801,7 +801,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
801
801
  logInvalidToolCalls("response", rawToolCalls);
802
802
  return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
803
803
  }
804
- events.push({ type: "tool_call_start", id, name });
804
+ events.push({ type: "tool_call_start", id, name: toolNames.restore(name) });
805
805
  events.push({ type: "tool_call_delta", arguments: args });
806
806
  events.push({ type: "tool_call_end" });
807
807
  }
@@ -818,5 +818,5 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
818
818
  budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
819
819
  }
820
820
  },
821
- };
821
+ }));
822
822
  }
@@ -32,7 +32,7 @@ import {
32
32
  createAdapterTierMetadata,
33
33
  } from "../../providers/fastwire";
34
34
  import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
35
- import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
35
+ import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
36
36
  import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
37
37
  import { isPlainObject } from "./internal";
38
38
  import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
@@ -41,6 +41,19 @@ import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuation
41
41
  import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
42
42
  import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
43
43
 
44
+ /**
45
+ * Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
46
+ * plaintext reasoning and cannot consume opaque reasoning state. The two existing flags are
47
+ * current evidence for that one provider contract, not equivalent capabilities: preservation
48
+ * keeps plaintext reasoning on the wire, while adjacency marks its strict tool-history shape.
49
+ * The moment a second provider needs this behavior, replace this derivation with an explicit
50
+ * registry capability rather than extending the inference.
51
+ */
52
+ export function requiresPlaintextReasoningReplay(provider: OcxProviderConfig): boolean {
53
+ return provider.preserveResponsesReasoningContent === true
54
+ && provider.requiresAdjacentResponsesToolResults === true;
55
+ }
56
+
44
57
  // Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
45
58
  // Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
46
59
  export const FORWARD_HEADERS = [
@@ -64,6 +77,16 @@ export const FORWARD_HEADERS = [
64
77
  CODEX_RESPONSES_LITE_HEADER,
65
78
  ];
66
79
 
80
+ /** Preserve the caller fingerprint unless the provider explicitly owns that header. */
81
+ function applyCallerUserAgentFallback(
82
+ headers: Record<string, string>,
83
+ incoming: IncomingMeta,
84
+ ): void {
85
+ if (Object.keys(headers).some(name => name.toLowerCase() === "user-agent")) return;
86
+ const userAgent = incoming.headers.get("user-agent");
87
+ if (userAgent) headers["User-Agent"] = userAgent;
88
+ }
89
+
67
90
  /** Replace every `input_image` part under a routed-compaction body with a short marker. */
68
91
  function stripInputImagesDeep(value: unknown): unknown {
69
92
  if (Array.isArray(value)) return value.map(stripInputImagesDeep);
@@ -221,6 +244,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
221
244
  if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
222
245
  if (provider.headers) Object.assign(headers, provider.headers);
223
246
  }
247
+ // Some Responses-compatible gateways select their Codex compatibility path from the real
248
+ // client fingerprint. This is a single non-credential fallback, not broader caller-header
249
+ // forwarding. Static provider headers remain authoritative in either auth mode.
250
+ applyCallerUserAgentFallback(headers, incoming);
224
251
 
225
252
  const forward = provider.authMode === "forward";
226
253
  let convertedRoutedCustomToolNames: Set<string> | undefined;
@@ -243,19 +270,31 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
243
270
  // tier write so a force-fast/default decision can never mutate parsed._rawBody.
244
271
  outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
245
272
  const stateless = provider.statelessResponses === true;
273
+ const adjacentToolResults = provider.requiresAdjacentResponsesToolResults === true;
274
+ // Adjacency reorders items the upstream would accept in some order. Pairing synthesizes an
275
+ // item the client never sent, which is a larger claim about the conversation, so it is its
276
+ // own capability: Kimi carries the adjacency flag but accepts a dangling call (#4726) and
277
+ // must not start receiving placeholders it never needed.
278
+ const pairedToolResults = provider.requiresPairedResponsesToolResults === true;
246
279
  if (stateless) outBody = stripStatefulResponsesParams(outBody);
247
280
  // A replay miss can leave a function_call_output whose paired function_call sat
248
281
  // in the prefix that was never expanded. A stateless upstream cannot resolve the
249
282
  // pair from its own storage either, so it needs the same repair the forward
250
283
  // backend gets — dropping previous_response_id is not much use if the body that
251
284
  // reaches the wire is unparseable.
285
+ // A parser can also 400 on a function_call with no matching output at all. DeepSeek gets
286
+ // that repair through statelessResponses. xAI cannot be marked stateless: its Responses API
287
+ // stores conversations for 30 days and documents previous_response_id. So it carries the
288
+ // pairing capability instead, which reuses the orphan-call placeholder without touching
289
+ // store or previous_response_id.
252
290
  if (provider.annotateEmptyToolOutputs === true) {
253
291
  outBody = annotateEmptyResponsesToolOutputs(outBody, true);
254
292
  }
255
- if (forward || stateless) {
256
- outBody = repairOrphanedInputItems(outBody, unexpandedMiss, stateless && !forward);
293
+ const synthesizeMissingCallOutputs = !forward && (stateless || pairedToolResults);
294
+ if (forward || stateless || pairedToolResults) {
295
+ outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
257
296
  }
258
- if (provider.requiresAdjacentResponsesToolResults === true) {
297
+ if (adjacentToolResults) {
259
298
  outBody = normalizeResponsesToolResultAdjacency(outBody);
260
299
  }
261
300
  if (forward) {
@@ -289,6 +328,20 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
289
328
  outBody = backfillWebSearchQueries(outBody);
290
329
  if (!isCanonicalOpenAiForwardProvider(provider)) {
291
330
  outBody = stripInternalChatMessageMetadataPassthrough(outBody);
331
+ // The same class of private field, one level up, but keyed on the DESTINATION rather than
332
+ // on the canonical surface alone. `src/server/responses/compact.ts` spreads the caller's
333
+ // raw body into the native `/responses/compact` request without passing through this
334
+ // adapter, and that endpoint is offered only to OpenAI-operated destinations
335
+ // (supportsNativeResponsesCompactEndpoint). Stripping on the canonical predicate here
336
+ // would make the two paths disagree for `openai-apikey`; stripping on the destination
337
+ // keeps every OpenAI-operated route byte-identical and removes the field exactly where it
338
+ // is known to break, which is a gateway this proxy does not operate.
339
+ //
340
+ // Placed before the routed compaction body is built and before serialization, so the HTTP,
341
+ // routed-compaction and WebSocket outbounds are all covered by this one call.
342
+ if (!isOpenAiOperatedResponsesDestination(provider)) {
343
+ outBody = stripCanonicalOnlyTopLevelFields(outBody);
344
+ }
292
345
  outBody = promoteClientLoadedTools(outBody);
293
346
  }
294
347
  if (!isCanonicalOpenAiForwardProvider(provider)) {
@@ -357,6 +410,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
357
410
  }
358
411
  }
359
412
  const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
413
+ // Providers with the strict plaintext tool-continuation contract cannot consume any
414
+ // encrypted reasoning blob, including one whose provenance is unknown. Combo routing
415
+ // separately refuses a proven cross-route replay when no plaintext exists; this final
416
+ // serializer guard ensures the foreign opaque state is never forwarded regardless.
360
417
  const sanitizedBody = normalizeToolSchemas(
361
418
  stripItemIdsWhenUnstored(
362
419
  stripInvalidItemIds(
@@ -370,7 +427,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
370
427
  {
371
428
  preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
372
429
  dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
373
- stripEncryptedContent: threadServingIdentityChanged,
430
+ stripEncryptedContent: threadServingIdentityChanged || requiresPlaintextReasoningReplay(provider),
374
431
  },
375
432
  ),
376
433
  provider,
@@ -120,6 +120,49 @@ export function stripInternalChatMessageMetadataPassthrough(body: unknown): unkn
120
120
  return changed ? { ...body, input } : body;
121
121
  }
122
122
 
123
+ /**
124
+ * OpenAI-private TOP-LEVEL request keys, the sibling of `CANONICAL_ONLY_TOOL_FIELDS` one level up.
125
+ *
126
+ * Codex attaches these on the request body itself rather than on a tool or an input item, and gates
127
+ * them on its own auth rather than on the destination URL. Loopback injection keeps Codex pointed at
128
+ * its built-in `openai` provider, so the client still believes it is addressing the canonical
129
+ * ChatGPT backend and keeps the key no matter where this proxy routes the turn. A Responses gateway
130
+ * that validates its top-level schema then rejects the whole request before inference.
131
+ *
132
+ * Keep this a table, and keep it to keys a client is OBSERVED to send. It is not an unknown-field
133
+ * sanitizer: a top-level key nobody has traced to a client is forwarded untouched, because deleting
134
+ * it would silently drop a parameter some other caller means.
135
+ */
136
+ const CANONICAL_ONLY_TOP_LEVEL_FIELDS: readonly string[] = [
137
+ // Cyber access program selector, new in Codex 0.155. codex-rs mints it from
138
+ // `cyber_access_program::for_auth`, which filters on ChatGPT auth alone and never on the
139
+ // destination base URL, and serializes it on the Responses request, the compaction input and the
140
+ // WebSocket `response.create` envelope. No public specification defines it, so a strict
141
+ // third-party gateway answers with an unknown-parameter error naming it, and every turn of that
142
+ // thread fails (#4853).
143
+ //
144
+ // `codex_output_schema` is deliberately NOT here. In codex-rs it is the `name` of the JSON-schema
145
+ // `text.format` object, not a top-level key, so listing it would delete a field this client never
146
+ // sends and discard it for any client that does send it meaningfully.
147
+ "access_programs",
148
+ ];
149
+
150
+ /**
151
+ * Remove the OpenAI-private top-level keys.
152
+ *
153
+ * The caller decides the boundary; see the call site in `passthrough.ts`, which applies this only
154
+ * to a destination OpenCodex does not operate. Returns the input unchanged when no listed key is
155
+ * present, so the common path allocates nothing and the caller-owned raw body is never mutated.
156
+ */
157
+ export function stripCanonicalOnlyTopLevelFields(body: unknown): unknown {
158
+ if (!isPlainObject(body)) return body;
159
+ if (!CANONICAL_ONLY_TOP_LEVEL_FIELDS.some(field => Object.hasOwn(body, field))) return body;
160
+
161
+ const next = { ...body };
162
+ for (const field of CANONICAL_ONLY_TOP_LEVEL_FIELDS) delete next[field];
163
+ return next;
164
+ }
165
+
123
166
  /**
124
167
  * When `store` is false, the upstream API does not persist response items. Any item ID
125
168
  * forwarded in `input` is then interpreted as a reference to a stored item that does not
@@ -0,0 +1,50 @@
1
+ import type { AdapterFetchContext } from "./base";
2
+ import type { SendClass } from "../lib/request-execution-budget";
3
+ import type { AttemptRecoveryKind } from "../usage/log";
4
+ import { abortError, SendBudgetExhaustedError } from "../lib/upstream-retry";
5
+
6
+ type PacedFetch = typeof globalThis.fetch & {
7
+ waitForPacing?: (signal?: AbortSignal) => Promise<void>;
8
+ unpacedFetch?: typeof globalThis.fetch;
9
+ };
10
+
11
+ /** One ordinal sequence per adapter fetchResponse call, across all of its inference retries.
12
+ * Consumption starts at underlying executor invocation; its own later preflight may still fail. */
13
+ export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallback = globalThis.fetch) {
14
+ const executor = (ctx.executor ?? fallback) as PacedFetch;
15
+ let ordinal = 0;
16
+ return async (options: {
17
+ url: string;
18
+ sendClass?: SendClass;
19
+ recovery?: AttemptRecoveryKind;
20
+ /** Runs only after admission, e.g. backoff and cancellation of a superseded response. */
21
+ beforeDispatch?: () => void | Promise<void>;
22
+ dispatch: (executor: typeof globalThis.fetch) => Promise<Response>;
23
+ }): Promise<Response> => {
24
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
25
+ const decision = ctx.sendBudget?.reserveDispatch({
26
+ sendClass: options.sendClass ?? "transient", targetKey: options.url,
27
+ });
28
+ if (decision && !decision.allowed) throw new SendBudgetExhaustedError(options.url);
29
+ const permit = decision?.allowed ? decision.permit : undefined;
30
+ let dispatched = false;
31
+ const physicalExecutor = (async (input, init) => {
32
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
33
+ if (init?.signal?.aborted) throw abortError(init.signal);
34
+ if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
35
+ dispatched = true;
36
+ ordinal += 1;
37
+ ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
38
+ return (executor.unpacedFetch ?? executor)(input, init);
39
+ }) as typeof globalThis.fetch;
40
+ try {
41
+ await executor.waitForPacing?.(ctx.abortSignal);
42
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
43
+ await options.beforeDispatch?.();
44
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
45
+ return await options.dispatch(physicalExecutor);
46
+ } finally {
47
+ permit?.release();
48
+ }
49
+ };
50
+ }
@@ -1,3 +1,10 @@
1
+ import {
2
+ isNonReplayableUpstreamCode,
3
+ isReplayRefusalCode,
4
+ markReplayRefusalResponse,
5
+ markResponseNonReplayable,
6
+ REPLAY_REFUSED_STATUS,
7
+ } from "../lib/upstream-retry";
1
8
  import {
2
9
  adapterFailureFromMessage,
3
10
  classifyError,
@@ -18,17 +25,34 @@ export function formatErrorResponse(
18
25
  error.code = CYBER_POLICY_ERROR_CODE;
19
26
  error.type = cyberPolicyErrorType(type);
20
27
  }
21
- const finalStatus = error.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
28
+ // Only the allowlisted transport verdicts survive this formatter. Do not forward
29
+ // arbitrary provider codes, and preserve the existing cyber-policy precedence.
30
+ const replayBlocked = error.code !== CYBER_POLICY_ERROR_CODE
31
+ && isNonReplayableUpstreamCode(options?.code);
32
+ if (replayBlocked) error.code = options!.code!;
33
+ // The replay refusal owns its status as well as its code. A combo or adapter formatter
34
+ // reaches here holding the upstream-shaped status it was about to report, and inheriting
35
+ // that would hand the client a 5xx it is configured to retry four times.
36
+ const finalStatus = error.code === CYBER_POLICY_ERROR_CODE
37
+ ? 400
38
+ : isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
22
39
  const headers = new Headers({ "Content-Type": "application/json" });
23
40
  const retryAfter = options?.retryAfter?.trim();
24
41
  if (error.code !== CYBER_POLICY_ERROR_CODE
42
+ && !replayBlocked
25
43
  && retryAfter
26
44
  && retryAfter.length > 0
27
45
  && retryAfter.length <= 128) {
28
46
  headers.set("Retry-After", retryAfter);
29
47
  }
30
- return new Response(JSON.stringify({ error }), {
48
+ const response = new Response(JSON.stringify({ error }), {
31
49
  status: finalStatus,
32
50
  headers,
33
51
  });
52
+ if (replayBlocked) markResponseNonReplayable(response);
53
+ // Re-wrapping is where the refusal loses its provenance: combo failure consumption parses
54
+ // the JSON and builds a new Response, and the code alone does not tell a later quota
55
+ // recorder that no upstream produced this status. Carry the narrower marker across too.
56
+ if (replayBlocked && isReplayRefusalCode(error.code)) markReplayRefusalResponse(response);
57
+ return response;
34
58
  }
@@ -68,6 +68,8 @@ function buildResponseJSONWithBudget(
68
68
  toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
69
69
  /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
70
70
  declaredToolNames?: ReadonlySet<string>;
71
+ /** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
72
+ enforceDeclaredToolNames?: boolean;
71
73
  /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
72
74
  toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
73
75
  freeformToolNames?: Set<string>;
@@ -432,7 +434,11 @@ function buildResponseJSONWithBudget(
432
434
  }
433
435
  flushToolCall();
434
436
  const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
435
- if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
437
+ if (
438
+ options?.declaredToolNames
439
+ && options.enforceDeclaredToolNames !== false
440
+ && !options.declaredToolNames.has(effectiveName)
441
+ ) {
436
442
  errorEvent = {
437
443
  type: "error",
438
444
  message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
@@ -521,7 +527,7 @@ function buildResponseJSONWithBudget(
521
527
  compactionEncryptedContent = e.compactionEncryptedContent;
522
528
  sawTerminal = true;
523
529
  endTurn = e.endTurn;
524
- cleanDone = e.stopReason === undefined;
530
+ cleanDone = !isTruncatedStopReason(e.stopReason);
525
531
  rawStopReason = e.stopReason;
526
532
  if (e.providerState) options?.onProviderState?.(e.providerState);
527
533
  // Match streaming: max_tokens and content_filter both terminate as incomplete.
package/src/bridge/sse.ts CHANGED
@@ -88,6 +88,20 @@ export function bridgeToResponsesSSE(
88
88
  onUsage?: (usage: OcxUsage | undefined) => void;
89
89
  /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
90
90
  declaredToolNames?: ReadonlySet<string>;
91
+ /**
92
+ * Whether `declaredToolNames` is an authorization boundary this proxy enforces, or only the
93
+ * catalog used to normalize provider-invented names back to declared ones.
94
+ *
95
+ * Defaults to enforcing. The chat and Anthropic inbound wires set it false: those specs make
96
+ * the server relay a tool call and leave execution or refusal to the client's own runner, and
97
+ * harnesses on them legitimately defer part of their catalog (#4735).
98
+ *
99
+ * It is a separate flag rather than simply withholding `declaredToolNames`, because the set
100
+ * also drives `normalizeDeclaredToolName` and `declaresCodeModeExec`. Passing `undefined`
101
+ * turns those off too, so a provider that invents `default.lookup` for a declared `lookup`
102
+ * would reach the client under the invented name instead of the normalized one.
103
+ */
104
+ enforceDeclaredToolNames?: boolean;
91
105
  /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
92
106
  toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
93
107
  /**
@@ -1008,7 +1022,11 @@ export function bridgeToResponsesSSE(
1008
1022
  : undefined;
1009
1023
  const mapped = toolNsMap?.get(effectiveName);
1010
1024
  const realName = mapped?.name ?? effectiveName;
1011
- if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
1025
+ if (
1026
+ options?.declaredToolNames
1027
+ && options.enforceDeclaredToolNames !== false
1028
+ && !options.declaredToolNames.has(effectiveName)
1029
+ ) {
1012
1030
  const failure = responseError(
1013
1031
  502,
1014
1032
  "upstream_error",
@@ -1153,7 +1171,7 @@ export function bridgeToResponsesSSE(
1153
1171
  break;
1154
1172
  }
1155
1173
  case "done": {
1156
- if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
1174
+ if (currentMsg) closeCurrentMessage(isTruncatedStopReason(event.stopReason) ? undefined : "final_answer");
1157
1175
  if (currentReasoning) closeCurrentReasoning();
1158
1176
  if (currentRawReasoning) closeCurrentRawReasoning();
1159
1177
  flushHiddenRawReasoning();