@bitkyc08/opencodex 2.56.0 → 2.57.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 (145) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-D4zuyIxQ.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +3 -3
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  8. package/src/adapters/command-code.ts +1 -1
  9. package/src/adapters/cursor/envelope-echo.ts +8 -2
  10. package/src/adapters/google.ts +7 -7
  11. package/src/adapters/kiro/payload.ts +17 -3
  12. package/src/adapters/kiro/reasoning.ts +70 -7
  13. package/src/adapters/kiro/stream.ts +8 -2
  14. package/src/adapters/kiro/wire.ts +2 -1
  15. package/src/adapters/kiro-events.ts +21 -13
  16. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  17. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  18. package/src/adapters/openai-chat.ts +8 -8
  19. package/src/adapters/openai-responses/passthrough.ts +32 -1
  20. package/src/bridge/errors.ts +26 -2
  21. package/src/bridge/response-json.ts +7 -1
  22. package/src/bridge/sse.ts +19 -1
  23. package/src/claude/desktop-profile.ts +66 -9
  24. package/src/claude/outbound.ts +18 -0
  25. package/src/cli/account-main.ts +1 -1
  26. package/src/cli/capabilities.ts +2 -2
  27. package/src/cli/combo.ts +10 -1
  28. package/src/cli/index.ts +48 -5
  29. package/src/cli/registry.ts +2 -1
  30. package/src/cli/system-command.ts +4 -4
  31. package/src/clients/config-export.ts +7 -3
  32. package/src/codex/account-label.ts +14 -3
  33. package/src/codex/account-store.ts +113 -26
  34. package/src/codex/account-usability.ts +21 -0
  35. package/src/codex/auth-api/login-flow.ts +14 -2
  36. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  37. package/src/codex/auth-context.ts +157 -7
  38. package/src/codex/catalog/aggregation.ts +80 -1
  39. package/src/codex/catalog/model-visibility.ts +1 -0
  40. package/src/codex/catalog/remote.ts +30 -0
  41. package/src/codex/catalog/retained-sync.ts +9 -1
  42. package/src/codex/catalog/routed-gather.ts +38 -1
  43. package/src/codex/cli-install-provenance.ts +7 -1
  44. package/src/codex/convergence.ts +7 -2
  45. package/src/codex/desktop-app/types.ts +11 -2
  46. package/src/codex/desktop-app/windows.ts +5 -5
  47. package/src/codex/inject/restore.ts +29 -2
  48. package/src/codex/inject.ts +9 -9
  49. package/src/codex/model-entitlements.ts +152 -15
  50. package/src/codex/pool-refresh-backoff.ts +12 -3
  51. package/src/codex/quota-rejection.ts +104 -15
  52. package/src/codex/routing/cache-affinity.ts +70 -0
  53. package/src/codex/routing/cooldown-math.ts +10 -0
  54. package/src/codex/routing/selection.ts +79 -2
  55. package/src/codex/routing/thread-affinity.ts +50 -2
  56. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  57. package/src/codex/routing.ts +29 -49
  58. package/src/codex/warmup.ts +1 -1
  59. package/src/combos/failover.ts +85 -0
  60. package/src/combos/request.ts +17 -10
  61. package/src/combos/types.ts +23 -2
  62. package/src/config/pending-teardown.ts +31 -0
  63. package/src/generated/compatibility-version.json +163 -135
  64. package/src/images/loop.ts +1 -1
  65. package/src/lib/errors.ts +17 -0
  66. package/src/lib/request-execution-budget.ts +147 -21
  67. package/src/lib/spend-reservation-ledger.ts +18 -0
  68. package/src/lib/state-store-registrations.ts +6 -2
  69. package/src/lib/test-home-guard.ts +85 -1
  70. package/src/lib/upstream-retry.ts +77 -10
  71. package/src/lib/windows-elevation.ts +76 -14
  72. package/src/oauth/index.ts +2 -2
  73. package/src/oauth/key-providers.ts +2 -2
  74. package/src/providers/kiro-models.ts +4 -3
  75. package/src/providers/label.ts +19 -1
  76. package/src/providers/model-discovery.ts +16 -0
  77. package/src/providers/registry/entries-core.ts +7 -0
  78. package/src/providers/registry/entries-extended.ts +9 -0
  79. package/src/providers/registry/model-seeds.ts +4 -0
  80. package/src/responses/reasoning-envelope.ts +6 -3
  81. package/src/routing/identity-domains.ts +21 -14
  82. package/src/routing/probe-lease.ts +103 -1
  83. package/src/server/chat-completions.ts +3 -1
  84. package/src/server/chat-native.ts +37 -9
  85. package/src/server/index/live-sideband.ts +37 -1
  86. package/src/server/index/websocket-handler.ts +6 -2
  87. package/src/server/index.ts +5 -5
  88. package/src/server/inspection-tee.ts +107 -0
  89. package/src/server/live.ts +46 -1
  90. package/src/server/management/combo-routes.ts +10 -1
  91. package/src/server/relay-eager.ts +2 -0
  92. package/src/server/relay.ts +14 -19
  93. package/src/server/request-log.ts +127 -3
  94. package/src/server/response-log-body.ts +153 -0
  95. package/src/server/responses/account-change-state.ts +74 -0
  96. package/src/server/responses/adapter-continuation.ts +33 -7
  97. package/src/server/responses/adapter-delivery.ts +5 -11
  98. package/src/server/responses/adapter-dispatch.ts +84 -13
  99. package/src/server/responses/codex-ws-wire.ts +5 -0
  100. package/src/server/responses/collaboration.ts +74 -4
  101. package/src/server/responses/combo-session-recall.ts +68 -8
  102. package/src/server/responses/compact.ts +54 -13
  103. package/src/server/responses/core-auth.ts +2 -0
  104. package/src/server/responses/core-codex-account.ts +51 -3
  105. package/src/server/responses/core-combo.ts +103 -23
  106. package/src/server/responses/core-errors.ts +18 -0
  107. package/src/server/responses/core-replay.ts +105 -32
  108. package/src/server/responses/core.ts +3 -3
  109. package/src/server/responses/encrypted-payload.ts +0 -1
  110. package/src/server/responses/input-admission.ts +126 -6
  111. package/src/server/responses/passthrough-delivery.ts +19 -6
  112. package/src/server/responses/passthrough-dispatch.ts +28 -10
  113. package/src/server/responses/passthrough-error.ts +38 -2
  114. package/src/server/responses/request-prepare.ts +132 -22
  115. package/src/server/responses/request-send-budget.ts +97 -2
  116. package/src/server/responses/request-spend.ts +147 -0
  117. package/src/server/responses/request-transport.ts +62 -3
  118. package/src/server/responses/run-turn-execution.ts +59 -31
  119. package/src/server/responses/sidecar-execution.ts +7 -13
  120. package/src/server/responses/terminal-guard.ts +65 -4
  121. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  122. package/src/service/windows-ops.ts +210 -16
  123. package/src/service/windows-scheduler.ts +28 -21
  124. package/src/service.ts +1 -1
  125. package/src/types/config.ts +4 -1
  126. package/src/types/request.ts +8 -5
  127. package/src/types/tools.ts +24 -0
  128. package/src/types.ts +2 -0
  129. package/src/update/index.ts +10 -0
  130. package/src/update/stop-contract.d.mts +1 -0
  131. package/src/update/stop-contract.mjs +19 -0
  132. package/src/update/stop-decision.d.mts +1 -1
  133. package/src/update/stop-decision.mjs +12 -3
  134. package/src/usage/log.ts +1 -1
  135. package/src/vision/anthropic-describe.ts +1 -1
  136. package/src/vision/describe.ts +5 -5
  137. package/src/web-search/anthropic-executor.ts +1 -1
  138. package/src/web-search/exa-executor.ts +1 -1
  139. package/src/web-search/executor.ts +1 -1
  140. package/src/web-search/gemini-executor.ts +1 -1
  141. package/src/web-search/loop.ts +1 -1
  142. package/src/web-search/ollama-executor.ts +1 -1
  143. package/src/web-search/parse.ts +67 -14
  144. package/src/web-search/passthrough-bridge.ts +64 -31
  145. 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
  }
@@ -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;
@@ -357,6 +384,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
357
384
  }
358
385
  }
359
386
  const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
387
+ // Providers with the strict plaintext tool-continuation contract cannot consume any
388
+ // encrypted reasoning blob, including one whose provenance is unknown. Combo routing
389
+ // separately refuses a proven cross-route replay when no plaintext exists; this final
390
+ // serializer guard ensures the foreign opaque state is never forwarded regardless.
360
391
  const sanitizedBody = normalizeToolSchemas(
361
392
  stripItemIdsWhenUnstored(
362
393
  stripInvalidItemIds(
@@ -370,7 +401,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
370
401
  {
371
402
  preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
372
403
  dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
373
- stripEncryptedContent: threadServingIdentityChanged,
404
+ stripEncryptedContent: threadServingIdentityChanged || requiresPlaintextReasoningReplay(provider),
374
405
  },
375
406
  ),
376
407
  provider,
@@ -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`,
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",
@@ -22,8 +22,34 @@ export interface RenderedDesktopModel extends DesktopProfileModel {
22
22
  supports1m: boolean;
23
23
  }
24
24
 
25
- const DATE_ALIAS = /^claude-opus-4-8-(2026\d{4})$/;
26
- const DAY_COUNT_2026 = 365;
25
+ // Managed-namespace date aliases run 2026-2035. The original 2026-only
26
+ // (365 slots) design failed with "all 365 encoded date slots are occupied"
27
+ // once a catalog exceeded 365 routes (stale assignments are retained by
28
+ // design, so the set only grows). Years before 2026 stay rejected: dated
29
+ // ids like `claude-opus-4-8-20250201` are real model snapshot ids, not
30
+ // managed aliases, and the inbound decoder relies on that distinction.
31
+ // Every emitted suffix stays 8 digits so modelMap date-stripping keeps
32
+ // working.
33
+ const DATE_ALIAS = /^claude-opus-4-8-(202[6-9]\d{4}|203[0-5]\d{4})$/;
34
+ const LEGACY_YEAR = 2026;
35
+ const LEGACY_DAY_COUNT = 365;
36
+ const ALIAS_FIRST_YEAR = 2026;
37
+ const ALIAS_LAST_YEAR = 2035;
38
+
39
+ function isLeapYear(year: number): boolean {
40
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
41
+ }
42
+
43
+ function daysInAliasYear(year: number): number {
44
+ return isLeapYear(year) ? 366 : 365;
45
+ }
46
+
47
+ export const TOTAL_ALIAS_SLOTS = (() => {
48
+ let total = 0;
49
+ for (let year = ALIAS_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) total += daysInAliasYear(year);
50
+ return total;
51
+ })();
52
+ export const ALIAS_YEAR_RANGE = { first: ALIAS_FIRST_YEAR, last: ALIAS_LAST_YEAR } as const;
27
53
 
28
54
  export class DesktopProfileError extends Error {
29
55
  constructor(message: string, readonly path = "profile") {
@@ -119,7 +145,7 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
119
145
  if (isRealAnthropicRoute(route)) {
120
146
  if (raw.alias !== routeModelId(route)) throw new DesktopProfileError("real Anthropic routes must keep their exact model id", `profile.assignments.${route}.alias`);
121
147
  } else if (!validDateAlias(raw.alias)) {
122
- throw new DesktopProfileError("must be a valid claude-opus-4-8-2026MMDD alias", `profile.assignments.${route}.alias`);
148
+ throw new DesktopProfileError("must be a valid claude-opus-4-8-YYYYMMDD alias", `profile.assignments.${route}.alias`);
123
149
  }
124
150
  if (aliases.has(raw.alias)) throw new DesktopProfileError(`duplicate alias "${raw.alias}"`, `profile.assignments.${route}.alias`);
125
151
  aliases.add(raw.alias);
@@ -144,26 +170,57 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
144
170
  return { version: 1, assignments, defaults, ...appliedMarkers(value) };
145
171
  }
146
172
 
147
- function dayOfYearAlias(dayIndex: number): string {
148
- const date = new Date(Date.UTC(2026, 0, dayIndex + 1));
173
+ function formatSlotDate(year: number, dayOfYear: number): string {
174
+ const date = new Date(Date.UTC(year, 0, dayOfYear));
149
175
  const y = date.getUTCFullYear();
150
176
  const m = String(date.getUTCMonth() + 1).padStart(2, "0");
151
177
  const d = String(date.getUTCDate()).padStart(2, "0");
152
178
  return `claude-opus-4-8-${y}${m}${d}`;
153
179
  }
154
180
 
181
+ // Legacy 2026 ring, byte-identical to the original allocator: the same route
182
+ // must keep resolving to the same 2026 alias it always had, and a probe over
183
+ // a nearly-full 2026 set must land on the same free date as before.
184
+ function legacyDayAlias(dayIndex: number): string {
185
+ return formatSlotDate(LEGACY_YEAR, dayIndex + 1);
186
+ }
187
+
188
+ // Overflow ring for catalogs past 365 routes (2027-2035). Probed only after
189
+ // every legacy slot is taken, so existing profiles never shift into it.
190
+ const OVERFLOW_FIRST_YEAR = LEGACY_YEAR + 1;
191
+ const OVERFLOW_SLOT_COUNT = TOTAL_ALIAS_SLOTS - LEGACY_DAY_COUNT;
192
+
193
+ function overflowSlotAlias(slotIndex: number): string {
194
+ let remaining = ((slotIndex % OVERFLOW_SLOT_COUNT) + OVERFLOW_SLOT_COUNT) % OVERFLOW_SLOT_COUNT;
195
+ for (let year = OVERFLOW_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) {
196
+ const days = daysInAliasYear(year);
197
+ if (remaining < days) return formatSlotDate(year, remaining + 1);
198
+ remaining -= days;
199
+ }
200
+ throw new DesktopProfileError("slot index out of range", "profile.assignments");
201
+ }
202
+
155
203
  function routeStartDay(route: string): number {
156
- return createHash("sha256").update(route).digest().readUInt32BE(0) % DAY_COUNT_2026;
204
+ return createHash("sha256").update(route).digest().readUInt32BE(0) % LEGACY_DAY_COUNT;
205
+ }
206
+
207
+ function routeOverflowStart(route: string): number {
208
+ return createHash("sha256").update(route).digest().readUInt32BE(4) % OVERFLOW_SLOT_COUNT;
157
209
  }
158
210
 
159
211
  function allocateAlias(route: string, used: Set<string>): string {
160
212
  if (isRealAnthropicRoute(route)) return routeModelId(route);
161
213
  const start = routeStartDay(route);
162
- for (let offset = 0; offset < DAY_COUNT_2026; offset += 1) {
163
- const alias = dayOfYearAlias((start + offset) % DAY_COUNT_2026);
214
+ for (let offset = 0; offset < LEGACY_DAY_COUNT; offset += 1) {
215
+ const alias = legacyDayAlias((start + offset) % LEGACY_DAY_COUNT);
216
+ if (!used.has(alias)) return alias;
217
+ }
218
+ const overflowStart = routeOverflowStart(route);
219
+ for (let offset = 0; offset < OVERFLOW_SLOT_COUNT; offset += 1) {
220
+ const alias = overflowSlotAlias((overflowStart + offset) % OVERFLOW_SLOT_COUNT);
164
221
  if (!used.has(alias)) return alias;
165
222
  }
166
- throw new DesktopProfileError("all 365 encoded date slots are occupied", `profile.assignments.${route}.alias`);
223
+ throw new DesktopProfileError(`all ${TOTAL_ALIAS_SLOTS} encoded date slots are occupied`, `profile.assignments.${route}.alias`);
167
224
  }
168
225
 
169
226
  export function reconcileDesktopProfile(
@@ -212,6 +212,8 @@ interface OpenBlock {
212
212
  argsBuf?: string;
213
213
  argsBufBytes?: number;
214
214
  webSearchArgsEmitted?: boolean;
215
+ /** True once ordinary function-call arguments were emitted to Anthropic SSE. */
216
+ toolArgsEmitted?: boolean;
215
217
  callId?: string;
216
218
  /** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
217
219
  reasoningPartKey?: string;
@@ -488,6 +490,7 @@ export function responsesSseToAnthropicSse(
488
490
  argsBuf: "",
489
491
  argsBufBytes: 0,
490
492
  webSearchArgsEmitted: false,
493
+ toolArgsEmitted: false,
491
494
  };
492
495
  break;
493
496
  }
@@ -518,6 +521,14 @@ export function responsesSseToAnthropicSse(
518
521
  type: "content_block_delta", index: open.index,
519
522
  delta: { type: "input_json_delta", partial_json: data.delta },
520
523
  });
524
+ open.toolArgsEmitted = true;
525
+ break;
526
+ }
527
+ case "response.function_call_arguments.done": {
528
+ if (!open || open.kind !== "tool_use" || open.bufferWebSearchArgs || open.toolArgsEmitted) break;
529
+ if (typeof data.arguments !== "string" || data.arguments.length === 0) break;
530
+ emit("content_block_delta", { type: "content_block_delta", index: open.index, delta: { type: "input_json_delta", partial_json: data.arguments } });
531
+ open.toolArgsEmitted = true;
521
532
  break;
522
533
  }
523
534
  case "response.output_item.done": {
@@ -565,6 +576,13 @@ export function responsesSseToAnthropicSse(
565
576
  delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
566
577
  });
567
578
  open.webSearchArgsEmitted = true;
579
+ } else if (!open.bufferWebSearchArgs && !open.toolArgsEmitted
580
+ && typeof item.arguments === "string" && item.arguments.length > 0) {
581
+ emit("content_block_delta", {
582
+ type: "content_block_delta", index: open.index,
583
+ delta: { type: "input_json_delta", partial_json: item.arguments },
584
+ });
585
+ open.toolArgsEmitted = true;
568
586
  }
569
587
  closeOpenBlock();
570
588
  }
@@ -245,7 +245,7 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
245
245
  }
246
246
  if (noWait) {
247
247
  printStatus({ flowId: startFlowId, ...pending });
248
- console.log("follow up: ocx account main reauth status --flow " + startFlowId);
248
+ if (!wantsJson) console.log("follow up: ocx account main reauth status --flow " + startFlowId);
249
249
  return 0;
250
250
  }
251
251
  // Blocking wait bounded by the service flow expiry (15-minute grant + margin).
@@ -710,10 +710,10 @@ export const CAPABILITIES: readonly Capability[] = [
710
710
  },
711
711
  {
712
712
  command: ["system", "codex-restart"],
713
- summary: "Restart the Codex app-server.",
713
+ summary: "Restart the Codex desktop app and app-servers.",
714
714
  routes: [{ method: "POST", path: "/api/system/codex-restart" }],
715
715
  flags: [
716
- { name: "--yes", value: "boolean", summary: "Required: restarts the operator's running Codex app-server." },
716
+ { name: "--yes", value: "boolean", summary: "Required: fully quits and relaunches the operator's Codex desktop app and restarts its app-servers." },
717
717
  { name: "--json", value: "boolean", summary: "Emit the restart result as JSON." },
718
718
  ],
719
719
  mutates: true,