@bitkyc08/opencodex 2.55.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 (256) 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-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,200 @@
1
+ import { debugProviderDiagnostic } from "../../lib/debug";
2
+ import { isDebugEnabled } from "../../lib/debug-settings";
3
+
4
+ export function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+
8
+ type InvalidToolCallReason =
9
+ | "tool_calls_not_array"
10
+ | "tool_call_not_object"
11
+ | "tool_call_id_invalid"
12
+ | "tool_call_function_not_object"
13
+ | "tool_call_function_name_invalid"
14
+ | "tool_call_function_name_blank"
15
+ | "tool_call_function_arguments_invalid";
16
+
17
+ export type InvalidToolCallDiagnostic = {
18
+ reason: InvalidToolCallReason;
19
+ callIndex?: number;
20
+ valueType: string;
21
+ };
22
+
23
+ type InvalidFieldShape =
24
+ | {
25
+ kind: "object";
26
+ knownKeys: string[];
27
+ knownFieldTypes: Record<string, string>;
28
+ hasUnknownKeys: boolean;
29
+ }
30
+ | {
31
+ kind: "array";
32
+ length: number;
33
+ };
34
+
35
+ const SAFE_TOOL_CALL_SHAPE_KEYS = [
36
+ "name",
37
+ "type",
38
+ "value",
39
+ "function",
40
+ "arguments",
41
+ "id",
42
+ "index",
43
+ ] as const;
44
+ const SAFE_TOOL_CALL_SHAPE_KEY_SET = new Set<string>(SAFE_TOOL_CALL_SHAPE_KEYS);
45
+
46
+ function structuralValueType(value: unknown): string {
47
+ return value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
48
+ }
49
+
50
+ function invalidToolCallField(rawToolCalls: unknown, diagnostic: InvalidToolCallDiagnostic): unknown {
51
+ if (diagnostic.reason === "tool_calls_not_array") return rawToolCalls;
52
+ if (!Array.isArray(rawToolCalls) || diagnostic.callIndex === undefined) return undefined;
53
+
54
+ const rawToolCall = rawToolCalls[diagnostic.callIndex];
55
+ if (diagnostic.reason === "tool_call_not_object") return rawToolCall;
56
+ if (!isRecord(rawToolCall)) return undefined;
57
+ if (diagnostic.reason === "tool_call_function_not_object") return rawToolCall.function;
58
+
59
+ const rawFunction = rawToolCall.function;
60
+ switch (diagnostic.reason) {
61
+ case "tool_call_id_invalid":
62
+ return rawToolCall.id;
63
+ case "tool_call_function_name_invalid":
64
+ return isRecord(rawFunction) ? rawFunction.name : undefined;
65
+ case "tool_call_function_arguments_invalid":
66
+ return isRecord(rawFunction) ? rawFunction.arguments : undefined;
67
+ default:
68
+ return undefined;
69
+ }
70
+ }
71
+
72
+ function fingerprintInvalidField(value: unknown): InvalidFieldShape | undefined {
73
+ if (Array.isArray(value)) return { kind: "array", length: value.length };
74
+ if (!isRecord(value)) return undefined;
75
+
76
+ const knownKeys: string[] = [];
77
+ const knownFieldTypes: Record<string, string> = {};
78
+ for (const key of SAFE_TOOL_CALL_SHAPE_KEYS) {
79
+ if (!Object.hasOwn(value, key)) continue;
80
+ knownKeys.push(key);
81
+ knownFieldTypes[key] = structuralValueType(value[key]);
82
+ }
83
+
84
+ let hasUnknownKeys = false;
85
+ for (const key of Object.keys(value)) {
86
+ if (!SAFE_TOOL_CALL_SHAPE_KEY_SET.has(key)) {
87
+ hasUnknownKeys = true;
88
+ break;
89
+ }
90
+ }
91
+ return { kind: "object", knownKeys, knownFieldTypes, hasUnknownKeys };
92
+ }
93
+
94
+ /**
95
+ * Streamed string fields are absent when null or undefined (#1731): OpenAI-compatible
96
+ * streamers repeat already-sent `id`/`name`/`arguments` as null on continuation deltas.
97
+ * The accumulator and this diagnostic share this predicate so they cannot disagree about
98
+ * which delta was the invalid one.
99
+ */
100
+ export function isInvalidStreamStringField(value: unknown): boolean {
101
+ return value != null && typeof value !== "string";
102
+ }
103
+
104
+ /**
105
+ * Explain only the rejected wire shape, never its values. This diagnostic exists so provider
106
+ * compatibility can be tightened from evidence without retaining tool arguments or credentials.
107
+ */
108
+ export function diagnoseInvalidToolCalls(
109
+ rawToolCalls: unknown,
110
+ mode: "stream" | "response",
111
+ ): InvalidToolCallDiagnostic | undefined {
112
+ if (!Array.isArray(rawToolCalls)) {
113
+ return { reason: "tool_calls_not_array", valueType: rawToolCalls === null ? "null" : typeof rawToolCalls };
114
+ }
115
+ for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
116
+ const rawToolCall = rawToolCalls[callIndex];
117
+ if (!isRecord(rawToolCall)) {
118
+ return {
119
+ reason: "tool_call_not_object",
120
+ callIndex,
121
+ valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
122
+ };
123
+ }
124
+ if (mode === "stream") {
125
+ // The streamed path validates the pieces it is about to store (#1531): a present
126
+ // `function` must be a record, and a present `name`/`arguments`/`id` must be a string.
127
+ // Blank names are caught later at flush, not here, so they are not diagnosed on this
128
+ // branch. Describe exactly that boundary rather than tightening compatibility in a
129
+ // diagnostic change.
130
+ // #1731: "present" means the same thing here as in the accumulator — null and undefined
131
+ // are both absent, because some OpenAI-compatible streamers repeat already-sent fields
132
+ // as null on continuation deltas. A separate predicate here would diagnose accepted
133
+ // padding as the failure and point compatibility work at the wrong delta.
134
+ const streamFunction = (rawToolCall as { function?: unknown }).function;
135
+ if (streamFunction !== undefined && streamFunction !== null) {
136
+ if (!isRecord(streamFunction)) {
137
+ return {
138
+ reason: "tool_call_function_not_object",
139
+ callIndex,
140
+ valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
141
+ };
142
+ }
143
+ if (isInvalidStreamStringField(streamFunction.name)) {
144
+ return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
145
+ }
146
+ if (isInvalidStreamStringField(streamFunction.arguments)) {
147
+ return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
148
+ }
149
+ }
150
+ if (isInvalidStreamStringField(rawToolCall.id)) {
151
+ return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
152
+ }
153
+ continue;
154
+ }
155
+ // Precedence must mirror the buffered validator below, or a payload with more than one
156
+ // problem is reported under the wrong reason and sends compatibility work after the wrong
157
+ // shape. That validator checks the `function` container first (`!isRecord(rawToolCall) ||
158
+ // !isRecord(rawToolCall.function)`), then id/name/arguments types together, and only then
159
+ // the blank name.
160
+ if (!isRecord(rawToolCall.function)) {
161
+ return {
162
+ reason: "tool_call_function_not_object",
163
+ callIndex,
164
+ valueType: rawToolCall.function === null ? "null" : Array.isArray(rawToolCall.function) ? "array" : typeof rawToolCall.function,
165
+ };
166
+ }
167
+ if (typeof rawToolCall.id !== "string") {
168
+ return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
169
+ }
170
+ if (typeof rawToolCall.function.name !== "string") {
171
+ return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawToolCall.function.name };
172
+ }
173
+ if (typeof rawToolCall.function.arguments !== "string") {
174
+ return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawToolCall.function.arguments };
175
+ }
176
+ // Last, matching the validator: #1531 also rejects a blank or whitespace-only name here,
177
+ // because such a call cannot select a dispatch target. Reporting it as `name_invalid`
178
+ // would claim a type problem for a correctly-typed value, so it gets its own code.
179
+ if (rawToolCall.function.name.trim().length === 0) {
180
+ return { reason: "tool_call_function_name_blank", callIndex, valueType: "string" };
181
+ }
182
+ }
183
+ return undefined;
184
+ }
185
+
186
+ export function logInvalidToolCalls(
187
+ mode: "stream" | "response",
188
+ rawToolCalls: unknown,
189
+ diagnosticOverride?: InvalidToolCallDiagnostic,
190
+ ): void {
191
+ if (!isDebugEnabled()) return;
192
+ const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
193
+ if (!diagnostic) return;
194
+ const fieldShape = fingerprintInvalidField(invalidToolCallField(rawToolCalls, diagnostic));
195
+ debugProviderDiagnostic("openai-chat", "invalid-tool-calls", {
196
+ mode,
197
+ ...diagnostic,
198
+ ...(fieldShape ? { fieldShape } : {}),
199
+ });
200
+ }
@@ -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
+ }