deepcodex 0.1.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.
- package/.codex-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/bin/opencodex.js +49 -0
- package/config/desktop.json +9 -0
- package/config/pilot.json +16 -0
- package/config/worker.json +78 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +47 -0
- package/prompts/worker.md +20 -0
- package/scripts/credentials.js +102 -0
- package/scripts/desktop.js +199 -0
- package/scripts/pilot-router.js +241 -0
- package/scripts/pilot.js +242 -0
- package/scripts/toml.js +6 -0
- package/scripts/worker.js +637 -0
- package/skills/delegate-flash/SKILL.md +63 -0
- package/vendor/codex-router/LICENSE +21 -0
- package/vendor/codex-router/deepseek-responses.js +55 -0
- package/vendor/codex-router/json-number-rewrite.js +58 -0
- package/vendor/codex-router/namespace-relay.js +4294 -0
- package/vendor/codex-router/sse-prefix.js +115 -0
- package/vendor/codex-router/subagent-completion.js +261 -0
- package/vendor/codex-router/tool-arguments.js +111 -0
- package/vendor/codex-router/tool-schema-root.js +1008 -0
|
@@ -0,0 +1,4294 @@
|
|
|
1
|
+
// Vendored from duolahypercho/codex-router at 63ec1f3602c28f2a28ccb7e9edaf7b4f7d191c6c.
|
|
2
|
+
// Source: src/namespace-relay.js; MIT license in LICENSE.
|
|
3
|
+
import { isUtf8 } from "node:buffer";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { Transform } from "node:stream";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { jsonNumberIsStableForRewrite } from "./json-number-rewrite.js";
|
|
9
|
+
import { HeaderlessSseDetector } from "./sse-prefix.js";
|
|
10
|
+
import { coerceFunctionCallArguments } from "./tool-arguments.js";
|
|
11
|
+
import {
|
|
12
|
+
inlineForeignRefs,
|
|
13
|
+
declareSchemaTypes,
|
|
14
|
+
nonRecursiveToolSchema,
|
|
15
|
+
providerToolSchema,
|
|
16
|
+
} from "./tool-schema-root.js";
|
|
17
|
+
import {
|
|
18
|
+
buildInterruptAgentCall,
|
|
19
|
+
filterAlreadyInterrupted,
|
|
20
|
+
interruptTargetFromCall,
|
|
21
|
+
} from "./subagent-completion.js";
|
|
22
|
+
|
|
23
|
+
// The Codex client ships most of its toolset as `type: "namespace"` entries:
|
|
24
|
+
// the collaboration runtime, the app toolset (threads, automations,
|
|
25
|
+
// navigation), and every MCP server (node_repl, peekaboo, github, ...).
|
|
26
|
+
//
|
|
27
|
+
// LiteLLM's Responses -> Chat Completions bridge drops namespace tools, which
|
|
28
|
+
// is how the app sends all of those to the model. A routed chat-completions
|
|
29
|
+
// provider would therefore see none of them: no collaboration tools, no
|
|
30
|
+
// threads, and no `mcp__node_repl__js` -- the runtime the in-app browser and
|
|
31
|
+
// computer-use skills drive. This module is the one relay for all of it:
|
|
32
|
+
//
|
|
33
|
+
// 1. flattenNamespaceTools -- namespace entries -> plain
|
|
34
|
+
// `<namespace>__<tool>` functions the
|
|
35
|
+
// provider accepts
|
|
36
|
+
// 2. flattenNamespacedHistory -- stored calls renamed to the flattened
|
|
37
|
+
// form so the model's transcript matches
|
|
38
|
+
// its tool list
|
|
39
|
+
// 3. NamespaceToolCallTransform -- function calls coming back restored to
|
|
40
|
+
// the app's native `{name, namespace}`
|
|
41
|
+
// shape so the client dispatches them
|
|
42
|
+
//
|
|
43
|
+
// The router only relays definitions and results; it never executes an app
|
|
44
|
+
// tool itself. Namespace names themselves may contain the delimiter
|
|
45
|
+
// (`mcp__codex_apps__github`), so restoration always resolves through the map
|
|
46
|
+
// built from the exact tools that were flattened -- never by splitting names.
|
|
47
|
+
// The same map may also index a dotted inventory alias (`namespace.tool`) when
|
|
48
|
+
// a Responses-native model echoes that wire form (#611); that is still an
|
|
49
|
+
// exact inventory hit, not a split.
|
|
50
|
+
|
|
51
|
+
export const NAMESPACE_DELIMITER = "__";
|
|
52
|
+
const DEFAULT_FUNCTION_NAMESPACE = "functions";
|
|
53
|
+
const MCP_NAMESPACE_PREFIX = "mcp__";
|
|
54
|
+
|
|
55
|
+
// Metadata derived from the request's exact tool schema. Keeping it beside the
|
|
56
|
+
// Map in a WeakMap preserves the Map's public shape for existing callers while
|
|
57
|
+
// letting the response path validate model-generated overrides.
|
|
58
|
+
const SPAWN_AGENT_MODELS = new WeakMap();
|
|
59
|
+
const TOOL_SEARCH_RELAYS = new WeakMap();
|
|
60
|
+
const CUSTOM_TOOL_RELAYS = new WeakMap();
|
|
61
|
+
const CUSTOM_TOOL_CODECS = new WeakMap();
|
|
62
|
+
const FUNCTION_RELAYS = new WeakMap();
|
|
63
|
+
// Flattened custom definitions retain the exact native identity beside the
|
|
64
|
+
// object. A literal plain name containing `__` must never acquire a namespace.
|
|
65
|
+
const CUSTOM_TOOL_IDENTITIES = new WeakMap();
|
|
66
|
+
const NAME_ALIASES = new WeakMap();
|
|
67
|
+
const PLAIN_TOOL_NAMES = new WeakMap();
|
|
68
|
+
// A provider-facing function reference can retain the same spelling as a
|
|
69
|
+
// bridged custom/tool-search relay while a later-discovered ordinary function
|
|
70
|
+
// with that native name receives an alias. Object identity is the only honest
|
|
71
|
+
// discriminator after both shapes have become `type: "function"`; keep it
|
|
72
|
+
// request-local and garbage-collectable rather than guessing from the name.
|
|
73
|
+
const SPECIAL_FUNCTION_REFERENCES = new WeakSet();
|
|
74
|
+
|
|
75
|
+
const TOOL_SEARCH_FUNCTION_NAME = "tool_search";
|
|
76
|
+
const CUSTOM_TOOL_INPUT_PROPERTY = "input";
|
|
77
|
+
|
|
78
|
+
export function toolSearchRelayAvailable(namespaces) {
|
|
79
|
+
return TOOL_SEARCH_RELAYS.has(namespaces);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function providerFunctionName(tool) {
|
|
83
|
+
return tool?.name ?? tool?.function?.name;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function withProviderFunctionName(tool, name) {
|
|
87
|
+
if (tool?.function?.name !== undefined) {
|
|
88
|
+
return { ...tool, function: { ...tool.function, name } };
|
|
89
|
+
}
|
|
90
|
+
return { ...tool, name };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function nativeToolKey(namespace, name) {
|
|
94
|
+
return JSON.stringify([namespace ?? null, name]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function boundedNameCandidate(wireName, identity, maxNameLength, attempt) {
|
|
98
|
+
const digest = createHash("sha256")
|
|
99
|
+
.update(`${identity}\0${attempt}`)
|
|
100
|
+
.digest("hex")
|
|
101
|
+
.slice(0, 12);
|
|
102
|
+
const suffix = `_${digest}`;
|
|
103
|
+
if (!Number.isFinite(maxNameLength)) return `${wireName}${suffix}`;
|
|
104
|
+
return `${wireName.slice(0, maxNameLength - suffix.length)}${suffix}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assignProviderName(relay, identity, wireName, native, { forceAlias = false } = {}) {
|
|
108
|
+
const existing = relay.nativeToProvider.get(identity);
|
|
109
|
+
if (existing) return existing;
|
|
110
|
+
|
|
111
|
+
let providerName = wireName;
|
|
112
|
+
if (
|
|
113
|
+
forceAlias ||
|
|
114
|
+
wireName.length > relay.maxNameLength ||
|
|
115
|
+
relay.providerOwners.has(wireName)
|
|
116
|
+
) {
|
|
117
|
+
let attempt = 0;
|
|
118
|
+
do {
|
|
119
|
+
providerName = boundedNameCandidate(
|
|
120
|
+
wireName,
|
|
121
|
+
identity,
|
|
122
|
+
relay.maxNameLength,
|
|
123
|
+
attempt,
|
|
124
|
+
);
|
|
125
|
+
attempt += 1;
|
|
126
|
+
} while (relay.providerOwners.has(providerName));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
relay.nativeToProvider.set(identity, providerName);
|
|
130
|
+
relay.providerOwners.set(providerName, identity);
|
|
131
|
+
if (native && providerName !== wireName) relay.providerToNative.set(providerName, native);
|
|
132
|
+
if (native) {
|
|
133
|
+
if (native.namespace === undefined) relay.plainProviderNames.add(providerName);
|
|
134
|
+
if (!relay.wireOwners.has(wireName)) relay.wireOwners.set(wireName, new Set());
|
|
135
|
+
relay.wireOwners.get(wireName).add(identity);
|
|
136
|
+
}
|
|
137
|
+
return providerName;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function initialFunctionIdentities(tools) {
|
|
141
|
+
const identities = new Map();
|
|
142
|
+
if (!Array.isArray(tools)) return identities;
|
|
143
|
+
for (const tool of tools) {
|
|
144
|
+
if (tool?.type === "namespace" && typeof tool.name === "string" && Array.isArray(tool.tools)) {
|
|
145
|
+
for (const child of tool.tools) {
|
|
146
|
+
if (child?.type !== "function" || typeof child.name !== "string" || !child.name) continue;
|
|
147
|
+
const wireName = `${tool.name}${NAMESPACE_DELIMITER}${child.name}`;
|
|
148
|
+
const native = { namespace: tool.name, name: child.name };
|
|
149
|
+
identities.set(nativeToolKey(native.namespace, native.name), { wireName, native });
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (tool?.type !== "function") continue;
|
|
154
|
+
const name = providerFunctionName(tool);
|
|
155
|
+
if (typeof name !== "string" || !name) continue;
|
|
156
|
+
const native = { name };
|
|
157
|
+
identities.set(nativeToolKey(undefined, name), { wireName: name, native });
|
|
158
|
+
}
|
|
159
|
+
return identities;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function initializeNameAliases(namespaces, tools, maxNameLength, aliasCollisions = false) {
|
|
163
|
+
const bounded = Number.isInteger(maxNameLength) && maxNameLength >= 16;
|
|
164
|
+
if (!bounded && !aliasCollisions) return undefined;
|
|
165
|
+
const relay = {
|
|
166
|
+
maxNameLength: bounded ? maxNameLength : Infinity,
|
|
167
|
+
nativeToProvider: new Map(),
|
|
168
|
+
providerToNative: new Map(),
|
|
169
|
+
providerOwners: new Map(),
|
|
170
|
+
plainProviderNames: new Set(),
|
|
171
|
+
wireOwners: new Map(),
|
|
172
|
+
};
|
|
173
|
+
NAME_ALIASES.set(namespaces, relay);
|
|
174
|
+
|
|
175
|
+
const identities = initialFunctionIdentities(tools);
|
|
176
|
+
const wireCounts = new Map();
|
|
177
|
+
for (const { wireName } of identities.values()) {
|
|
178
|
+
wireCounts.set(wireName, (wireCounts.get(wireName) || 0) + 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Reserve every legal, unique name first. Long names and native collisions
|
|
182
|
+
// are then assigned in stable identity order, so reordering an otherwise
|
|
183
|
+
// identical tool list cannot change the aliases sent to the provider.
|
|
184
|
+
const pending = [];
|
|
185
|
+
for (const [identity, entry] of [...identities].sort(([left], [right]) =>
|
|
186
|
+
left.localeCompare(right),
|
|
187
|
+
)) {
|
|
188
|
+
if (entry.wireName.length <= relay.maxNameLength && wireCounts.get(entry.wireName) === 1) {
|
|
189
|
+
assignProviderName(relay, identity, entry.wireName, entry.native);
|
|
190
|
+
} else {
|
|
191
|
+
pending.push([identity, entry]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const [identity, entry] of pending) {
|
|
195
|
+
assignProviderName(relay, identity, entry.wireName, entry.native, { forceAlias: true });
|
|
196
|
+
}
|
|
197
|
+
return relay;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function providerNameForNative(namespaces, namespace, name) {
|
|
201
|
+
const relay = NAME_ALIASES.get(namespaces);
|
|
202
|
+
if (!relay) return namespace === undefined ? name : `${namespace}${NAMESPACE_DELIMITER}${name}`;
|
|
203
|
+
const identity = nativeToolKey(namespace, name);
|
|
204
|
+
const existing = relay.nativeToProvider.get(identity);
|
|
205
|
+
if (existing) return existing;
|
|
206
|
+
const wireName = namespace === undefined ? name : `${namespace}${NAMESPACE_DELIMITER}${name}`;
|
|
207
|
+
return assignProviderName(relay, identity, wireName, {
|
|
208
|
+
...(namespace === undefined ? {} : { namespace }),
|
|
209
|
+
name,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reserveSpecialProviderName(namespaces, identity, wireName) {
|
|
214
|
+
const relay = NAME_ALIASES.get(namespaces);
|
|
215
|
+
if (!relay) return wireName;
|
|
216
|
+
return assignProviderName(relay, identity, wireName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function providerNameForWire(namespaces, wireName) {
|
|
220
|
+
const relay = NAME_ALIASES.get(namespaces);
|
|
221
|
+
if (!relay) return undefined;
|
|
222
|
+
const owners = relay.wireOwners.get(wireName);
|
|
223
|
+
if (!owners || owners.size !== 1) return undefined;
|
|
224
|
+
const [identity] = owners;
|
|
225
|
+
return relay.nativeToProvider.get(identity);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function providerVisibleToolNames(tools) {
|
|
229
|
+
const names = new Set();
|
|
230
|
+
if (!Array.isArray(tools)) return names;
|
|
231
|
+
for (const tool of tools) {
|
|
232
|
+
if (tool?.type === "namespace" && Array.isArray(tool.tools)) {
|
|
233
|
+
if (typeof tool.name === "string" && tool.name) names.add(tool.name);
|
|
234
|
+
for (const fn of tool.tools) {
|
|
235
|
+
if (fn?.name) names.add(`${tool.name}${NAMESPACE_DELIMITER}${fn.name}`);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const name = providerFunctionName(tool);
|
|
240
|
+
if (typeof name === "string" && name) names.add(name);
|
|
241
|
+
}
|
|
242
|
+
return names;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function availableCustomToolName(nativeName, visibleNames) {
|
|
246
|
+
if (!visibleNames.has(nativeName)) return nativeName;
|
|
247
|
+
const stem = `codex_custom_${nativeName}`;
|
|
248
|
+
if (!visibleNames.has(stem)) return stem;
|
|
249
|
+
let suffix = 1;
|
|
250
|
+
while (visibleNames.has(`${stem}_${suffix}`)) suffix += 1;
|
|
251
|
+
return `${stem}_${suffix}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// A custom tool's `format` is the model's only specification of the freeform
|
|
255
|
+
// payload it must emit: Codex ships apply_patch's V4A dialect as a lark
|
|
256
|
+
// grammar and describes the format nowhere else. A function tool has no
|
|
257
|
+
// grammar slot, so dropping `format.definition` on the way through would hand
|
|
258
|
+
// the model a bare "raw input" string and leave any model that has not
|
|
259
|
+
// memorised V4A emitting patches Codex cannot parse. Carry the definition in
|
|
260
|
+
// the bridged description instead -- that is the one field every
|
|
261
|
+
// function-tool provider does put in front of the model.
|
|
262
|
+
export function bridgedCustomToolDescription(tool) {
|
|
263
|
+
const sections = [];
|
|
264
|
+
if (typeof tool?.description === "string" && tool.description.trim()) {
|
|
265
|
+
sections.push(tool.description.trim());
|
|
266
|
+
}
|
|
267
|
+
const format = tool?.format;
|
|
268
|
+
const definition = typeof format?.definition === "string" ? format.definition.trim() : "";
|
|
269
|
+
if (definition) {
|
|
270
|
+
const syntax = typeof format?.syntax === "string" && format.syntax.trim()
|
|
271
|
+
? format.syntax.trim()
|
|
272
|
+
: "grammar";
|
|
273
|
+
sections.push(
|
|
274
|
+
`The \`${CUSTOM_TOOL_INPUT_PROPERTY}\` string is freeform text, not JSON, and must parse ` +
|
|
275
|
+
`against this ${syntax} grammar:\n\n${definition}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return sections.length ? sections.join("\n\n") : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// OpenCode accepts ordinary JSON-schema function tools but rejects OpenAI's
|
|
282
|
+
// freeform `type: "custom"` definition. Codex exposes apply_patch only in that
|
|
283
|
+
// native form. Present the same raw-patch contract as one required string
|
|
284
|
+
// property, translate matching history and a forced native custom choice, and
|
|
285
|
+
// retain a request-local reverse map so the response path can restore the exact
|
|
286
|
+
// custom-tool shape Codex executes. An unrelated function or native namespace
|
|
287
|
+
// with the same name receives a collision-safe alias and is otherwise untouched.
|
|
288
|
+
export function bridgeCustomTools(
|
|
289
|
+
tools,
|
|
290
|
+
input,
|
|
291
|
+
namespaces,
|
|
292
|
+
toolChoice,
|
|
293
|
+
names = ["apply_patch"],
|
|
294
|
+
{ maxNameLength, bridgeAll = false, codecs } = {},
|
|
295
|
+
) {
|
|
296
|
+
if (!(namespaces instanceof Map)) {
|
|
297
|
+
return { tools, input, toolChoice, bridged: false };
|
|
298
|
+
}
|
|
299
|
+
if (Number.isInteger(maxNameLength) && !NAME_ALIASES.has(namespaces)) {
|
|
300
|
+
initializeNameAliases(namespaces, tools, maxNameLength);
|
|
301
|
+
}
|
|
302
|
+
const requested = new Set(names);
|
|
303
|
+
const shouldBridge = (name) => bridgeAll || requested.has(name);
|
|
304
|
+
// Stored custom calls may use the client's already-flat spelling. Resolve
|
|
305
|
+
// only exact live definitions, before registering history or forced choices,
|
|
306
|
+
// so they share the declaration's identity and bounded provider alias.
|
|
307
|
+
const customWireIdentities = new Map();
|
|
308
|
+
const otherWireNames = new Set();
|
|
309
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
310
|
+
const native = CUSTOM_TOOL_IDENTITIES.get(tool);
|
|
311
|
+
if (native) {
|
|
312
|
+
const wireName = `${native.namespace}${NAMESPACE_DELIMITER}${native.name}`;
|
|
313
|
+
const previous = customWireIdentities.get(wireName);
|
|
314
|
+
customWireIdentities.set(wireName,
|
|
315
|
+
!customWireIdentities.has(wireName) ||
|
|
316
|
+
(previous?.namespace === native.namespace && previous?.name === native.name)
|
|
317
|
+
? native : undefined);
|
|
318
|
+
} else {
|
|
319
|
+
const name = providerFunctionName(tool);
|
|
320
|
+
const original = NAME_ALIASES.get(namespaces)?.providerToNative.get(name);
|
|
321
|
+
otherWireNames.add(original?.namespace === undefined ? original?.name ?? name
|
|
322
|
+
: `${original.namespace}${NAMESPACE_DELIMITER}${original.name}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const identityOf = (reference) => CUSTOM_TOOL_IDENTITIES.get(reference) ||
|
|
326
|
+
(reference.namespace === undefined && !otherWireNames.has(reference.name)
|
|
327
|
+
? customWireIdentities.get(reference.name) : undefined) || {
|
|
328
|
+
name: reference.name,
|
|
329
|
+
...(typeof reference.namespace === "string" && reference.namespace
|
|
330
|
+
? { namespace: reference.namespace } : {}),
|
|
331
|
+
};
|
|
332
|
+
const keyOf = (reference) => {
|
|
333
|
+
const native = identityOf(reference);
|
|
334
|
+
return nativeToolKey(native.namespace, native.name);
|
|
335
|
+
};
|
|
336
|
+
const nativeTools = new Map();
|
|
337
|
+
const remember = (reference) => {
|
|
338
|
+
const native = identityOf(reference);
|
|
339
|
+
if (typeof native.name !== "string" || !native.name) return;
|
|
340
|
+
const wireName = CUSTOM_TOOL_IDENTITIES.has(reference) ? reference.name
|
|
341
|
+
: native.namespace === undefined ? native.name
|
|
342
|
+
: providerNameForNative(namespaces, native.namespace, native.name);
|
|
343
|
+
if (shouldBridge(reference.name) || shouldBridge(wireName)) {
|
|
344
|
+
nativeTools.set(keyOf(reference), { native, wireName });
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
if (Array.isArray(tools)) {
|
|
348
|
+
for (const tool of tools) if (tool?.type === "custom") remember(tool);
|
|
349
|
+
}
|
|
350
|
+
if (Array.isArray(input)) {
|
|
351
|
+
for (const item of input) if (item?.type === "custom_tool_call") remember(item);
|
|
352
|
+
}
|
|
353
|
+
if (toolChoice?.type === "custom") remember(toolChoice);
|
|
354
|
+
if (toolChoice?.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
355
|
+
for (const choice of toolChoice.tools) {
|
|
356
|
+
if (choice?.type === "custom") remember(choice);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!nativeTools.size) return { tools, input, toolChoice, bridged: false };
|
|
360
|
+
|
|
361
|
+
// Console Go validates optional item ids on function-shaped history against
|
|
362
|
+
// the `fc` prefix. The rewrite used to keep `ctc_` / `ctco_` ids on the new
|
|
363
|
+
// type, which 400s every follow-up after apply_patch (#780). call_id still
|
|
364
|
+
// pairs the call with its result. A native-minted `fc…` id is kept.
|
|
365
|
+
const withoutIncompatibleFunctionItemId = (item) => {
|
|
366
|
+
if (typeof item?.id !== "string" || item.id.startsWith("fc")) return item;
|
|
367
|
+
const { id: _id, ...rest } = item;
|
|
368
|
+
return rest;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const ordinaryTools = Array.isArray(tools)
|
|
372
|
+
? tools.filter((tool) => !(tool?.type === "custom" && nativeTools.has(keyOf(tool))))
|
|
373
|
+
: tools;
|
|
374
|
+
const visibleNames = providerVisibleToolNames(ordinaryTools);
|
|
375
|
+
const nativeToProvider = new Map();
|
|
376
|
+
const providerToNative = new Map();
|
|
377
|
+
const providerCodecs = new Map();
|
|
378
|
+
for (const [identity, { native, wireName }] of nativeTools) {
|
|
379
|
+
const availableName = availableCustomToolName(wireName, visibleNames);
|
|
380
|
+
const providerName = Number.isInteger(maxNameLength)
|
|
381
|
+
? reserveSpecialProviderName(
|
|
382
|
+
namespaces,
|
|
383
|
+
native.namespace === undefined ? `custom:${native.name}` : `custom:${identity}`,
|
|
384
|
+
availableName,
|
|
385
|
+
)
|
|
386
|
+
: availableName;
|
|
387
|
+
visibleNames.add(providerName);
|
|
388
|
+
nativeToProvider.set(identity, providerName);
|
|
389
|
+
providerToNative.set(providerName, native.namespace === undefined ? native.name : native);
|
|
390
|
+
// History/forced choice alone never grants a codec-backed tool. It must
|
|
391
|
+
// be a plain native custom tool declared in this exact client request.
|
|
392
|
+
if (
|
|
393
|
+
native.namespace === undefined &&
|
|
394
|
+
codecs instanceof Map && codecs.has(native.name) &&
|
|
395
|
+
tools?.some((tool) => tool?.type === "custom" && keyOf(tool) === identity)
|
|
396
|
+
) providerCodecs.set(providerName, codecs.get(native.name));
|
|
397
|
+
}
|
|
398
|
+
CUSTOM_TOOL_RELAYS.set(namespaces, providerToNative);
|
|
399
|
+
CUSTOM_TOOL_CODECS.set(namespaces, providerCodecs);
|
|
400
|
+
|
|
401
|
+
let changedTools = false;
|
|
402
|
+
const routedTools = Array.isArray(tools)
|
|
403
|
+
? tools.map((tool) => {
|
|
404
|
+
const providerName =
|
|
405
|
+
tool?.type === "custom" ? nativeToProvider.get(keyOf(tool)) : undefined;
|
|
406
|
+
if (!providerName) return tool;
|
|
407
|
+
changedTools = true;
|
|
408
|
+
const codec = providerCodecs.get(providerName);
|
|
409
|
+
const description = codec ? codec.description(tool.description) : bridgedCustomToolDescription(tool);
|
|
410
|
+
return {
|
|
411
|
+
type: "function",
|
|
412
|
+
name: providerName,
|
|
413
|
+
...(description ? { description } : {}),
|
|
414
|
+
parameters: codec?.parameters ?? {
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: {
|
|
417
|
+
[CUSTOM_TOOL_INPUT_PROPERTY]: {
|
|
418
|
+
type: "string",
|
|
419
|
+
description: "The complete raw freeform input for this tool, preserved verbatim.",
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
required: [CUSTOM_TOOL_INPUT_PROPERTY],
|
|
423
|
+
additionalProperties: false,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
})
|
|
427
|
+
: tools;
|
|
428
|
+
|
|
429
|
+
let routedToolChoice = toolChoice;
|
|
430
|
+
const providerChoiceName =
|
|
431
|
+
toolChoice?.type === "custom" ? nativeToProvider.get(keyOf(toolChoice)) : undefined;
|
|
432
|
+
if (providerChoiceName) {
|
|
433
|
+
const { namespace: _namespace, ...rest } = toolChoice;
|
|
434
|
+
routedToolChoice = { ...rest, type: "function", name: providerChoiceName };
|
|
435
|
+
SPECIAL_FUNCTION_REFERENCES.add(routedToolChoice);
|
|
436
|
+
} else if (toolChoice?.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
437
|
+
let changed = false;
|
|
438
|
+
const choices = toolChoice.tools.map((choice) => {
|
|
439
|
+
const providerName =
|
|
440
|
+
choice?.type === "custom" ? nativeToProvider.get(keyOf(choice)) : undefined;
|
|
441
|
+
if (!providerName) return choice;
|
|
442
|
+
changed = true;
|
|
443
|
+
const { namespace: _namespace, ...rest } = choice;
|
|
444
|
+
const routedChoice = { ...rest, type: "function", name: providerName };
|
|
445
|
+
SPECIAL_FUNCTION_REFERENCES.add(routedChoice);
|
|
446
|
+
return routedChoice;
|
|
447
|
+
});
|
|
448
|
+
if (changed) routedToolChoice = { ...toolChoice, tools: choices };
|
|
449
|
+
}
|
|
450
|
+
const changedToolChoice = routedToolChoice !== toolChoice;
|
|
451
|
+
|
|
452
|
+
if (!Array.isArray(input)) {
|
|
453
|
+
return {
|
|
454
|
+
tools: routedTools,
|
|
455
|
+
input,
|
|
456
|
+
toolChoice: routedToolChoice,
|
|
457
|
+
bridged: changedTools || changedToolChoice,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const bridgedCallIds = new Set();
|
|
461
|
+
let changedInput = false;
|
|
462
|
+
const routedInput = input.map((item) => {
|
|
463
|
+
const providerName =
|
|
464
|
+
item?.type === "custom_tool_call" ? nativeToProvider.get(keyOf(item)) : undefined;
|
|
465
|
+
if (providerName && typeof item.input === "string") {
|
|
466
|
+
const { type: _type, input: customInput, name: _name, namespace: _namespace, ...rest } = item;
|
|
467
|
+
if (typeof item.call_id === "string" && item.call_id) {
|
|
468
|
+
bridgedCallIds.add(item.call_id);
|
|
469
|
+
}
|
|
470
|
+
changedInput = true;
|
|
471
|
+
// A negotiated client adapter may carry its original provider argument
|
|
472
|
+
// string inside native input. Restore it verbatim, including failed JSON.
|
|
473
|
+
// History conversion does not register an executable response codec.
|
|
474
|
+
// Codecs are registered for plain native names only; a namespaced tool
|
|
475
|
+
// with the same bare name keeps the ordinary history envelope.
|
|
476
|
+
const historicalArguments = item.namespace === undefined
|
|
477
|
+
? codecs?.get(item.name)?.encodeHistoryInput?.(customInput)
|
|
478
|
+
: undefined;
|
|
479
|
+
const routedCall = withoutIncompatibleFunctionItemId({
|
|
480
|
+
...rest,
|
|
481
|
+
type: "function_call",
|
|
482
|
+
name: providerName,
|
|
483
|
+
arguments: historicalArguments ?? JSON.stringify({ [CUSTOM_TOOL_INPUT_PROPERTY]: customInput }),
|
|
484
|
+
});
|
|
485
|
+
SPECIAL_FUNCTION_REFERENCES.add(routedCall);
|
|
486
|
+
return routedCall;
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
item?.type === "custom_tool_call_output" &&
|
|
490
|
+
typeof item.call_id === "string" &&
|
|
491
|
+
bridgedCallIds.has(item.call_id)
|
|
492
|
+
) {
|
|
493
|
+
changedInput = true;
|
|
494
|
+
return withoutIncompatibleFunctionItemId({ ...item, type: "function_call_output" });
|
|
495
|
+
}
|
|
496
|
+
return item;
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
tools: routedTools,
|
|
500
|
+
input: changedInput ? routedInput : input,
|
|
501
|
+
toolChoice: routedToolChoice,
|
|
502
|
+
bridged: changedTools || changedInput || changedToolChoice,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Extra provider-visible names that restore to an already-bridged native custom
|
|
507
|
+
// tool. Used by the Grok edit facade to offer search_replace/write while Codex
|
|
508
|
+
// still executes apply_patch. Aliases never create a native tool that the
|
|
509
|
+
// client did not declare; they only add spellings onto an existing relay.
|
|
510
|
+
export function registerCustomToolRelays(namespaces, aliases) {
|
|
511
|
+
if (!(namespaces instanceof Map) || !Array.isArray(aliases) || aliases.length === 0) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
const relays = CUSTOM_TOOL_RELAYS.get(namespaces);
|
|
515
|
+
const codecs = CUSTOM_TOOL_CODECS.get(namespaces);
|
|
516
|
+
if (!(relays instanceof Map) || !(codecs instanceof Map)) return false;
|
|
517
|
+
let changed = false;
|
|
518
|
+
for (const alias of aliases) {
|
|
519
|
+
const providerName = typeof alias?.providerName === "string" ? alias.providerName.trim() : "";
|
|
520
|
+
const nativeName = typeof alias?.nativeName === "string" ? alias.nativeName.trim() : "";
|
|
521
|
+
if (!providerName || !nativeName) continue;
|
|
522
|
+
if (![...relays.values()].includes(nativeName)) continue;
|
|
523
|
+
relays.set(providerName, nativeName);
|
|
524
|
+
if (alias.codec) codecs.set(providerName, alias.codec);
|
|
525
|
+
changed = true;
|
|
526
|
+
}
|
|
527
|
+
return changed;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Provider-visible function names that restore to an ordinary client function
|
|
531
|
+
// (not a custom tool). Used by the Grok read facade to offer read_file/grep
|
|
532
|
+
// while Codex still executes exec_command. Relays never invent a native
|
|
533
|
+
// function the client did not declare.
|
|
534
|
+
export function registerFunctionRelays(namespaces, relays) {
|
|
535
|
+
if (!(namespaces instanceof Map) || !Array.isArray(relays) || relays.length === 0) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
const existing = FUNCTION_RELAYS.get(namespaces) ?? new Map();
|
|
539
|
+
let changed = false;
|
|
540
|
+
for (const relay of relays) {
|
|
541
|
+
const providerName = typeof relay?.providerName === "string" ? relay.providerName.trim() : "";
|
|
542
|
+
const nativeName = typeof relay?.nativeName === "string" ? relay.nativeName.trim() : "";
|
|
543
|
+
if (!providerName || !nativeName || typeof relay.rewriteArguments !== "function") continue;
|
|
544
|
+
existing.set(providerName, {
|
|
545
|
+
nativeName,
|
|
546
|
+
...(typeof relay.nativeNamespace === "string" && relay.nativeNamespace
|
|
547
|
+
? { nativeNamespace: relay.nativeNamespace }
|
|
548
|
+
: {}),
|
|
549
|
+
rewriteArguments: relay.rewriteArguments,
|
|
550
|
+
maxArgumentBytes: Number.isInteger(relay.maxArgumentBytes) && relay.maxArgumentBytes > 0
|
|
551
|
+
? relay.maxArgumentBytes
|
|
552
|
+
: 256 * 1024,
|
|
553
|
+
});
|
|
554
|
+
changed = true;
|
|
555
|
+
}
|
|
556
|
+
if (!changed) return false;
|
|
557
|
+
FUNCTION_RELAYS.set(namespaces, existing);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function availableToolSearchName(tools) {
|
|
562
|
+
const names = providerVisibleToolNames(tools);
|
|
563
|
+
if (!names.has(TOOL_SEARCH_FUNCTION_NAME)) return TOOL_SEARCH_FUNCTION_NAME;
|
|
564
|
+
let suffix = 1;
|
|
565
|
+
while (names.has(`codex_tool_search_${suffix}`)) suffix += 1;
|
|
566
|
+
return `codex_tool_search_${suffix}`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function providerToolSearchDescription(description, providerName) {
|
|
570
|
+
if (typeof description !== "string") return undefined;
|
|
571
|
+
if (providerName === TOOL_SEARCH_FUNCTION_NAME) return description;
|
|
572
|
+
const rewritten = description.replaceAll(
|
|
573
|
+
`\`${TOOL_SEARCH_FUNCTION_NAME}\``,
|
|
574
|
+
`\`${providerName}\``,
|
|
575
|
+
);
|
|
576
|
+
return `${rewritten}\n\nFor this routed request, call \`${providerName}\` for deferred tool discovery; \`${TOOL_SEARCH_FUNCTION_NAME}\` is a separate ordinary function.`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function schemaStringValues(schema, values = new Set()) {
|
|
580
|
+
if (!schema || typeof schema !== "object") return values;
|
|
581
|
+
if (typeof schema.const === "string") values.add(schema.const);
|
|
582
|
+
if (Array.isArray(schema.enum)) {
|
|
583
|
+
for (const value of schema.enum) if (typeof value === "string") values.add(value);
|
|
584
|
+
}
|
|
585
|
+
for (const keyword of ["anyOf", "oneOf", "allOf"]) {
|
|
586
|
+
if (!Array.isArray(schema[keyword])) continue;
|
|
587
|
+
for (const branch of schema[keyword]) schemaStringValues(branch, values);
|
|
588
|
+
}
|
|
589
|
+
return values;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// A fresh local thread inherits the routed session model when the caller did
|
|
593
|
+
// not choose one. An in-session subagent is always pinned to the routed parent:
|
|
594
|
+
// its model argument is generated by the parent model, not an independent user
|
|
595
|
+
// choice, and preserving a cross-provider value can silently cross a billing
|
|
596
|
+
// boundary. Follow-up messages intentionally keep the target thread's settings,
|
|
597
|
+
// and cloud tasks require model omission, so neither is rewritten.
|
|
598
|
+
export const SPAWN_MODEL_TOOLS = new Set(["create_thread", "spawn_agent"]);
|
|
599
|
+
const SPAWN_MODEL_NAMESPACES = new Map([
|
|
600
|
+
["codex_app", new Set(["create_thread"])],
|
|
601
|
+
["collaboration", new Set(["spawn_agent"])],
|
|
602
|
+
]);
|
|
603
|
+
|
|
604
|
+
function isSpawnModelCall(item) {
|
|
605
|
+
if (!item || typeof item.name !== "string") return false;
|
|
606
|
+
// Flattened forms the router sends to chat-completions bridges, such as
|
|
607
|
+
// `codex_app__create_thread` and `collaboration__spawn_agent`.
|
|
608
|
+
for (const [namespace, names] of SPAWN_MODEL_NAMESPACES) {
|
|
609
|
+
const prefix = `${namespace}${NAMESPACE_DELIMITER}`;
|
|
610
|
+
if (item.name.startsWith(prefix)) return names.has(item.name.slice(prefix.length));
|
|
611
|
+
}
|
|
612
|
+
// Native namespace form openai-responses providers keep.
|
|
613
|
+
return SPAWN_MODEL_NAMESPACES.get(item.namespace)?.has(item.name) === true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function isSubagentSpawnCall(item) {
|
|
617
|
+
if (!item || typeof item.name !== "string") return false;
|
|
618
|
+
if (item.namespace === "collaboration" && item.name === "spawn_agent") return true;
|
|
619
|
+
return item.namespace === undefined && item.name === `collaboration${NAMESPACE_DELIMITER}spawn_agent`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Inject the session model into local create_thread calls that omitted it, and
|
|
623
|
+
// into spawn_agent calls that carry no model of their own.
|
|
624
|
+
//
|
|
625
|
+
// An explicit subagent model wins. Codex ships the override as a plain string
|
|
626
|
+
// on the tool schema rather than a schema enum, and validates the value against
|
|
627
|
+
// its own advertised list, so anything that reaches here is the operator's own
|
|
628
|
+
// delegation choice and not a value the parent model invented. Rewriting it
|
|
629
|
+
// back to the routed parent discarded that choice and made a cross-provider
|
|
630
|
+
// subagent impossible from any routed session, which is the one thing
|
|
631
|
+
// `expose_spawn_agent_model_overrides = true` exists to allow.
|
|
632
|
+
//
|
|
633
|
+
// `sanitizeSpawnAgentModel` still drops a value outside the advertised set when
|
|
634
|
+
// a client version does ship one, so that guard is unaffected: the item it
|
|
635
|
+
// clears arrives here without a model and inherits the parent as before.
|
|
636
|
+
//
|
|
637
|
+
// `model` is the routed session's model (route.slug). Returns a rewritten item
|
|
638
|
+
// only when the call carries no model of its own; otherwise returns the item
|
|
639
|
+
// untouched.
|
|
640
|
+
export function injectSessionModelForSpawnCalls(item, model) {
|
|
641
|
+
if (!isSpawnModelCall(item)) return item;
|
|
642
|
+
if (typeof model !== "string" || !model) return item;
|
|
643
|
+
if (typeof item.arguments !== "string") return item;
|
|
644
|
+
if (!jsonArgumentsAreUnambiguous(item.arguments, { allowEmpty: true })) return item;
|
|
645
|
+
let args;
|
|
646
|
+
try {
|
|
647
|
+
args = JSON.parse(item.arguments);
|
|
648
|
+
} catch {
|
|
649
|
+
return item;
|
|
650
|
+
}
|
|
651
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return item;
|
|
652
|
+
if (args.model !== undefined && !isSubagentSpawnCall(item)) return item;
|
|
653
|
+
if (args.target?.type === "chatgptWorkCloud") return item;
|
|
654
|
+
if (typeof args.model === "string" && args.model) return item;
|
|
655
|
+
return { ...item, arguments: JSON.stringify({ ...args, model }) };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const MAX_JSON_CAPTURE_BYTES = 64 * 1024 * 1024;
|
|
659
|
+
const CAPTURE_PART_BYTES = 64 * 1024;
|
|
660
|
+
const INITIAL_SSE_CAPTURE_PART_BYTES = 1024;
|
|
661
|
+
const MAX_TRACKED_OUTPUT_ITEMS = 4096;
|
|
662
|
+
const MAX_TRACKED_STATE_BYTES = 8 * 1024 * 1024;
|
|
663
|
+
const TRACKED_STATE_FIXED_BYTES = 512;
|
|
664
|
+
// Before any semantic output, stop staging an undecided SSE frame before
|
|
665
|
+
// downstream response guards lose sight of their own byte ceilings. Once a
|
|
666
|
+
// namespace rewrite, suppression, or injection has committed the wire shape,
|
|
667
|
+
// a later terminal event may carry the complete response and therefore shares
|
|
668
|
+
// the non-streaming JSON capture bound. Crossing either phase's bound releases
|
|
669
|
+
// raw bytes before a commit and terminates the stream after one.
|
|
670
|
+
const MAX_SSE_FRAME_BYTES = 10 * 1024 * 1024;
|
|
671
|
+
const MAX_COMMITTED_SSE_FRAME_BYTES = MAX_JSON_CAPTURE_BYTES;
|
|
672
|
+
const LINE_FEED = 0x0a;
|
|
673
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
674
|
+
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
675
|
+
const SSE_EVENT_FIELD = Buffer.from("event", "ascii");
|
|
676
|
+
const SSE_DATA_FIELD = Buffer.from("data", "ascii");
|
|
677
|
+
const JSON_NUMBER_AT_OFFSET = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
678
|
+
|
|
679
|
+
function sseFieldValue(line, prefixLength) {
|
|
680
|
+
const value = line.slice(prefixLength);
|
|
681
|
+
// The SSE grammar removes one optional U+0020 after the colon. Tabs,
|
|
682
|
+
// repeated spaces, and trailing spaces are event data, not formatting.
|
|
683
|
+
return value.startsWith(" ") ? value.slice(1) : value;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function sseLineFieldValue(line, name) {
|
|
687
|
+
return line === name ? "" : sseFieldValue(line, name.length + 1);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function stringFingerprint(value) {
|
|
691
|
+
return {
|
|
692
|
+
length: value.length,
|
|
693
|
+
digest: createHash("sha256").update(value, "utf16le").digest(),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function canonicalJsonFingerprint(value) {
|
|
698
|
+
const hash = createHash("sha256");
|
|
699
|
+
let length = 0;
|
|
700
|
+
const update = (part, encoding = "utf8") => {
|
|
701
|
+
hash.update(part, encoding);
|
|
702
|
+
length += Buffer.byteLength(part, encoding);
|
|
703
|
+
};
|
|
704
|
+
const updateString = (marker, text) => {
|
|
705
|
+
update(`${marker}${text.length}:`, "ascii");
|
|
706
|
+
update(text, "utf16le");
|
|
707
|
+
};
|
|
708
|
+
const stack = [{ kind: "value", value }];
|
|
709
|
+
while (stack.length) {
|
|
710
|
+
const frame = stack.pop();
|
|
711
|
+
if (frame.kind === "array") {
|
|
712
|
+
if (frame.index >= frame.value.length) {
|
|
713
|
+
update("]", "ascii");
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
stack.push({ ...frame, index: frame.index + 1 });
|
|
717
|
+
stack.push({ kind: "value", value: frame.value[frame.index] });
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (frame.kind === "object") {
|
|
721
|
+
if (frame.index >= frame.keys.length) {
|
|
722
|
+
update("}", "ascii");
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const key = frame.keys[frame.index];
|
|
726
|
+
stack.push({ ...frame, index: frame.index + 1 });
|
|
727
|
+
stack.push({ kind: "value", value: frame.value[key] });
|
|
728
|
+
stack.push({ kind: "key", value: key });
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (frame.kind === "key") {
|
|
732
|
+
updateString("k", frame.value);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const current = frame.value;
|
|
737
|
+
if (current === null) {
|
|
738
|
+
update("n", "ascii");
|
|
739
|
+
} else if (typeof current === "string") {
|
|
740
|
+
updateString("s", current);
|
|
741
|
+
} else if (typeof current === "number") {
|
|
742
|
+
const number = Number.isNaN(current)
|
|
743
|
+
? "NaN"
|
|
744
|
+
: Object.is(current, -0)
|
|
745
|
+
? "-0"
|
|
746
|
+
: String(current);
|
|
747
|
+
updateString("d", number);
|
|
748
|
+
} else if (typeof current === "boolean") {
|
|
749
|
+
update(current ? "t" : "f", "ascii");
|
|
750
|
+
} else if (Array.isArray(current)) {
|
|
751
|
+
update(`a${current.length}:[`, "ascii");
|
|
752
|
+
stack.push({ kind: "array", value: current, index: 0 });
|
|
753
|
+
} else if (typeof current === "object") {
|
|
754
|
+
const keys = Object.keys(current).sort();
|
|
755
|
+
update(`o${keys.length}:{`, "ascii");
|
|
756
|
+
stack.push({ kind: "object", value: current, keys, index: 0 });
|
|
757
|
+
} else {
|
|
758
|
+
throw new TypeError("Tool-search arguments contain a non-JSON value.");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { length, digest: hash.digest() };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function fingerprintMatches(fingerprint, length, digest) {
|
|
765
|
+
return (
|
|
766
|
+
fingerprint.length === length &&
|
|
767
|
+
Buffer.isBuffer(digest) &&
|
|
768
|
+
fingerprint.digest.equals(digest)
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function trackedStateBytes(state) {
|
|
773
|
+
let bytes = TRACKED_STATE_FIXED_BYTES;
|
|
774
|
+
for (const field of [
|
|
775
|
+
"itemId",
|
|
776
|
+
"callId",
|
|
777
|
+
"sourceType",
|
|
778
|
+
"sourceName",
|
|
779
|
+
"sourceNamespace",
|
|
780
|
+
"outputType",
|
|
781
|
+
"outputName",
|
|
782
|
+
"outputNamespace",
|
|
783
|
+
]) {
|
|
784
|
+
if (typeof state[field] === "string") bytes += Buffer.byteLength(state[field], "utf8");
|
|
785
|
+
}
|
|
786
|
+
return bytes;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// JSON.parse deliberately accepts duplicate object members and keeps the last
|
|
790
|
+
// one. That is useful for ordinary application input, but unsafe at a rewrite
|
|
791
|
+
// boundary: the bytes can name one tool first and another tool last, while a
|
|
792
|
+
// downstream parser is free to make the opposite choice. Parsing also rounds
|
|
793
|
+
// unsafe integers and accepts overflowing exponents that stringify as null.
|
|
794
|
+
// Audit the complete JSON grammar before parsing, compare decoded key values
|
|
795
|
+
// so spellings such as `"name"` and `"\u006eame"` collide, and reject numeric
|
|
796
|
+
// values whose parse/stringify semantics are known to be lossy.
|
|
797
|
+
function jsonIsUnambiguousForRewrite(text, { allowLossyNumbers = false } = {}) {
|
|
798
|
+
if (typeof text !== "string") return false;
|
|
799
|
+
let offset = 0;
|
|
800
|
+
|
|
801
|
+
const skipWhitespace = () => {
|
|
802
|
+
while (
|
|
803
|
+
offset < text.length &&
|
|
804
|
+
(text[offset] === " " ||
|
|
805
|
+
text[offset] === "\t" ||
|
|
806
|
+
text[offset] === "\r" ||
|
|
807
|
+
text[offset] === "\n")
|
|
808
|
+
) {
|
|
809
|
+
offset += 1;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const stringToken = () => {
|
|
814
|
+
if (text[offset] !== '"') return undefined;
|
|
815
|
+
const start = offset;
|
|
816
|
+
offset += 1;
|
|
817
|
+
while (offset < text.length) {
|
|
818
|
+
const code = text.charCodeAt(offset);
|
|
819
|
+
const character = text[offset];
|
|
820
|
+
if (character === '"') {
|
|
821
|
+
offset += 1;
|
|
822
|
+
return text.slice(start, offset);
|
|
823
|
+
}
|
|
824
|
+
if (code < 0x20) return undefined;
|
|
825
|
+
if (character !== "\\") {
|
|
826
|
+
offset += 1;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
offset += 1;
|
|
830
|
+
const escape = text[offset];
|
|
831
|
+
if (escape === "u") {
|
|
832
|
+
const digits = text.slice(offset + 1, offset + 5);
|
|
833
|
+
if (digits.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(digits)) return undefined;
|
|
834
|
+
offset += 5;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (!['"', "\\", "/", "b", "f", "n", "r", "t"].includes(escape)) {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
offset += 1;
|
|
841
|
+
}
|
|
842
|
+
return undefined;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const literal = (value) => {
|
|
846
|
+
if (!text.startsWith(value, offset)) return false;
|
|
847
|
+
offset += value.length;
|
|
848
|
+
return true;
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const number = () => {
|
|
852
|
+
JSON_NUMBER_AT_OFFSET.lastIndex = offset;
|
|
853
|
+
const match = JSON_NUMBER_AT_OFFSET.exec(text);
|
|
854
|
+
if (!match) return false;
|
|
855
|
+
// Turn metadata is inspected only for string/bool namespace identities and
|
|
856
|
+
// is never reserialized here. Its unrelated numeric fields may therefore
|
|
857
|
+
// be lossy without changing the identity decision. Duplicate object keys
|
|
858
|
+
// remain forbidden in every mode: JSON.parse's last-wins behavior would
|
|
859
|
+
// otherwise let ambiguous metadata hide an ordinary-function collision.
|
|
860
|
+
if (!allowLossyNumbers && !jsonNumberIsStableForRewrite(match[0])) return false;
|
|
861
|
+
offset = JSON_NUMBER_AT_OFFSET.lastIndex;
|
|
862
|
+
return true;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const value = () => {
|
|
866
|
+
skipWhitespace();
|
|
867
|
+
const character = text[offset];
|
|
868
|
+
if (character === "{") return object();
|
|
869
|
+
if (character === "[") return array();
|
|
870
|
+
if (character === '"') return stringToken() !== undefined;
|
|
871
|
+
if (character === "t") return literal("true");
|
|
872
|
+
if (character === "f") return literal("false");
|
|
873
|
+
if (character === "n") return literal("null");
|
|
874
|
+
return number();
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const object = () => {
|
|
878
|
+
offset += 1;
|
|
879
|
+
skipWhitespace();
|
|
880
|
+
if (text[offset] === "}") {
|
|
881
|
+
offset += 1;
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
const keys = new Set();
|
|
885
|
+
while (offset < text.length) {
|
|
886
|
+
skipWhitespace();
|
|
887
|
+
const token = stringToken();
|
|
888
|
+
if (token === undefined) return false;
|
|
889
|
+
let key;
|
|
890
|
+
try {
|
|
891
|
+
key = JSON.parse(token);
|
|
892
|
+
} catch {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (keys.has(key)) return false;
|
|
896
|
+
keys.add(key);
|
|
897
|
+
skipWhitespace();
|
|
898
|
+
if (text[offset] !== ":") return false;
|
|
899
|
+
offset += 1;
|
|
900
|
+
if (!value()) return false;
|
|
901
|
+
skipWhitespace();
|
|
902
|
+
if (text[offset] === "}") {
|
|
903
|
+
offset += 1;
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
if (text[offset] !== ",") return false;
|
|
907
|
+
offset += 1;
|
|
908
|
+
}
|
|
909
|
+
return false;
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const array = () => {
|
|
913
|
+
offset += 1;
|
|
914
|
+
skipWhitespace();
|
|
915
|
+
if (text[offset] === "]") {
|
|
916
|
+
offset += 1;
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
while (offset < text.length) {
|
|
920
|
+
if (!value()) return false;
|
|
921
|
+
skipWhitespace();
|
|
922
|
+
if (text[offset] === "]") {
|
|
923
|
+
offset += 1;
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
if (text[offset] !== ",") return false;
|
|
927
|
+
offset += 1;
|
|
928
|
+
}
|
|
929
|
+
return false;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
try {
|
|
933
|
+
if (!value()) return false;
|
|
934
|
+
skipWhitespace();
|
|
935
|
+
return offset === text.length;
|
|
936
|
+
} catch {
|
|
937
|
+
// Excessive nesting and any other scanner failure are ambiguity, not
|
|
938
|
+
// permission to fall back to JSON.parse's lossy interpretation.
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
export function jsonArgumentsAreUnambiguous(value, { allowEmpty = false } = {}) {
|
|
944
|
+
if (typeof value !== "string") return true;
|
|
945
|
+
if (allowEmpty && value.trim() === "") return true;
|
|
946
|
+
return jsonIsUnambiguousForRewrite(value);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Repair one tool's parameter root, or return it untouched. Providers reject a
|
|
950
|
+
// union or nullable-object root by name -- xAI, DeepSeek V4, and the
|
|
951
|
+
// opencode-go Responses surface all do -- and none of them care whether the
|
|
952
|
+
// tool arrived inside a namespace. `providerToolSchema` returns anything it
|
|
953
|
+
// does not recognize by identity, so an ordinary root costs one call and no
|
|
954
|
+
// copy.
|
|
955
|
+
export function repairToolSchemaRoot(
|
|
956
|
+
tool,
|
|
957
|
+
{ nonRecursive = false, inlineForeignRefs: inlineRefs = false, declareTypes = false } = {},
|
|
958
|
+
) {
|
|
959
|
+
// Moonshot alone rejects a `$ref` that does not point into `#/$defs/` or one
|
|
960
|
+
// that carries sibling keywords, so only the route that asks for it pays the
|
|
961
|
+
// inlining -- every other provider keeps the exact wire payload it has today.
|
|
962
|
+
// The normalizer returns a clean schema by identity, so an ordinary toolset
|
|
963
|
+
// is not copied.
|
|
964
|
+
const relaySchema = (schema) => {
|
|
965
|
+
const repaired = providerToolSchema(schema);
|
|
966
|
+
const inlined = inlineRefs ? inlineForeignRefs(repaired) : repaired;
|
|
967
|
+
// After inlining, so a type is declared on the branches a `$ref` brought in
|
|
968
|
+
// rather than only on the reference that pointed at them.
|
|
969
|
+
return declareTypes ? declareSchemaTypes(inlined) : inlined;
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// Preserve the established shared-provider behavior byte-for-byte. Native
|
|
973
|
+
// namespace traversal and inputSchema rewriting belong only to the OpenCode
|
|
974
|
+
// compatibility pass below; other providers keep the original root repair.
|
|
975
|
+
if (!nonRecursive) {
|
|
976
|
+
const parameters = tool?.function?.parameters ?? tool?.parameters;
|
|
977
|
+
if (parameters === undefined) return tool;
|
|
978
|
+
const repaired = relaySchema(parameters);
|
|
979
|
+
if (repaired === parameters) return tool;
|
|
980
|
+
return tool.function
|
|
981
|
+
? { ...tool, function: { ...tool.function, parameters: repaired } }
|
|
982
|
+
: { ...tool, parameters: repaired };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (tool?.type === "namespace" && Array.isArray(tool.tools)) {
|
|
986
|
+
let changed = false;
|
|
987
|
+
const children = tool.tools.map((child) => {
|
|
988
|
+
const repaired = repairToolSchemaRoot(child, {
|
|
989
|
+
nonRecursive,
|
|
990
|
+
inlineForeignRefs: inlineRefs,
|
|
991
|
+
});
|
|
992
|
+
if (repaired !== child) changed = true;
|
|
993
|
+
return repaired;
|
|
994
|
+
});
|
|
995
|
+
return changed ? { ...tool, tools: children } : tool;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
let repairedTool = tool;
|
|
999
|
+
let changed = false;
|
|
1000
|
+
const repair = (schema) => nonRecursiveToolSchema(relaySchema(schema));
|
|
1001
|
+
|
|
1002
|
+
if (tool?.function?.parameters !== undefined) {
|
|
1003
|
+
const parameters = repair(tool.function.parameters);
|
|
1004
|
+
if (parameters !== tool.function.parameters) {
|
|
1005
|
+
repairedTool = {
|
|
1006
|
+
...repairedTool,
|
|
1007
|
+
function: { ...repairedTool.function, parameters },
|
|
1008
|
+
};
|
|
1009
|
+
changed = true;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// Flattened namespace children deliberately carry both inputSchema (the
|
|
1013
|
+
// client-native declaration) and parameters (the Chat Completions alias).
|
|
1014
|
+
// Repair both: choosing inputSchema first would leave the provider-facing
|
|
1015
|
+
// parameters recursive on Ox even though the Responses branch was fixed.
|
|
1016
|
+
for (const field of ["parameters", "inputSchema"]) {
|
|
1017
|
+
if (tool?.[field] === undefined) continue;
|
|
1018
|
+
const schema = repair(tool[field]);
|
|
1019
|
+
if (schema === tool[field]) continue;
|
|
1020
|
+
repairedTool = { ...repairedTool, [field]: schema };
|
|
1021
|
+
changed = true;
|
|
1022
|
+
}
|
|
1023
|
+
return changed ? repairedTool : tool;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Array form for callers that relay tools without flattening them --
|
|
1027
|
+
// Responses-native providers keep the namespace shape but still need a root
|
|
1028
|
+
// their upstream accepts. Returns the original array when nothing needed
|
|
1029
|
+
// repair, so the common request is not copied.
|
|
1030
|
+
export function repairToolSchemaRoots(tools, options) {
|
|
1031
|
+
if (!Array.isArray(tools)) return tools;
|
|
1032
|
+
let changed = false;
|
|
1033
|
+
const repaired = tools.map((tool) => {
|
|
1034
|
+
const next = repairToolSchemaRoot(tool, options);
|
|
1035
|
+
if (next !== tool) changed = true;
|
|
1036
|
+
return next;
|
|
1037
|
+
});
|
|
1038
|
+
return changed ? repaired : tools;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// OpenCode currently accepts search_content_types only on the legacy
|
|
1042
|
+
// web_search_preview shape. Codex sends the field on web_search, so remove only
|
|
1043
|
+
// that unsupported extension and preserve every other search-tool option.
|
|
1044
|
+
export function stripSearchContentTypes(tools) {
|
|
1045
|
+
if (!Array.isArray(tools)) return tools;
|
|
1046
|
+
let changed = false;
|
|
1047
|
+
const stripped = tools.map((tool) => {
|
|
1048
|
+
if (tool?.type !== "web_search" || !("search_content_types" in tool)) return tool;
|
|
1049
|
+
changed = true;
|
|
1050
|
+
const { search_content_types: _unsupported, ...rest } = tool;
|
|
1051
|
+
return rest;
|
|
1052
|
+
});
|
|
1053
|
+
return changed ? stripped : tools;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// agent_message is a Codex collaboration input item, not part of the public
|
|
1057
|
+
// Responses schema OpenCode implements. The readable handoff has already been
|
|
1058
|
+
// recovered before this boundary, so keep its content and present it as the
|
|
1059
|
+
// equivalent user message strict compatible endpoints accept.
|
|
1060
|
+
export function agentMessagesAsUserMessages(input) {
|
|
1061
|
+
if (!Array.isArray(input)) return input;
|
|
1062
|
+
let changed = false;
|
|
1063
|
+
const converted = input.map((item) => {
|
|
1064
|
+
if (item?.type !== "agent_message" || !Array.isArray(item.content)) return item;
|
|
1065
|
+
changed = true;
|
|
1066
|
+
return { type: "message", role: "user", content: item.content };
|
|
1067
|
+
});
|
|
1068
|
+
return changed ? converted : input;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Codex can inherit an image with detail:"original" from the parent thread.
|
|
1072
|
+
// OpenCode rejects that OpenAI-only hint but accepts the same image as auto.
|
|
1073
|
+
// Preserve the image bytes and surrounding transcript; change only the hint.
|
|
1074
|
+
export function downgradeOriginalImageDetail(input) {
|
|
1075
|
+
if (!Array.isArray(input)) return input;
|
|
1076
|
+
let changed = false;
|
|
1077
|
+
const converted = input.map((item) => {
|
|
1078
|
+
if (!Array.isArray(item?.content)) return item;
|
|
1079
|
+
let contentChanged = false;
|
|
1080
|
+
const content = item.content.map((part) => {
|
|
1081
|
+
if (part?.type !== "input_image" || part.detail !== "original") return part;
|
|
1082
|
+
changed = true;
|
|
1083
|
+
contentChanged = true;
|
|
1084
|
+
return { ...part, detail: "auto" };
|
|
1085
|
+
});
|
|
1086
|
+
return contentChanged ? { ...item, content } : item;
|
|
1087
|
+
});
|
|
1088
|
+
return changed ? converted : input;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const REASONING_ENCRYPTED_INCLUDE = "reasoning.encrypted_content";
|
|
1092
|
+
|
|
1093
|
+
function reasoningItemHasVisibleText(item) {
|
|
1094
|
+
if (typeof item?.summary === "string" && item.summary) return true;
|
|
1095
|
+
if (
|
|
1096
|
+
Array.isArray(item?.summary) &&
|
|
1097
|
+
item.summary.some((part) => typeof part?.text === "string" && part.text)
|
|
1098
|
+
) {
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
if (typeof item?.content === "string" && item.content) return true;
|
|
1102
|
+
if (
|
|
1103
|
+
Array.isArray(item?.content) &&
|
|
1104
|
+
item.content.some((part) => typeof part?.text === "string" && part.text)
|
|
1105
|
+
) {
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// OpenCode Zen's anonymous Muse Contributor Free Responses route is a Console
|
|
1112
|
+
// proxy. Meta issues reasoning `encrypted_content` to Console's caller, not to
|
|
1113
|
+
// this router. Replaying it 400s with "reasoning `encrypted_content` was not
|
|
1114
|
+
// issued to this caller". Drop the continuation token; keep any summary text.
|
|
1115
|
+
// Paid Zen/Go keep a stable key and stay outside this exact-route gate.
|
|
1116
|
+
export function stripUnissuedEncryptedReasoning(input) {
|
|
1117
|
+
if (!Array.isArray(input)) return input;
|
|
1118
|
+
let changed = false;
|
|
1119
|
+
const next = [];
|
|
1120
|
+
for (const item of input) {
|
|
1121
|
+
if (item?.type !== "reasoning" || item.encrypted_content === undefined) {
|
|
1122
|
+
next.push(item);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
changed = true;
|
|
1126
|
+
const { encrypted_content: _encryptedContent, ...rest } = item;
|
|
1127
|
+
if (reasoningItemHasVisibleText(rest)) next.push(rest);
|
|
1128
|
+
}
|
|
1129
|
+
return changed ? next : input;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export function stripUnissuedEncryptedReasoningInclude(include) {
|
|
1133
|
+
if (!Array.isArray(include)) return include;
|
|
1134
|
+
const next = include.filter((entry) => entry !== REASONING_ENCRYPTED_INCLUDE);
|
|
1135
|
+
if (next.length === include.length) return include;
|
|
1136
|
+
return next.length > 0 ? next : undefined;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function flattenNamespaceChild(namespace, fn, providerName) {
|
|
1140
|
+
const clientSchema = fn.parameters ?? fn.inputSchema;
|
|
1141
|
+
const parameters =
|
|
1142
|
+
clientSchema === undefined ? undefined : providerToolSchema(clientSchema);
|
|
1143
|
+
const flattened = {
|
|
1144
|
+
...fn,
|
|
1145
|
+
name: providerName ?? `${namespace}${NAMESPACE_DELIMITER}${fn.name}`,
|
|
1146
|
+
...(parameters === undefined ? {} : { parameters }),
|
|
1147
|
+
};
|
|
1148
|
+
if (fn.type === "custom") {
|
|
1149
|
+
CUSTOM_TOOL_IDENTITIES.set(flattened, { namespace, name: fn.name });
|
|
1150
|
+
}
|
|
1151
|
+
return flattened;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Flatten every namespace entry into plain functions named
|
|
1155
|
+
// `<namespace>__<tool>`. Returns the set of namespaces that were flattened
|
|
1156
|
+
// (name -> tool names) so callers can rename history and restore calls.
|
|
1157
|
+
export function flattenNamespaceTools(
|
|
1158
|
+
tools,
|
|
1159
|
+
{ bridgeToolSearch = true, maxNameLength, aliasCollisions = false } = {},
|
|
1160
|
+
) {
|
|
1161
|
+
if (!Array.isArray(tools)) return { tools, flattened: false, namespaces: new Map() };
|
|
1162
|
+
const flattened = [];
|
|
1163
|
+
const namespaces = new Map();
|
|
1164
|
+
const plainToolNames = new Set();
|
|
1165
|
+
PLAIN_TOOL_NAMES.set(namespaces, plainToolNames);
|
|
1166
|
+
initializeNameAliases(namespaces, tools, maxNameLength, aliasCollisions);
|
|
1167
|
+
const spawnAgentModels = new Set();
|
|
1168
|
+
const toolSearchName = bridgeToolSearch
|
|
1169
|
+
? reserveSpecialProviderName(
|
|
1170
|
+
namespaces,
|
|
1171
|
+
"tool-search",
|
|
1172
|
+
availableToolSearchName(tools),
|
|
1173
|
+
)
|
|
1174
|
+
: undefined;
|
|
1175
|
+
let toolSearchRelay;
|
|
1176
|
+
let changed = false;
|
|
1177
|
+
for (const tool of tools) {
|
|
1178
|
+
// Codex registers deferred tools client-side and exposes this native
|
|
1179
|
+
// control so the model can search them on demand. Chat-completions
|
|
1180
|
+
// providers reject the native type, so present the same request-local
|
|
1181
|
+
// capability as an ordinary function and restore its calls on the
|
|
1182
|
+
// response path. Only the native client-executed shape enables the relay;
|
|
1183
|
+
// an unrelated function named `tool_search` never does. When such a plain
|
|
1184
|
+
// function already exists, use a deterministic alias so neither call can
|
|
1185
|
+
// hijack the other.
|
|
1186
|
+
if (tool?.type === "tool_search") {
|
|
1187
|
+
changed = true;
|
|
1188
|
+
if (bridgeToolSearch && tool.execution === "client" && !toolSearchRelay) {
|
|
1189
|
+
const parameters =
|
|
1190
|
+
tool.parameters === undefined ? undefined : providerToolSchema(tool.parameters);
|
|
1191
|
+
const description = providerToolSearchDescription(tool.description, toolSearchName);
|
|
1192
|
+
flattened.push({
|
|
1193
|
+
type: "function",
|
|
1194
|
+
name: toolSearchName,
|
|
1195
|
+
...(description === undefined ? {} : { description }),
|
|
1196
|
+
...(parameters === undefined ? {} : { parameters }),
|
|
1197
|
+
});
|
|
1198
|
+
toolSearchRelay = { providerName: toolSearchName };
|
|
1199
|
+
}
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (tool?.type === "namespace" && Array.isArray(tool.tools)) {
|
|
1203
|
+
const names = new Set();
|
|
1204
|
+
for (const fn of tool.tools) {
|
|
1205
|
+
if (!fn?.name) continue;
|
|
1206
|
+
// Codex names function schemas `inputSchema`, while LiteLLM's
|
|
1207
|
+
// Responses -> Chat Completions adapter reads only `parameters`.
|
|
1208
|
+
// Without this alias every flattened namespace child reaches the
|
|
1209
|
+
// provider as an empty object schema, so MCP calls cannot receive the
|
|
1210
|
+
// arguments their server requires. Keep inputSchema too: it is the
|
|
1211
|
+
// client's native representation and responses-native routes retain
|
|
1212
|
+
// it untouched.
|
|
1213
|
+
//
|
|
1214
|
+
// Strict upstreams (Moonshot/Kimi, the xAI CLI proxy) reject the whole
|
|
1215
|
+
// request -- not the one tool -- over a union-rooted parameter schema or
|
|
1216
|
+
// an enum literal that contradicts its declared type. Codex's own
|
|
1217
|
+
// `codex_app__automation_update` ships a `oneOf` root, so a session that
|
|
1218
|
+
// never touches automations still dies on its first message. Normalize
|
|
1219
|
+
// only the provider-facing copy; `inputSchema` stays exactly as the
|
|
1220
|
+
// client sent it.
|
|
1221
|
+
flattened.push(
|
|
1222
|
+
flattenNamespaceChild(
|
|
1223
|
+
tool.name,
|
|
1224
|
+
fn,
|
|
1225
|
+
providerNameForNative(namespaces, tool.name, fn.name),
|
|
1226
|
+
),
|
|
1227
|
+
);
|
|
1228
|
+
names.add(fn.name);
|
|
1229
|
+
if (tool.name === "collaboration" && fn.name === "spawn_agent") {
|
|
1230
|
+
const schema = fn.parameters ?? fn.inputSchema;
|
|
1231
|
+
schemaStringValues(schema?.properties?.model, spawnAgentModels);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (names.size > 0) {
|
|
1235
|
+
namespaces.set(tool.name, names);
|
|
1236
|
+
changed = true;
|
|
1237
|
+
}
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
// A plain function tool needs the same repair as a namespaced one. The
|
|
1241
|
+
// rejections are the provider's, not the namespace's: DeepSeek V4 Flash and
|
|
1242
|
+
// Pro both 400 a `type: ["object","null"]` root with "schema must be a JSON
|
|
1243
|
+
// Schema of 'type: \"object\"'", and xAI rejects a union root the same way,
|
|
1244
|
+
// whether the tool arrived inside a namespace or on its own. Repairing only
|
|
1245
|
+
// the flattened children left every client-declared tool to fail on the
|
|
1246
|
+
// provider that objects. `providerToolSchema` returns anything it does not
|
|
1247
|
+
// recognize unchanged, so a tool with an ordinary root is not copied.
|
|
1248
|
+
let repaired = repairToolSchemaRoot(tool);
|
|
1249
|
+
const name = tool?.type === "function" ? providerFunctionName(repaired) : undefined;
|
|
1250
|
+
if (typeof name === "string" && name) {
|
|
1251
|
+
const providerName = providerNameForNative(namespaces, undefined, name);
|
|
1252
|
+
if (providerName !== name) repaired = withProviderFunctionName(repaired, providerName);
|
|
1253
|
+
plainToolNames.add(providerName);
|
|
1254
|
+
}
|
|
1255
|
+
if (repaired !== tool) changed = true;
|
|
1256
|
+
flattened.push(repaired);
|
|
1257
|
+
}
|
|
1258
|
+
if (spawnAgentModels.size > 0) SPAWN_AGENT_MODELS.set(namespaces, spawnAgentModels);
|
|
1259
|
+
if (toolSearchRelay) TOOL_SEARCH_RELAYS.set(namespaces, toolSearchRelay);
|
|
1260
|
+
return { tools: flattened, flattened: changed, namespaces };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Codex may flatten native function/custom names before sending a custom-provider
|
|
1264
|
+
// request. Recover only declarations backed by its canonical turn metadata, then
|
|
1265
|
+
// let the normal namespace pipeline handle aliases, schemas, custom tools and
|
|
1266
|
+
// response restoration. Metadata alone must never add an executable tool.
|
|
1267
|
+
export function restorePreflattenedToolNamespaces(tools, clientMetadata) {
|
|
1268
|
+
if (!Array.isArray(tools)) return tools;
|
|
1269
|
+
const encoded = clientMetadata?.["x-codex-turn-metadata"];
|
|
1270
|
+
if (typeof encoded !== "string" ||
|
|
1271
|
+
!jsonIsUnambiguousForRewrite(encoded, { allowLossyNumbers: true })) return tools;
|
|
1272
|
+
let metadata;
|
|
1273
|
+
try {
|
|
1274
|
+
metadata = JSON.parse(encoded);
|
|
1275
|
+
} catch {
|
|
1276
|
+
return tools;
|
|
1277
|
+
}
|
|
1278
|
+
const inventory = metadata?.tool_namespaces_info;
|
|
1279
|
+
if (!plainObject(inventory)) return tools;
|
|
1280
|
+
|
|
1281
|
+
// Default-namespace entries are already plain tools. Never reinterpret a
|
|
1282
|
+
// literal name containing __ merely because another namespace claims it.
|
|
1283
|
+
const ordinaryNames = new Set();
|
|
1284
|
+
if (Object.hasOwn(inventory, DEFAULT_FUNCTION_NAMESPACE)) {
|
|
1285
|
+
const ordinary = inventory[DEFAULT_FUNCTION_NAMESPACE];
|
|
1286
|
+
if (ordinary?.name !== DEFAULT_FUNCTION_NAMESPACE ||
|
|
1287
|
+
!plainObject(ordinary.functions)) return tools;
|
|
1288
|
+
for (const [name, info] of Object.entries(ordinary.functions)) {
|
|
1289
|
+
if (!name || !plainObject(info) || info.name !== name) return tools;
|
|
1290
|
+
ordinaryNames.add(name);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const candidates = new Map();
|
|
1295
|
+
const remember = (wireName, native) => {
|
|
1296
|
+
if (!candidates.has(wireName)) {
|
|
1297
|
+
candidates.set(wireName, native);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
const previous = candidates.get(wireName);
|
|
1301
|
+
if (!previous || previous.namespace !== native.namespace ||
|
|
1302
|
+
previous.name !== native.name) candidates.set(wireName, undefined);
|
|
1303
|
+
};
|
|
1304
|
+
for (const [namespace, namespaceInfo] of Object.entries(inventory)) {
|
|
1305
|
+
if (!namespace || namespace === DEFAULT_FUNCTION_NAMESPACE ||
|
|
1306
|
+
namespaceInfo?.name !== namespace || !plainObject(namespaceInfo.functions)) continue;
|
|
1307
|
+
for (const [name, info] of Object.entries(namespaceInfo.functions)) {
|
|
1308
|
+
const mcp = info?.source?.kind === "mcp" &&
|
|
1309
|
+
typeof info.source.server_name === "string" && info.source.server_name &&
|
|
1310
|
+
namespace === `${MCP_NAMESPACE_PREFIX}${info.source.server_name}`;
|
|
1311
|
+
if (!name || info?.name !== name || info.direct !== true ||
|
|
1312
|
+
(!mcp && info.source?.kind !== "harness")) continue;
|
|
1313
|
+
remember(`${namespace}${NAMESPACE_DELIMITER}${name}`, { namespace, name });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const existingNamespaces = new Map();
|
|
1318
|
+
const existingIdentities = new Set();
|
|
1319
|
+
const repeatedNamespaces = new Set();
|
|
1320
|
+
const declarations = new Map();
|
|
1321
|
+
for (const tool of tools) {
|
|
1322
|
+
if (tool?.type === "namespace" && Array.isArray(tool.tools)) {
|
|
1323
|
+
if (existingNamespaces.has(tool.name)) repeatedNamespaces.add(tool.name);
|
|
1324
|
+
existingNamespaces.set(tool.name, tool);
|
|
1325
|
+
for (const child of tool.tools) {
|
|
1326
|
+
if (typeof child?.name !== "string" || !child.name) continue;
|
|
1327
|
+
remember(`${tool.name}${NAMESPACE_DELIMITER}${child.name}`,
|
|
1328
|
+
{ namespace: tool.name, name: child.name });
|
|
1329
|
+
existingIdentities.add(nativeToolKey(tool.name, child.name));
|
|
1330
|
+
}
|
|
1331
|
+
} else if (tool?.type === "function" || tool?.type === "custom") {
|
|
1332
|
+
const name = providerFunctionName(tool);
|
|
1333
|
+
// Metadata has no function/custom discriminator. Repeated wire names
|
|
1334
|
+
// cannot be safely assigned to one declaration, even across those types.
|
|
1335
|
+
declarations.set(name, declarations.has(name) ? undefined : tool);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const recovered = new Map();
|
|
1340
|
+
const groups = new Map();
|
|
1341
|
+
for (const [wireName, native] of candidates) {
|
|
1342
|
+
const tool = declarations.get(wireName);
|
|
1343
|
+
if (!native || !tool || ordinaryNames.has(wireName) ||
|
|
1344
|
+
repeatedNamespaces.has(native.namespace) ||
|
|
1345
|
+
existingIdentities.has(nativeToolKey(native.namespace, native.name))) continue;
|
|
1346
|
+
recovered.set(tool, native);
|
|
1347
|
+
if (!groups.has(native.namespace)) {
|
|
1348
|
+
const existing = existingNamespaces.get(native.namespace);
|
|
1349
|
+
groups.set(native.namespace, existing
|
|
1350
|
+
? { ...existing, tools: [] }
|
|
1351
|
+
: { type: "namespace", name: native.namespace, tools: [] });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (!recovered.size) return tools;
|
|
1355
|
+
|
|
1356
|
+
// One declaration per namespace lets app expansion prefer the complete client
|
|
1357
|
+
// schema without injecting the same deferred snapshot into multiple fragments.
|
|
1358
|
+
const restored = [];
|
|
1359
|
+
const emitted = new Set();
|
|
1360
|
+
for (const tool of tools) {
|
|
1361
|
+
const native = recovered.get(tool);
|
|
1362
|
+
const group = native ? groups.get(native.namespace)
|
|
1363
|
+
: tool?.type === "namespace" ? groups.get(tool.name) : undefined;
|
|
1364
|
+
if (!group) {
|
|
1365
|
+
restored.push(tool);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (!emitted.has(group)) {
|
|
1369
|
+
emitted.add(group);
|
|
1370
|
+
restored.push(group);
|
|
1371
|
+
}
|
|
1372
|
+
if (native) {
|
|
1373
|
+
const { function: definition, ...rest } = tool;
|
|
1374
|
+
group.tools.push({ ...rest, ...definition, name: native.name });
|
|
1375
|
+
} else {
|
|
1376
|
+
group.tools.push(...tool.tools);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return restored;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function plainObject(value) {
|
|
1383
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function validToolSearchHistoryArguments(value) {
|
|
1387
|
+
const argumentsObject = plainObject(value);
|
|
1388
|
+
if (!argumentsObject || typeof argumentsObject.query !== "string") return false;
|
|
1389
|
+
if (!argumentsObject.query.trim()) return false;
|
|
1390
|
+
const { limit } = argumentsObject;
|
|
1391
|
+
return limit === undefined || (Number.isInteger(limit) && limit > 0);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function discoveredProviderTools(toolSpecs, namespaces) {
|
|
1395
|
+
if (!Array.isArray(toolSpecs)) return [];
|
|
1396
|
+
const discovered = [];
|
|
1397
|
+
for (const tool of toolSpecs) {
|
|
1398
|
+
if (tool?.type === "namespace" && typeof tool.name === "string" && Array.isArray(tool.tools)) {
|
|
1399
|
+
for (const fn of tool.tools) {
|
|
1400
|
+
if (fn?.type !== "function" || !fn.name) continue;
|
|
1401
|
+
discovered.push({
|
|
1402
|
+
tool: flattenNamespaceChild(
|
|
1403
|
+
tool.name,
|
|
1404
|
+
fn,
|
|
1405
|
+
providerNameForNative(namespaces, tool.name, fn.name),
|
|
1406
|
+
),
|
|
1407
|
+
native: { namespace: tool.name, name: fn.name },
|
|
1408
|
+
nativeName: fn.name,
|
|
1409
|
+
identity: nativeToolKey(tool.name, fn.name),
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (tool?.type !== "function" || !providerFunctionName(tool)) continue;
|
|
1415
|
+
const nativeName = providerFunctionName(tool);
|
|
1416
|
+
const providerName = providerNameForNative(namespaces, undefined, nativeName);
|
|
1417
|
+
let providerTool = repairToolSchemaRoot(tool);
|
|
1418
|
+
if (providerName !== nativeName) {
|
|
1419
|
+
providerTool = withProviderFunctionName(providerTool, providerName);
|
|
1420
|
+
}
|
|
1421
|
+
discovered.push({
|
|
1422
|
+
tool: providerTool,
|
|
1423
|
+
nativeName,
|
|
1424
|
+
identity: nativeToolKey(undefined, nativeName),
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
return discovered;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function addDiscoveredNamespace(namespaces, native) {
|
|
1431
|
+
if (!native) return;
|
|
1432
|
+
let names = namespaces.get(native.namespace);
|
|
1433
|
+
if (!names) {
|
|
1434
|
+
names = new Set();
|
|
1435
|
+
namespaces.set(native.namespace, names);
|
|
1436
|
+
}
|
|
1437
|
+
names.add(native.name);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
export class ToolSearchHistoryCapacityError extends Error {
|
|
1441
|
+
constructor({ available, required }) {
|
|
1442
|
+
super(
|
|
1443
|
+
`Stored tool_search history references ${required} discovered tools, but only ` +
|
|
1444
|
+
`${available} provider tool slots remain.`,
|
|
1445
|
+
);
|
|
1446
|
+
this.name = "ToolSearchHistoryCapacityError";
|
|
1447
|
+
this.available = available;
|
|
1448
|
+
this.required = required;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// A native tool_search output changes what the model may call on the next
|
|
1453
|
+
// turn. The Responses API understands that special history item directly;
|
|
1454
|
+
// LiteLLM's chat-completions bridge does not. Translate matched call/output
|
|
1455
|
+
// pairs into ordinary function history and add the returned definitions to
|
|
1456
|
+
// this request's provider-facing tool list. A model switch may leave no live
|
|
1457
|
+
// search relay; in that explicitly enabled mode, preserve the definitions but
|
|
1458
|
+
// drop the now-unusable native control pair. Live top-level schemas win on a
|
|
1459
|
+
// name collision. Native items that do not form one unique, ordered,
|
|
1460
|
+
// well-formed pair are dropped: a chat-completions provider cannot consume
|
|
1461
|
+
// them, and forwarding one would make the transcript promise unavailable
|
|
1462
|
+
// tools.
|
|
1463
|
+
export function flattenToolSearchHistory(
|
|
1464
|
+
input,
|
|
1465
|
+
tools,
|
|
1466
|
+
namespaces,
|
|
1467
|
+
{ maxTools = Infinity, recoverWithoutRelay = false, toolChoice } = {},
|
|
1468
|
+
) {
|
|
1469
|
+
const relay = TOOL_SEARCH_RELAYS.get(namespaces);
|
|
1470
|
+
if (!Array.isArray(input)) {
|
|
1471
|
+
return { input, tools, flattened: false };
|
|
1472
|
+
}
|
|
1473
|
+
if (!Array.isArray(tools)) {
|
|
1474
|
+
const routedInput = input.filter(
|
|
1475
|
+
(item) => item?.type !== "tool_search_call" && item?.type !== "tool_search_output",
|
|
1476
|
+
);
|
|
1477
|
+
return {
|
|
1478
|
+
input: routedInput.length === input.length ? input : routedInput,
|
|
1479
|
+
tools,
|
|
1480
|
+
flattened: routedInput.length !== input.length,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const callsById = new Map();
|
|
1485
|
+
const invalidIds = new Set();
|
|
1486
|
+
let nativeItems = 0;
|
|
1487
|
+
// Pair in one forward walk. Outputs may follow several parallel calls, but
|
|
1488
|
+
// they may never reach backwards past an orphan, duplicate, or malformed
|
|
1489
|
+
// item with the same id. Records are materialized only after the walk, so a
|
|
1490
|
+
// duplicate discovered late invalidates the entire id before any tool can be
|
|
1491
|
+
// added to the provider request.
|
|
1492
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1493
|
+
const item = input[index];
|
|
1494
|
+
const isCall = item?.type === "tool_search_call";
|
|
1495
|
+
const isOutput = item?.type === "tool_search_output";
|
|
1496
|
+
if (!isCall && !isOutput) continue;
|
|
1497
|
+
nativeItems += 1;
|
|
1498
|
+
|
|
1499
|
+
const id = typeof item.call_id === "string" && item.call_id ? item.call_id : undefined;
|
|
1500
|
+
if (!id) continue;
|
|
1501
|
+
if (invalidIds.has(id)) continue;
|
|
1502
|
+
|
|
1503
|
+
if (isCall) {
|
|
1504
|
+
const valid =
|
|
1505
|
+
item.execution === "client" && validToolSearchHistoryArguments(item.arguments);
|
|
1506
|
+
if (!valid || callsById.has(id)) {
|
|
1507
|
+
callsById.delete(id);
|
|
1508
|
+
invalidIds.add(id);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
callsById.set(id, { call: item, callIndex: index });
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const call = callsById.get(id);
|
|
1516
|
+
const valid =
|
|
1517
|
+
item.execution === "client" &&
|
|
1518
|
+
item.status === "completed" &&
|
|
1519
|
+
Array.isArray(item.tools);
|
|
1520
|
+
if (!valid || !call || call.output) {
|
|
1521
|
+
callsById.delete(id);
|
|
1522
|
+
invalidIds.add(id);
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
call.output = item;
|
|
1526
|
+
call.outputIndex = index;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (nativeItems === 0) return { input, tools, flattened: false };
|
|
1530
|
+
|
|
1531
|
+
const callsByIndex = new Map();
|
|
1532
|
+
const outputsByIndex = new Map();
|
|
1533
|
+
if (relay || recoverWithoutRelay) {
|
|
1534
|
+
for (const [id, record] of callsById) {
|
|
1535
|
+
if (!record.output || invalidIds.has(id)) continue;
|
|
1536
|
+
callsByIndex.set(record.callIndex, record);
|
|
1537
|
+
outputsByIndex.set(record.outputIndex, record);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const toolCapacity = Number.isInteger(maxTools) && maxTools >= 0 ? maxTools : Infinity;
|
|
1542
|
+
const remainingToolCapacity = Math.max(0, toolCapacity - tools.length);
|
|
1543
|
+
const visibleNames = providerVisibleToolNames(tools);
|
|
1544
|
+
const initialNameAliases = new Map(
|
|
1545
|
+
NAME_ALIASES.get(namespaces)?.nativeToProvider || [],
|
|
1546
|
+
);
|
|
1547
|
+
const definitionOwnersByName = new Map();
|
|
1548
|
+
const discoveries = [];
|
|
1549
|
+
const discoveriesByOutputIndex = new Map();
|
|
1550
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1551
|
+
const item = input[index];
|
|
1552
|
+
if (!outputsByIndex.has(index)) continue;
|
|
1553
|
+
const records = [];
|
|
1554
|
+
for (const candidate of discoveredProviderTools(item.tools, namespaces)) {
|
|
1555
|
+
const name = providerFunctionName(candidate.tool);
|
|
1556
|
+
if (!name) continue;
|
|
1557
|
+
const priorOwner = definitionOwnersByName.get(name);
|
|
1558
|
+
const shadowedByClient = visibleNames.has(name) && !priorOwner;
|
|
1559
|
+
const record = {
|
|
1560
|
+
...candidate,
|
|
1561
|
+
name,
|
|
1562
|
+
outputIndex: index,
|
|
1563
|
+
shadowed: shadowedByClient || priorOwner !== undefined,
|
|
1564
|
+
definitionOwner: shadowedByClient ? undefined : priorOwner,
|
|
1565
|
+
};
|
|
1566
|
+
if (!visibleNames.has(name)) {
|
|
1567
|
+
visibleNames.add(name);
|
|
1568
|
+
definitionOwnersByName.set(name, record);
|
|
1569
|
+
record.definitionOwner = record;
|
|
1570
|
+
}
|
|
1571
|
+
discoveries.push(record);
|
|
1572
|
+
records.push(record);
|
|
1573
|
+
}
|
|
1574
|
+
discoveriesByOutputIndex.set(index, records);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// A bounded provider surface may omit only unused discoveries. Resolve each
|
|
1578
|
+
// stored call against the definitions that existed at that point in the
|
|
1579
|
+
// transcript, using the same precedence as flattenNamespacedHistory: an
|
|
1580
|
+
// explicit namespace is exact, an exact plain native identity wins over a
|
|
1581
|
+
// stale flattened namespace spelling, then provider aliases/raw namespace
|
|
1582
|
+
// spellings, then a unique bare namespace name. Later discoveries must not
|
|
1583
|
+
// retroactively make an earlier bare call ambiguous. A forced tool choice is
|
|
1584
|
+
// evaluated after all stored discoveries and reserves its schema too.
|
|
1585
|
+
const CURRENT_DEFINITION = Symbol("current-tool-definition");
|
|
1586
|
+
const identityOwners = new Map();
|
|
1587
|
+
const plainNativeOwners = new Map();
|
|
1588
|
+
const providerOwners = new Map();
|
|
1589
|
+
const wireNamespaceOwners = new Map();
|
|
1590
|
+
const bareNamespaceOwners = new Map();
|
|
1591
|
+
const addOwner = (owners, name, owner) => {
|
|
1592
|
+
if (typeof name !== "string" || !name) return;
|
|
1593
|
+
if (!owners.has(name)) owners.set(name, new Set());
|
|
1594
|
+
owners.get(name).add(owner);
|
|
1595
|
+
};
|
|
1596
|
+
const uniqueOwner = (owners) => owners?.size === 1 ? [...owners][0] : undefined;
|
|
1597
|
+
const rememberIdentity = ({ identity, native, nativeName, name, owner }) => {
|
|
1598
|
+
if (!identityOwners.has(identity)) identityOwners.set(identity, owner);
|
|
1599
|
+
addOwner(providerOwners, name, identity);
|
|
1600
|
+
if (native) {
|
|
1601
|
+
addOwner(
|
|
1602
|
+
wireNamespaceOwners,
|
|
1603
|
+
`${native.namespace}${NAMESPACE_DELIMITER}${native.name}`,
|
|
1604
|
+
identity,
|
|
1605
|
+
);
|
|
1606
|
+
addOwner(bareNamespaceOwners, native.name, identity);
|
|
1607
|
+
} else {
|
|
1608
|
+
addOwner(plainNativeOwners, nativeName, identity);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
for (const [namespace, names] of namespaces) {
|
|
1613
|
+
for (const name of names) {
|
|
1614
|
+
rememberIdentity({
|
|
1615
|
+
identity: nativeToolKey(namespace, name),
|
|
1616
|
+
native: { namespace, name },
|
|
1617
|
+
name: providerNameForNative(namespaces, namespace, name),
|
|
1618
|
+
owner: CURRENT_DEFINITION,
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (initialNameAliases.size) {
|
|
1623
|
+
for (const [identity, providerName] of initialNameAliases) {
|
|
1624
|
+
let decoded;
|
|
1625
|
+
try {
|
|
1626
|
+
decoded = JSON.parse(identity);
|
|
1627
|
+
} catch {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
if (!Array.isArray(decoded) || decoded.length !== 2 || decoded[0] !== null) continue;
|
|
1631
|
+
rememberIdentity({
|
|
1632
|
+
identity,
|
|
1633
|
+
nativeName: decoded[1],
|
|
1634
|
+
name: providerName,
|
|
1635
|
+
owner: CURRENT_DEFINITION,
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
for (const name of PLAIN_TOOL_NAMES.get(namespaces) || []) {
|
|
1640
|
+
rememberIdentity({
|
|
1641
|
+
identity: nativeToolKey(undefined, name),
|
|
1642
|
+
nativeName: name,
|
|
1643
|
+
name,
|
|
1644
|
+
owner: CURRENT_DEFINITION,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
// Custom/tool-search relays are provider-visible but are never ordinary
|
|
1649
|
+
// discovered definitions. Reserve their spellings as current identities so
|
|
1650
|
+
// a matching model-visible name cannot be attributed to a discovery.
|
|
1651
|
+
for (const name of CUSTOM_TOOL_RELAYS.get(namespaces)?.keys() || []) {
|
|
1652
|
+
const identity = `special:custom:${name}`;
|
|
1653
|
+
identityOwners.set(identity, CURRENT_DEFINITION);
|
|
1654
|
+
addOwner(providerOwners, name, identity);
|
|
1655
|
+
}
|
|
1656
|
+
for (const name of FUNCTION_RELAYS.get(namespaces)?.keys() || []) {
|
|
1657
|
+
const identity = `special:function:${name}`;
|
|
1658
|
+
identityOwners.set(identity, CURRENT_DEFINITION);
|
|
1659
|
+
addOwner(providerOwners, name, identity);
|
|
1660
|
+
}
|
|
1661
|
+
const toolSearch = TOOL_SEARCH_RELAYS.get(namespaces);
|
|
1662
|
+
if (toolSearch) {
|
|
1663
|
+
const identity = "special:tool-search";
|
|
1664
|
+
identityOwners.set(identity, CURRENT_DEFINITION);
|
|
1665
|
+
addOwner(providerOwners, toolSearch.providerName, identity);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const referencedDefinitions = new Set();
|
|
1669
|
+
const referencedIdentities = new Set();
|
|
1670
|
+
const markReference = (reference) => {
|
|
1671
|
+
if (!reference || typeof reference !== "object" || Array.isArray(reference)) return;
|
|
1672
|
+
const nestedName = reference.function?.name;
|
|
1673
|
+
const name = typeof nestedName === "string" ? nestedName : reference.name;
|
|
1674
|
+
if (typeof name !== "string" || !name) return;
|
|
1675
|
+
const namespace =
|
|
1676
|
+
typeof reference.namespace === "string" && reference.namespace
|
|
1677
|
+
? reference.namespace
|
|
1678
|
+
: undefined;
|
|
1679
|
+
let identity;
|
|
1680
|
+
if (namespace) {
|
|
1681
|
+
const exact = nativeToolKey(namespace, name);
|
|
1682
|
+
if (identityOwners.has(exact)) identity = exact;
|
|
1683
|
+
} else if (!SPECIAL_FUNCTION_REFERENCES.has(reference)) {
|
|
1684
|
+
identity = uniqueOwner(plainNativeOwners.get(name));
|
|
1685
|
+
identity ??= uniqueOwner(providerOwners.get(name));
|
|
1686
|
+
identity ??= uniqueOwner(wireNamespaceOwners.get(name));
|
|
1687
|
+
identity ??= uniqueOwner(bareNamespaceOwners.get(name));
|
|
1688
|
+
}
|
|
1689
|
+
if (!identity) return;
|
|
1690
|
+
referencedIdentities.add(identity);
|
|
1691
|
+
const owner = identityOwners.get(identity);
|
|
1692
|
+
if (owner && owner !== CURRENT_DEFINITION) referencedDefinitions.add(owner);
|
|
1693
|
+
};
|
|
1694
|
+
const discoveriesByIndex = new Map();
|
|
1695
|
+
for (const discovery of discoveries) {
|
|
1696
|
+
if (!discoveriesByIndex.has(discovery.outputIndex)) {
|
|
1697
|
+
discoveriesByIndex.set(discovery.outputIndex, []);
|
|
1698
|
+
}
|
|
1699
|
+
discoveriesByIndex.get(discovery.outputIndex).push(discovery);
|
|
1700
|
+
}
|
|
1701
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1702
|
+
for (const discovery of discoveriesByIndex.get(index) || []) {
|
|
1703
|
+
const owner = discovery.definitionOwner || CURRENT_DEFINITION;
|
|
1704
|
+
rememberIdentity({ ...discovery, owner });
|
|
1705
|
+
}
|
|
1706
|
+
const item = input[index];
|
|
1707
|
+
if (item?.type === "function_call") markReference(item);
|
|
1708
|
+
}
|
|
1709
|
+
if (toolChoice?.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
1710
|
+
for (const choice of toolChoice.tools) {
|
|
1711
|
+
if (choice?.type === "function") markReference(choice);
|
|
1712
|
+
}
|
|
1713
|
+
} else if (toolChoice?.type === "function") {
|
|
1714
|
+
markReference(toolChoice);
|
|
1715
|
+
}
|
|
1716
|
+
const requiredDefinitions = [...referencedDefinitions];
|
|
1717
|
+
if (requiredDefinitions.length > remainingToolCapacity) {
|
|
1718
|
+
throw new ToolSearchHistoryCapacityError({
|
|
1719
|
+
available: remainingToolCapacity,
|
|
1720
|
+
required: requiredDefinitions.length,
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
const acceptedDiscoveries = new Set(requiredDefinitions);
|
|
1724
|
+
for (const discovery of discoveries) {
|
|
1725
|
+
if (acceptedDiscoveries.size >= remainingToolCapacity) break;
|
|
1726
|
+
if (discovery.shadowed) continue;
|
|
1727
|
+
acceptedDiscoveries.add(discovery);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
let routedTools = tools;
|
|
1731
|
+
const routedInput = [];
|
|
1732
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1733
|
+
const item = input[index];
|
|
1734
|
+
if (item?.type === "tool_search_call") {
|
|
1735
|
+
if (!callsByIndex.has(index)) continue;
|
|
1736
|
+
if (!relay) continue;
|
|
1737
|
+
const {
|
|
1738
|
+
type: _type,
|
|
1739
|
+
execution: _execution,
|
|
1740
|
+
status: _status,
|
|
1741
|
+
arguments: searchArguments,
|
|
1742
|
+
...rest
|
|
1743
|
+
} = item;
|
|
1744
|
+
const routedCall = {
|
|
1745
|
+
...rest,
|
|
1746
|
+
type: "function_call",
|
|
1747
|
+
name: relay.providerName,
|
|
1748
|
+
arguments: JSON.stringify(searchArguments),
|
|
1749
|
+
};
|
|
1750
|
+
SPECIAL_FUNCTION_REFERENCES.add(routedCall);
|
|
1751
|
+
routedInput.push(routedCall);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (item?.type !== "tool_search_output") {
|
|
1756
|
+
routedInput.push(item);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (!outputsByIndex.has(index)) continue;
|
|
1760
|
+
|
|
1761
|
+
const accepted = [];
|
|
1762
|
+
for (const discovery of discoveriesByOutputIndex.get(index) || []) {
|
|
1763
|
+
if (acceptedDiscoveries.has(discovery)) {
|
|
1764
|
+
accepted.push(discovery.tool);
|
|
1765
|
+
addDiscoveredNamespace(namespaces, discovery.native);
|
|
1766
|
+
if (!discovery.native) PLAIN_TOOL_NAMES.get(namespaces)?.add(discovery.name);
|
|
1767
|
+
} else if (
|
|
1768
|
+
recoverWithoutRelay &&
|
|
1769
|
+
discovery.shadowed &&
|
|
1770
|
+
referencedIdentities.has(discovery.identity)
|
|
1771
|
+
) {
|
|
1772
|
+
// A current client definition owns the provider-visible name and its
|
|
1773
|
+
// schema must win. The stored native identity is still needed to
|
|
1774
|
+
// flatten the later historical call and restore any repeated call.
|
|
1775
|
+
addDiscoveredNamespace(namespaces, discovery.native);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
// Keep the live-name set across outputs. The first valid discovery wins;
|
|
1779
|
+
// later outputs omit a duplicate from both their result and the request's
|
|
1780
|
+
// tool list instead of advertising a schema that cannot take precedence.
|
|
1781
|
+
if (accepted.length) {
|
|
1782
|
+
if (routedTools === tools) routedTools = [...tools];
|
|
1783
|
+
routedTools.push(...accepted);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// The current provider cannot execute a fresh native tool_search call.
|
|
1787
|
+
// Preserve the discovered definitions above, but remove the now-unusable
|
|
1788
|
+
// call/output control pair from the chat-completions transcript.
|
|
1789
|
+
if (!relay) continue;
|
|
1790
|
+
|
|
1791
|
+
const {
|
|
1792
|
+
type: _type,
|
|
1793
|
+
execution: _execution,
|
|
1794
|
+
status: _status,
|
|
1795
|
+
tools: _tools,
|
|
1796
|
+
...rest
|
|
1797
|
+
} = item;
|
|
1798
|
+
routedInput.push({
|
|
1799
|
+
...rest,
|
|
1800
|
+
type: "function_call_output",
|
|
1801
|
+
output: JSON.stringify({ tools: accepted }),
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
return {
|
|
1806
|
+
input: routedInput,
|
|
1807
|
+
tools: routedTools,
|
|
1808
|
+
flattened: true,
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Flattening only the tool list leaves the model reading two names for one
|
|
1813
|
+
// tool: `collaboration__spawn_agent` in its tools, but a bare `spawn_agent`
|
|
1814
|
+
// in its own call history, because LiteLLM's bridge drops the `namespace`
|
|
1815
|
+
// field when it converts stored function calls to Chat Completions tool calls.
|
|
1816
|
+
// The model imitates the history, emits the bare name, nothing rewrites it,
|
|
1817
|
+
// and Codex answers `unsupported call` -- permanently, since every failure
|
|
1818
|
+
// adds another bare example. Rename the history to match the flattened tools.
|
|
1819
|
+
export function flattenNamespacedHistory(input, namespaces) {
|
|
1820
|
+
const nameRelay = NAME_ALIASES.get(namespaces);
|
|
1821
|
+
if (!Array.isArray(input) || (namespaces.size === 0 && !nameRelay)) return input;
|
|
1822
|
+
const flattenedNames = new Set();
|
|
1823
|
+
const bareOwners = new Map();
|
|
1824
|
+
for (const [namespace, names] of namespaces) {
|
|
1825
|
+
for (const name of names) {
|
|
1826
|
+
flattenedNames.add(`${namespace}${NAMESPACE_DELIMITER}${name}`);
|
|
1827
|
+
if (!bareOwners.has(name)) bareOwners.set(name, new Set());
|
|
1828
|
+
bareOwners.get(name).add(namespace);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
const providerNames = new Set([
|
|
1832
|
+
...(PLAIN_TOOL_NAMES.get(namespaces) || []),
|
|
1833
|
+
...(nameRelay?.providerToNative.keys() || []),
|
|
1834
|
+
...(nameRelay?.plainProviderNames || []),
|
|
1835
|
+
...(CUSTOM_TOOL_RELAYS.get(namespaces)?.keys() || []),
|
|
1836
|
+
...(FUNCTION_RELAYS.get(namespaces)?.keys() || []),
|
|
1837
|
+
]);
|
|
1838
|
+
const toolSearch = TOOL_SEARCH_RELAYS.get(namespaces);
|
|
1839
|
+
if (toolSearch) providerNames.add(toolSearch.providerName);
|
|
1840
|
+
return input.map((item) => {
|
|
1841
|
+
if (item?.type !== "function_call") return item;
|
|
1842
|
+
const { name } = item;
|
|
1843
|
+
if (typeof name !== "string") return item;
|
|
1844
|
+
// The client stores namespaced calls as { name, namespace }.
|
|
1845
|
+
const namespace = item.namespace;
|
|
1846
|
+
if (typeof namespace === "string" && namespaces.get(namespace)?.has(name)) {
|
|
1847
|
+
const { namespace: _namespace, ...rest } = item;
|
|
1848
|
+
return { ...rest, name: providerNameForNative(namespaces, namespace, name) };
|
|
1849
|
+
}
|
|
1850
|
+
// A custom/tool-search call already bridged in this request owns its exact
|
|
1851
|
+
// provider spelling. A later-discovered ordinary function can have the same
|
|
1852
|
+
// native name but a different provider alias; object identity keeps the two
|
|
1853
|
+
// histories distinct after both have become ordinary function calls.
|
|
1854
|
+
if (SPECIAL_FUNCTION_REFERENCES.has(item)) return item;
|
|
1855
|
+
// A plain native function may have the exact spelling a namespace child
|
|
1856
|
+
// would normally flatten to (for example plain `a__b` beside namespace
|
|
1857
|
+
// `a` / child `b`). Both definitions receive collision aliases. Resolve
|
|
1858
|
+
// the exact plain identity before treating that spelling as a raw
|
|
1859
|
+
// namespace wire name, or stored plain history would cite neither alias.
|
|
1860
|
+
const plainProviderName =
|
|
1861
|
+
namespace === undefined
|
|
1862
|
+
? nameRelay?.nativeToProvider.get(nativeToolKey(undefined, name))
|
|
1863
|
+
: undefined;
|
|
1864
|
+
if (plainProviderName && plainProviderName !== name) {
|
|
1865
|
+
return { ...item, name: plainProviderName };
|
|
1866
|
+
}
|
|
1867
|
+
// Provider-visible plain and special-relay names take precedence only when
|
|
1868
|
+
// history carries no valid native namespace or exact aliased plain identity.
|
|
1869
|
+
// Otherwise a plain `read` tool could prevent `{ namespace: "mcp", name:
|
|
1870
|
+
// "read" }` from being rewritten to the namespaced definition actually sent
|
|
1871
|
+
// upstream.
|
|
1872
|
+
if (providerNames.has(name)) return item;
|
|
1873
|
+
if (flattenedNames.has(name)) {
|
|
1874
|
+
const providerName = providerNameForWire(namespaces, name);
|
|
1875
|
+
return providerName && providerName !== name ? { ...item, name: providerName } : item;
|
|
1876
|
+
}
|
|
1877
|
+
// Calls stored without a namespace field whose bare name belongs to
|
|
1878
|
+
// exactly one flattened namespace.
|
|
1879
|
+
if (namespace === undefined) {
|
|
1880
|
+
const owners = bareOwners.get(name);
|
|
1881
|
+
if (owners && owners.size === 1) {
|
|
1882
|
+
const [owner] = [...owners];
|
|
1883
|
+
const { namespace: _namespace, ...rest } = item;
|
|
1884
|
+
return { ...rest, name: providerNameForNative(namespaces, owner, name) };
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return item;
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function compactionToolIdentityInventory(input, tools) {
|
|
1892
|
+
const plainNames = new Set();
|
|
1893
|
+
const namespaceNames = new Map();
|
|
1894
|
+
const rememberNamespace = (namespace, name) => {
|
|
1895
|
+
if (typeof namespace !== "string" || !namespace || typeof name !== "string" || !name) {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (!namespaceNames.has(namespace)) namespaceNames.set(namespace, new Set());
|
|
1899
|
+
namespaceNames.get(namespace).add(name);
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
1903
|
+
if (tool?.type === "namespace" && typeof tool.name === "string") {
|
|
1904
|
+
for (const child of Array.isArray(tool.tools) ? tool.tools : []) {
|
|
1905
|
+
if (child?.type === "function") rememberNamespace(tool.name, child.name);
|
|
1906
|
+
}
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (tool?.type !== "function") continue;
|
|
1910
|
+
const name = providerFunctionName(tool);
|
|
1911
|
+
if (typeof name === "string" && name) plainNames.add(name);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
const calls = (Array.isArray(input) ? input : []).filter(
|
|
1915
|
+
(item) => item?.type === "function_call" && typeof item.name === "string" && item.name,
|
|
1916
|
+
);
|
|
1917
|
+
for (const item of calls) rememberNamespace(item.namespace, item.name);
|
|
1918
|
+
|
|
1919
|
+
const rawNamespaceOwners = new Map();
|
|
1920
|
+
const bareNamespaceOwners = new Map();
|
|
1921
|
+
for (const [namespace, names] of namespaceNames) {
|
|
1922
|
+
for (const name of names) {
|
|
1923
|
+
const wireName = `${namespace}${NAMESPACE_DELIMITER}${name}`;
|
|
1924
|
+
if (!rawNamespaceOwners.has(wireName)) rawNamespaceOwners.set(wireName, new Set());
|
|
1925
|
+
rawNamespaceOwners.get(wireName).add(namespace);
|
|
1926
|
+
if (!bareNamespaceOwners.has(name)) bareNamespaceOwners.set(name, new Set());
|
|
1927
|
+
bareNamespaceOwners.get(name).add(namespace);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
for (const item of calls) {
|
|
1931
|
+
if (item.namespace !== undefined) continue;
|
|
1932
|
+
const rawOwners = rawNamespaceOwners.get(item.name);
|
|
1933
|
+
const bareOwners = bareNamespaceOwners.get(item.name);
|
|
1934
|
+
if (
|
|
1935
|
+
!plainNames.has(item.name) &&
|
|
1936
|
+
((rawOwners && rawOwners.size === 1) || (bareOwners && bareOwners.size === 1))
|
|
1937
|
+
) {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
plainNames.add(item.name);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
return [
|
|
1944
|
+
...[...plainNames]
|
|
1945
|
+
.sort((left, right) => left.localeCompare(right))
|
|
1946
|
+
.map((name) => ({ type: "function", name })),
|
|
1947
|
+
...[...namespaceNames]
|
|
1948
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1949
|
+
.map(([name, names]) => ({
|
|
1950
|
+
type: "namespace",
|
|
1951
|
+
name,
|
|
1952
|
+
tools: [...names]
|
|
1953
|
+
.sort((left, right) => left.localeCompare(right))
|
|
1954
|
+
.map((childName) => ({ type: "function", name: childName })),
|
|
1955
|
+
})),
|
|
1956
|
+
];
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// Routed compaction sends no live tools, but it still replays the complete
|
|
1960
|
+
// transcript to the summarizer. Console Go rejects Codex-native tool item
|
|
1961
|
+
// discriminators on that replay just as it does on an ordinary turn. Build a
|
|
1962
|
+
// names-only request-local inventory from the payload and explicit history,
|
|
1963
|
+
// bridge custom calls, flatten namespace history with the same bounded naming
|
|
1964
|
+
// contract, and remove deferred-search metadata (schemas, not tool results)
|
|
1965
|
+
// that cannot be consumed without a live tool_search control.
|
|
1966
|
+
export function strictOpenCodeCompactionInput(input, tools, { maxNameLength = 64 } = {}) {
|
|
1967
|
+
if (!Array.isArray(input)) return input;
|
|
1968
|
+
const inventory = compactionToolIdentityInventory(input, tools);
|
|
1969
|
+
const flattened = flattenNamespaceTools(inventory, {
|
|
1970
|
+
bridgeToolSearch: false,
|
|
1971
|
+
maxNameLength,
|
|
1972
|
+
});
|
|
1973
|
+
const customNames = [
|
|
1974
|
+
...new Set(
|
|
1975
|
+
input
|
|
1976
|
+
.filter(
|
|
1977
|
+
(item) =>
|
|
1978
|
+
item?.type === "custom_tool_call" &&
|
|
1979
|
+
typeof item.name === "string" &&
|
|
1980
|
+
item.name,
|
|
1981
|
+
)
|
|
1982
|
+
.map((item) => item.name),
|
|
1983
|
+
),
|
|
1984
|
+
];
|
|
1985
|
+
const custom = bridgeCustomTools(
|
|
1986
|
+
[],
|
|
1987
|
+
input,
|
|
1988
|
+
flattened.namespaces,
|
|
1989
|
+
undefined,
|
|
1990
|
+
customNames,
|
|
1991
|
+
{ maxNameLength },
|
|
1992
|
+
);
|
|
1993
|
+
const withoutSearch = custom.input.filter(
|
|
1994
|
+
(item) =>
|
|
1995
|
+
item?.type !== "tool_search_call" &&
|
|
1996
|
+
item?.type !== "tool_search_output" &&
|
|
1997
|
+
item?.type !== "custom_tool_call" &&
|
|
1998
|
+
item?.type !== "custom_tool_call_output",
|
|
1999
|
+
);
|
|
2000
|
+
return flattenNamespacedHistory(withoutSearch, flattened.namespaces);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function flattenToolChoiceReference(reference, namespaces) {
|
|
2004
|
+
if (!reference || typeof reference !== "object" || Array.isArray(reference)) return reference;
|
|
2005
|
+
const toolSearch = TOOL_SEARCH_RELAYS.get(namespaces);
|
|
2006
|
+
if (reference.type === "tool_search" && toolSearch) {
|
|
2007
|
+
const { execution: _execution, ...rest } = reference;
|
|
2008
|
+
const routedReference = {
|
|
2009
|
+
...rest,
|
|
2010
|
+
type: "function",
|
|
2011
|
+
name: toolSearch.providerName,
|
|
2012
|
+
};
|
|
2013
|
+
SPECIAL_FUNCTION_REFERENCES.add(routedReference);
|
|
2014
|
+
return routedReference;
|
|
2015
|
+
}
|
|
2016
|
+
if (reference.type !== "function") return reference;
|
|
2017
|
+
|
|
2018
|
+
const nestedName = reference.function?.name;
|
|
2019
|
+
const name = typeof nestedName === "string" ? nestedName : reference.name;
|
|
2020
|
+
if (typeof name !== "string" || !name) return reference;
|
|
2021
|
+
const namespace =
|
|
2022
|
+
typeof reference.namespace === "string" && reference.namespace
|
|
2023
|
+
? reference.namespace
|
|
2024
|
+
: undefined;
|
|
2025
|
+
let providerName;
|
|
2026
|
+
if (namespace && namespaces.get(namespace)?.has(name)) {
|
|
2027
|
+
providerName = providerNameForNative(namespaces, namespace, name);
|
|
2028
|
+
} else if (!namespace) {
|
|
2029
|
+
if (SPECIAL_FUNCTION_REFERENCES.has(reference)) return reference;
|
|
2030
|
+
const exactPlainProviderName = NAME_ALIASES.get(namespaces)?.nativeToProvider.get(
|
|
2031
|
+
nativeToolKey(undefined, name),
|
|
2032
|
+
);
|
|
2033
|
+
if (exactPlainProviderName && exactPlainProviderName !== name) {
|
|
2034
|
+
providerName = exactPlainProviderName;
|
|
2035
|
+
}
|
|
2036
|
+
const alreadyProviderVisible =
|
|
2037
|
+
PLAIN_TOOL_NAMES.get(namespaces)?.has(name) ||
|
|
2038
|
+
NAME_ALIASES.get(namespaces)?.plainProviderNames.has(name) ||
|
|
2039
|
+
CUSTOM_TOOL_RELAYS.get(namespaces)?.has(name) ||
|
|
2040
|
+
TOOL_SEARCH_RELAYS.get(namespaces)?.providerName === name;
|
|
2041
|
+
if (!providerName && alreadyProviderVisible) return reference;
|
|
2042
|
+
providerName ||= exactPlainProviderName || providerNameForWire(namespaces, name);
|
|
2043
|
+
if (!providerName) {
|
|
2044
|
+
const owners = [...namespaces].filter(([, names]) => names.has(name));
|
|
2045
|
+
if (owners.length === 1) {
|
|
2046
|
+
providerName = providerNameForNative(namespaces, owners[0][0], name);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (!providerName || (providerName === name && namespace === undefined)) return reference;
|
|
2051
|
+
|
|
2052
|
+
const { namespace: _namespace, ...rest } = reference;
|
|
2053
|
+
if (typeof nestedName === "string") {
|
|
2054
|
+
return { ...rest, function: { ...reference.function, name: providerName } };
|
|
2055
|
+
}
|
|
2056
|
+
return { ...rest, name: providerName };
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// A bounded provider name is one contract across the whole request. Rewrite
|
|
2060
|
+
// forced references only after tool-search history has expanded the live tool
|
|
2061
|
+
// set, so a discovered definition and an allowed-tools choice cannot disagree.
|
|
2062
|
+
export function flattenToolChoice(toolChoice, namespaces) {
|
|
2063
|
+
if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) {
|
|
2064
|
+
return toolChoice;
|
|
2065
|
+
}
|
|
2066
|
+
if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) {
|
|
2067
|
+
return flattenToolChoiceReference(toolChoice, namespaces);
|
|
2068
|
+
}
|
|
2069
|
+
let changed = false;
|
|
2070
|
+
const tools = toolChoice.tools.map((tool) => {
|
|
2071
|
+
const rewritten = flattenToolChoiceReference(tool, namespaces);
|
|
2072
|
+
if (rewritten !== tool) changed = true;
|
|
2073
|
+
return rewritten;
|
|
2074
|
+
});
|
|
2075
|
+
return changed ? { ...toolChoice, tools } : toolChoice;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// Reverse lookups for restoring calls: flattened name -> native
|
|
2079
|
+
// { namespace, name }, and bare tool name -> namespaces that own it.
|
|
2080
|
+
export function buildNamespaceLookups(namespaces) {
|
|
2081
|
+
const flatToNative = new Map();
|
|
2082
|
+
const bareToNamespaces = new Map();
|
|
2083
|
+
const nameAliases = NAME_ALIASES.get(namespaces);
|
|
2084
|
+
const customTools = CUSTOM_TOOL_RELAYS.get(namespaces);
|
|
2085
|
+
const bridgedCustomIdentities = new Set(
|
|
2086
|
+
[...(customTools?.values() || [])]
|
|
2087
|
+
.filter((native) => typeof native === "object")
|
|
2088
|
+
.map((native) => nativeToolKey(native.namespace, native.name)),
|
|
2089
|
+
);
|
|
2090
|
+
// Dotted wire spellings (`namespace.tool`) some Responses-native models emit
|
|
2091
|
+
// instead of `__` (#611). Collect candidates first; only an unambiguous
|
|
2092
|
+
// inventory pair is registered — never invent identity by splitting a name
|
|
2093
|
+
// (#568).
|
|
2094
|
+
const dottedCandidates = new Map();
|
|
2095
|
+
const rememberDotted = (dottedName, native) => {
|
|
2096
|
+
if (!dottedCandidates.has(dottedName)) {
|
|
2097
|
+
dottedCandidates.set(dottedName, native);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
const previous = dottedCandidates.get(dottedName);
|
|
2101
|
+
if (
|
|
2102
|
+
!previous ||
|
|
2103
|
+
previous.namespace !== native.namespace ||
|
|
2104
|
+
previous.name !== native.name
|
|
2105
|
+
) {
|
|
2106
|
+
dottedCandidates.set(dottedName, undefined);
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
for (const [namespace, names] of namespaces) {
|
|
2110
|
+
for (const name of names) {
|
|
2111
|
+
// The custom relay owns this identity now, including any collision alias.
|
|
2112
|
+
// Its former flattened spelling may belong to an ordinary function.
|
|
2113
|
+
if (bridgedCustomIdentities.has(nativeToolKey(namespace, name))) continue;
|
|
2114
|
+
const providerName =
|
|
2115
|
+
nameAliases?.nativeToProvider.get(nativeToolKey(namespace, name)) ||
|
|
2116
|
+
`${namespace}${NAMESPACE_DELIMITER}${name}`;
|
|
2117
|
+
const native = { namespace, name };
|
|
2118
|
+
flatToNative.set(providerName, native);
|
|
2119
|
+
const dottedName = `${namespace}.${name}`;
|
|
2120
|
+
if (dottedName !== providerName) rememberDotted(dottedName, native);
|
|
2121
|
+
if (!bareToNamespaces.has(name)) bareToNamespaces.set(name, new Set());
|
|
2122
|
+
bareToNamespaces.get(name).add(namespace);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (nameAliases) {
|
|
2126
|
+
for (const [providerName, native] of nameAliases.providerToNative) {
|
|
2127
|
+
if (bridgedCustomIdentities.has(nativeToolKey(native.namespace, native.name))) continue;
|
|
2128
|
+
flatToNative.set(providerName, native);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const plainToolNames = new Set([
|
|
2132
|
+
...(PLAIN_TOOL_NAMES.get(namespaces) || []),
|
|
2133
|
+
...(nameAliases?.plainProviderNames || []),
|
|
2134
|
+
]);
|
|
2135
|
+
for (const [dottedName, native] of dottedCandidates) {
|
|
2136
|
+
if (!native || plainToolNames.has(dottedName)) continue;
|
|
2137
|
+
const existing = flatToNative.get(dottedName);
|
|
2138
|
+
if (
|
|
2139
|
+
existing &&
|
|
2140
|
+
(existing.namespace !== native.namespace || existing.name !== native.name)
|
|
2141
|
+
) {
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
flatToNative.set(dottedName, native);
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
flatToNative,
|
|
2148
|
+
bareToNamespaces,
|
|
2149
|
+
plainToolNames,
|
|
2150
|
+
identityAliases: Boolean(nameAliases),
|
|
2151
|
+
spawnAgentModels: SPAWN_AGENT_MODELS.get(namespaces),
|
|
2152
|
+
toolSearch: TOOL_SEARCH_RELAYS.get(namespaces),
|
|
2153
|
+
customTools,
|
|
2154
|
+
customCodecs: CUSTOM_TOOL_CODECS.get(namespaces),
|
|
2155
|
+
functionRelays: FUNCTION_RELAYS.get(namespaces),
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function sanitizeSpawnAgentModel(item, lookups) {
|
|
2160
|
+
if (item?.namespace !== "collaboration" || item.name !== "spawn_agent") return item;
|
|
2161
|
+
const allowed = lookups.spawnAgentModels;
|
|
2162
|
+
if (!(allowed instanceof Set) || allowed.size === 0 || typeof item.arguments !== "string") {
|
|
2163
|
+
return item;
|
|
2164
|
+
}
|
|
2165
|
+
if (!jsonArgumentsAreUnambiguous(item.arguments)) return item;
|
|
2166
|
+
let args;
|
|
2167
|
+
try {
|
|
2168
|
+
args = JSON.parse(item.arguments);
|
|
2169
|
+
} catch {
|
|
2170
|
+
return item;
|
|
2171
|
+
}
|
|
2172
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return item;
|
|
2173
|
+
if (typeof args.model !== "string" || allowed.has(args.model)) return item;
|
|
2174
|
+
const { model: _invalidModel, ...safeArgs } = args;
|
|
2175
|
+
return { ...item, arguments: JSON.stringify(safeArgs) };
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Restore one SSE event's function call to the client's native namespace
|
|
2179
|
+
// shape. A flattened `<namespace>__<tool>` name resolves exactly. A bare tool
|
|
2180
|
+
// name (some models emit the unqualified form) is restored only when it is
|
|
2181
|
+
// unambiguous across every flattened namespace; a collision stays untouched
|
|
2182
|
+
// rather than guessing which runtime owns it.
|
|
2183
|
+
function functionRelayIdentityMatches(item, relay) {
|
|
2184
|
+
if (!relay || item?.type !== "function_call" || item.name !== relay.nativeName) return false;
|
|
2185
|
+
if (typeof relay.nativeNamespace === "string" && relay.nativeNamespace) {
|
|
2186
|
+
return item.namespace === relay.nativeNamespace;
|
|
2187
|
+
}
|
|
2188
|
+
return item.namespace === undefined;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function restoreFunctionRelayCall(item, relay, argumentsText) {
|
|
2192
|
+
const {
|
|
2193
|
+
name: _name,
|
|
2194
|
+
namespace: _namespace,
|
|
2195
|
+
arguments: _arguments,
|
|
2196
|
+
encrypted_function_args: _encryptedFunctionArgs,
|
|
2197
|
+
...rest
|
|
2198
|
+
} = item;
|
|
2199
|
+
return {
|
|
2200
|
+
...rest,
|
|
2201
|
+
name: relay.nativeName,
|
|
2202
|
+
...(relay.nativeNamespace ? { namespace: relay.nativeNamespace } : {}),
|
|
2203
|
+
arguments: argumentsText,
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function rewriteFunctionCallArguments(item) {
|
|
2208
|
+
if (!item || typeof item !== "object") return item;
|
|
2209
|
+
if (!jsonArgumentsAreUnambiguous(item.arguments, { allowEmpty: true })) return item;
|
|
2210
|
+
const argumentsText = coerceFunctionCallArguments(item.arguments);
|
|
2211
|
+
if (argumentsText === item.arguments) return item;
|
|
2212
|
+
return { ...item, arguments: argumentsText };
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
function toolSearchArguments(value, allowPlaceholder) {
|
|
2216
|
+
if (plainObject(value)) return value;
|
|
2217
|
+
if (typeof value !== "string") return undefined;
|
|
2218
|
+
if (allowPlaceholder && value.trim() === "") return {};
|
|
2219
|
+
if (!jsonArgumentsAreUnambiguous(value)) return undefined;
|
|
2220
|
+
try {
|
|
2221
|
+
return plainObject(JSON.parse(value));
|
|
2222
|
+
} catch {
|
|
2223
|
+
return undefined;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function rewriteToolSearchFunctionCallItem(item, lookups, allowPlaceholder) {
|
|
2228
|
+
const relay = lookups.toolSearch;
|
|
2229
|
+
if (
|
|
2230
|
+
!relay ||
|
|
2231
|
+
item?.type !== "function_call" ||
|
|
2232
|
+
item.name !== relay.providerName ||
|
|
2233
|
+
item.namespace !== undefined ||
|
|
2234
|
+
typeof item.call_id !== "string" ||
|
|
2235
|
+
!item.call_id
|
|
2236
|
+
) {
|
|
2237
|
+
return undefined;
|
|
2238
|
+
}
|
|
2239
|
+
const argumentsObject = toolSearchArguments(item.arguments, allowPlaceholder);
|
|
2240
|
+
if (!argumentsObject) return undefined;
|
|
2241
|
+
const {
|
|
2242
|
+
type: _type,
|
|
2243
|
+
name: _name,
|
|
2244
|
+
namespace: _namespace,
|
|
2245
|
+
arguments: _arguments,
|
|
2246
|
+
encrypted_function_args: _encryptedFunctionArgs,
|
|
2247
|
+
...rest
|
|
2248
|
+
} = item;
|
|
2249
|
+
return {
|
|
2250
|
+
...rest,
|
|
2251
|
+
type: "tool_search_call",
|
|
2252
|
+
execution: "client",
|
|
2253
|
+
arguments: argumentsObject,
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
function customToolInput(
|
|
2258
|
+
value,
|
|
2259
|
+
allowPlaceholder = false,
|
|
2260
|
+
property = CUSTOM_TOOL_INPUT_PROPERTY,
|
|
2261
|
+
codec,
|
|
2262
|
+
) {
|
|
2263
|
+
if (allowPlaceholder && (value === undefined || value === "")) return "";
|
|
2264
|
+
const argumentsText = codec?.preserveRawArguments === true ? value : coerceFunctionCallArguments(value);
|
|
2265
|
+
if (typeof argumentsText !== "string") return undefined;
|
|
2266
|
+
if (codec) {
|
|
2267
|
+
try {
|
|
2268
|
+
return codec.decodeArguments(argumentsText);
|
|
2269
|
+
} catch {
|
|
2270
|
+
return undefined;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
if (!jsonArgumentsAreUnambiguous(argumentsText)) return undefined;
|
|
2274
|
+
try {
|
|
2275
|
+
const parsed = JSON.parse(argumentsText);
|
|
2276
|
+
return typeof parsed?.[property] === "string" ? parsed[property] : undefined;
|
|
2277
|
+
} catch {
|
|
2278
|
+
return undefined;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// LiteLLM decides a native custom call's input itself: it unwraps a string
|
|
2283
|
+
// `content` from a JSON object and otherwise keeps the provider's arguments
|
|
2284
|
+
// verbatim (`unwrap_custom_tool_arguments` in its custom_tools module). Its
|
|
2285
|
+
// completed item carries that decision, so the relay must derive the same
|
|
2286
|
+
// input rather than a stricter one. A present non-string `content` has no
|
|
2287
|
+
// faithful equivalent of Python's str() and stays unsupported.
|
|
2288
|
+
const LITELLM_MAX_CUSTOM_ARGUMENTS_LENGTH = 1_000_000;
|
|
2289
|
+
|
|
2290
|
+
function litellmCustomToolInput(argumentsText) {
|
|
2291
|
+
if (typeof argumentsText !== "string") return undefined;
|
|
2292
|
+
if (argumentsText === "") return "";
|
|
2293
|
+
if (argumentsText.length > LITELLM_MAX_CUSTOM_ARGUMENTS_LENGTH) return argumentsText;
|
|
2294
|
+
let parsed;
|
|
2295
|
+
try {
|
|
2296
|
+
parsed = JSON.parse(argumentsText);
|
|
2297
|
+
} catch {
|
|
2298
|
+
return argumentsText;
|
|
2299
|
+
}
|
|
2300
|
+
if (!plainObject(parsed) || !Object.hasOwn(parsed, LITELLM_CUSTOM_TOOL_INPUT_PROPERTY)) {
|
|
2301
|
+
return argumentsText;
|
|
2302
|
+
}
|
|
2303
|
+
const content = parsed[LITELLM_CUSTOM_TOOL_INPUT_PROPERTY];
|
|
2304
|
+
return typeof content === "string" ? content : undefined;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
function rewriteCustomToolFunctionCallItem(item, lookups, allowPlaceholder) {
|
|
2308
|
+
if (
|
|
2309
|
+
item?.type !== "function_call" ||
|
|
2310
|
+
item.namespace !== undefined ||
|
|
2311
|
+
!(lookups.customTools instanceof Map)
|
|
2312
|
+
) {
|
|
2313
|
+
return undefined;
|
|
2314
|
+
}
|
|
2315
|
+
const native = customToolIdentity(lookups.customTools.get(item.name));
|
|
2316
|
+
if (!native) return undefined;
|
|
2317
|
+
const input = customToolInput(item.arguments, allowPlaceholder, CUSTOM_TOOL_INPUT_PROPERTY, lookups.customCodecs?.get(item.name));
|
|
2318
|
+
if (input === undefined) return undefined;
|
|
2319
|
+
const {
|
|
2320
|
+
type: _type,
|
|
2321
|
+
name: _name,
|
|
2322
|
+
arguments: _arguments,
|
|
2323
|
+
encrypted_function_args: _encryptedFunctionArgs,
|
|
2324
|
+
...rest
|
|
2325
|
+
} = item;
|
|
2326
|
+
return {
|
|
2327
|
+
...rest,
|
|
2328
|
+
type: "custom_tool_call",
|
|
2329
|
+
...native,
|
|
2330
|
+
...(allowPlaceholder && input === "" ? {} : { input }),
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
function customToolIdentity(value) {
|
|
2335
|
+
return typeof value === "string" ? { name: value } : value;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
function customCallIdentityMatches(source, item, lookups) {
|
|
2339
|
+
if (typeof item.name !== "string" || !item.name) return false;
|
|
2340
|
+
if (source?.type === "function_call") {
|
|
2341
|
+
if (source.namespace !== undefined) return false;
|
|
2342
|
+
const native = customToolIdentity(lookups.customTools?.get(source.name));
|
|
2343
|
+
return Boolean(native && native.name === item.name && native.namespace === item.namespace);
|
|
2344
|
+
}
|
|
2345
|
+
// Native custom calls are not rewritten; keep their existing exact shape.
|
|
2346
|
+
return source?.type === "custom_tool_call" && source.name === item.name &&
|
|
2347
|
+
source.namespace === item.namespace &&
|
|
2348
|
+
(item.namespace === undefined || (typeof item.namespace === "string" && Boolean(item.namespace)));
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function rewriteNamespaceFunctionCallItem(
|
|
2352
|
+
item,
|
|
2353
|
+
lookups,
|
|
2354
|
+
sessionModel,
|
|
2355
|
+
{ allowIncompleteToolSearch = false } = {},
|
|
2356
|
+
) {
|
|
2357
|
+
if (!item || item.type !== "function_call") return undefined;
|
|
2358
|
+
if (!rawCodecItem(item, lookups) && !jsonArgumentsAreUnambiguous(item.arguments, { allowEmpty: true })) return undefined;
|
|
2359
|
+
const exactPlainProviderIdentity =
|
|
2360
|
+
lookups.identityAliases &&
|
|
2361
|
+
item.namespace === undefined &&
|
|
2362
|
+
lookups.plainToolNames?.has(item.name);
|
|
2363
|
+
const functionRelay = lookups.functionRelays instanceof Map
|
|
2364
|
+
? lookups.functionRelays.get(item.name)
|
|
2365
|
+
: undefined;
|
|
2366
|
+
if (functionRelay && item.namespace === undefined) {
|
|
2367
|
+
if (allowIncompleteToolSearch && (item.arguments === undefined || item.arguments === "")) {
|
|
2368
|
+
return restoreFunctionRelayCall(item, functionRelay, item.arguments ?? "");
|
|
2369
|
+
}
|
|
2370
|
+
if (
|
|
2371
|
+
typeof item.arguments !== "string" ||
|
|
2372
|
+
Buffer.byteLength(item.arguments, "utf8") > functionRelay.maxArgumentBytes
|
|
2373
|
+
) {
|
|
2374
|
+
return undefined;
|
|
2375
|
+
}
|
|
2376
|
+
const rewrittenArguments = functionRelay.rewriteArguments(item.arguments);
|
|
2377
|
+
if (typeof rewrittenArguments !== "string") return undefined;
|
|
2378
|
+
return restoreFunctionRelayCall(item, functionRelay, rewrittenArguments);
|
|
2379
|
+
}
|
|
2380
|
+
const customTool = rewriteCustomToolFunctionCallItem(
|
|
2381
|
+
item,
|
|
2382
|
+
lookups,
|
|
2383
|
+
allowIncompleteToolSearch,
|
|
2384
|
+
);
|
|
2385
|
+
if (customTool) return customTool;
|
|
2386
|
+
const toolSearch = rewriteToolSearchFunctionCallItem(
|
|
2387
|
+
item,
|
|
2388
|
+
lookups,
|
|
2389
|
+
allowIncompleteToolSearch,
|
|
2390
|
+
);
|
|
2391
|
+
if (toolSearch) return toolSearch;
|
|
2392
|
+
let rewritten = item;
|
|
2393
|
+
const resolved = lookups.flatToNative.get(item.name);
|
|
2394
|
+
if (resolved) {
|
|
2395
|
+
const { namespace: _providerNamespace, ...rest } = item;
|
|
2396
|
+
rewritten = resolved.namespace === undefined
|
|
2397
|
+
? { ...rest, name: resolved.name }
|
|
2398
|
+
: { ...rest, name: resolved.name, namespace: resolved.namespace };
|
|
2399
|
+
} else {
|
|
2400
|
+
const owners = lookups.bareToNamespaces.get(item.name);
|
|
2401
|
+
if (
|
|
2402
|
+
item.namespace === undefined &&
|
|
2403
|
+
!lookups.plainToolNames?.has(item.name) &&
|
|
2404
|
+
owners &&
|
|
2405
|
+
owners.size === 1
|
|
2406
|
+
) {
|
|
2407
|
+
const [namespace] = [...owners];
|
|
2408
|
+
rewritten = {
|
|
2409
|
+
...item,
|
|
2410
|
+
namespace,
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
rewritten = sanitizeSpawnAgentModel(rewritten, lookups);
|
|
2415
|
+
// A client may declare an ordinary function whose literal name is
|
|
2416
|
+
// `codex_app__create_thread`. Its request-local alias resolves back to that
|
|
2417
|
+
// exact plain identity, not the app namespace. Do not infer app semantics
|
|
2418
|
+
// from the restored spelling after the lookup has already proved otherwise.
|
|
2419
|
+
if (!exactPlainProviderIdentity) {
|
|
2420
|
+
rewritten = injectSessionModelForSpawnCalls(rewritten, sessionModel);
|
|
2421
|
+
}
|
|
2422
|
+
rewritten = rewriteFunctionCallArguments(rewritten);
|
|
2423
|
+
return rewritten === item ? undefined : rewritten;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
export function rewriteNamespaceFunctionCall(event, lookups, sessionModel) {
|
|
2427
|
+
const item = rewriteNamespaceFunctionCallItem(event?.item, lookups, sessionModel, {
|
|
2428
|
+
allowIncompleteToolSearch: event?.type === "response.output_item.added",
|
|
2429
|
+
});
|
|
2430
|
+
return item ? { ...event, item } : undefined;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
function rewriteOutputItems(output, lookups, sessionModel) {
|
|
2434
|
+
if (!Array.isArray(output)) return undefined;
|
|
2435
|
+
let changed = false;
|
|
2436
|
+
const rewritten = output.map((item) => {
|
|
2437
|
+
const next = rewriteNamespaceFunctionCallItem(item, lookups, sessionModel);
|
|
2438
|
+
if (!next) return item;
|
|
2439
|
+
changed = true;
|
|
2440
|
+
return next;
|
|
2441
|
+
});
|
|
2442
|
+
return changed ? rewritten : undefined;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// Only the exact declared client-hook codec defers argument syntax to the
|
|
2446
|
+
// native hook. Identity, outer JSON, lifecycle and byte bounds stay enforced.
|
|
2447
|
+
function rawCodecItem(item, lookups) {
|
|
2448
|
+
return item?.type === "function_call" && item.namespace === undefined &&
|
|
2449
|
+
lookups?.customCodecs?.get(item.name)?.preserveRawArguments === true;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function embeddedFunctionArgumentsAreUnambiguous(payload, lookups, rawArgumentsDone = false) {
|
|
2453
|
+
const safeItem = (item) =>
|
|
2454
|
+
item?.type !== "function_call" || rawCodecItem(item, lookups) ||
|
|
2455
|
+
jsonArgumentsAreUnambiguous(item.arguments, { allowEmpty: true });
|
|
2456
|
+
if (!safeItem(payload?.item)) return false;
|
|
2457
|
+
if (
|
|
2458
|
+
payload?.type === "response.function_call_arguments.done" &&
|
|
2459
|
+
!rawArgumentsDone &&
|
|
2460
|
+
!jsonArgumentsAreUnambiguous(payload.arguments, { allowEmpty: true })
|
|
2461
|
+
) {
|
|
2462
|
+
return false;
|
|
2463
|
+
}
|
|
2464
|
+
for (const output of [payload?.output, payload?.response?.output]) {
|
|
2465
|
+
if (!Array.isArray(output)) continue;
|
|
2466
|
+
if (!output.every(safeItem)) return false;
|
|
2467
|
+
}
|
|
2468
|
+
return true;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Non-streaming Responses return completed function calls in an `output`
|
|
2472
|
+
// array instead of SSE `item` events. Restore both shapes through the same
|
|
2473
|
+
// exact request-local lookup so stream mode cannot change dispatch semantics.
|
|
2474
|
+
// Returns a copy only when at least one call was restored.
|
|
2475
|
+
export function rewriteNamespaceResponsePayload(payload, lookups, sessionModel) {
|
|
2476
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
2477
|
+
let rewritten = rewriteNamespaceFunctionCall(payload, lookups, sessionModel) || payload;
|
|
2478
|
+
let changed = rewritten !== payload;
|
|
2479
|
+
|
|
2480
|
+
if (payload.type === "response.function_call_arguments.done") {
|
|
2481
|
+
const argumentsText = jsonArgumentsAreUnambiguous(rewritten.arguments, {
|
|
2482
|
+
allowEmpty: true,
|
|
2483
|
+
})
|
|
2484
|
+
? coerceFunctionCallArguments(rewritten.arguments)
|
|
2485
|
+
: rewritten.arguments;
|
|
2486
|
+
if (argumentsText !== rewritten.arguments) {
|
|
2487
|
+
rewritten = { ...rewritten, arguments: argumentsText };
|
|
2488
|
+
changed = true;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
const output = rewriteOutputItems(rewritten.output, lookups, sessionModel);
|
|
2493
|
+
if (output) {
|
|
2494
|
+
rewritten = { ...rewritten, output };
|
|
2495
|
+
changed = true;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
const responseOutput = rewriteOutputItems(rewritten.response?.output, lookups, sessionModel);
|
|
2499
|
+
if (responseOutput) {
|
|
2500
|
+
rewritten = {
|
|
2501
|
+
...rewritten,
|
|
2502
|
+
response: { ...rewritten.response, output: responseOutput },
|
|
2503
|
+
};
|
|
2504
|
+
changed = true;
|
|
2505
|
+
}
|
|
2506
|
+
return changed ? rewritten : undefined;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// Inject missing collaboration.interrupt_agent calls for children that already
|
|
2510
|
+
// finished (FINAL_ANSWER in the request input) when the model forgot to close
|
|
2511
|
+
// them. Codex 0.147 keeps those children Working until interrupt_agent runs or
|
|
2512
|
+
// the user opens the child. Sequence numbers continue after the last model
|
|
2513
|
+
// event so Codex accepts the spliced calls as part of the same response.
|
|
2514
|
+
function nextSequence(event, lastSequence) {
|
|
2515
|
+
const value = Number(event?.sequence_number);
|
|
2516
|
+
return Number.isFinite(value) ? value : lastSequence;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
function trackInterruptFromItem(item, interrupted) {
|
|
2520
|
+
if (!item) return;
|
|
2521
|
+
const target = interruptTargetFromCall(item);
|
|
2522
|
+
if (target) interrupted.add(target);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function appendInterruptCallsToOutput(output, pending, interrupted) {
|
|
2526
|
+
const remaining = filterAlreadyInterrupted(pending, interrupted);
|
|
2527
|
+
if (!remaining.length) return { output, injected: 0, remaining: [] };
|
|
2528
|
+
const base = Array.isArray(output) ? [...output] : [];
|
|
2529
|
+
for (const item of base) trackInterruptFromItem(item, interrupted);
|
|
2530
|
+
const still = filterAlreadyInterrupted(remaining, interrupted);
|
|
2531
|
+
for (const target of still) {
|
|
2532
|
+
const call = buildInterruptAgentCall(target);
|
|
2533
|
+
base.push(call);
|
|
2534
|
+
interrupted.add(target);
|
|
2535
|
+
}
|
|
2536
|
+
return { output: base, injected: still.length, remaining: still };
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
const CUSTOM_TOOL_OPENING_LIMIT = 1024;
|
|
2540
|
+
const LITELLM_CUSTOM_TOOL_INPUT_PROPERTY = "content";
|
|
2541
|
+
const CUSTOM_TOOL_OPENING_PATTERNS = Object.freeze({
|
|
2542
|
+
[CUSTOM_TOOL_INPUT_PROPERTY]: /^\s*\{\s*"input"\s*:\s*"/,
|
|
2543
|
+
[LITELLM_CUSTOM_TOOL_INPUT_PROPERTY]: /^\s*\{\s*"content"\s*:\s*"/,
|
|
2544
|
+
});
|
|
2545
|
+
const JSON_ESCAPES = Object.freeze({
|
|
2546
|
+
'"': '"',
|
|
2547
|
+
"\\": "\\",
|
|
2548
|
+
"/": "/",
|
|
2549
|
+
b: "\b",
|
|
2550
|
+
f: "\f",
|
|
2551
|
+
n: "\n",
|
|
2552
|
+
r: "\r",
|
|
2553
|
+
t: "\t",
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
// Decode one new function-argument fragment into the native custom-tool input.
|
|
2557
|
+
// The wrapper prefix is bounded and every encoded patch character is visited
|
|
2558
|
+
// once. Only an incomplete escape (at most six characters) is retained between
|
|
2559
|
+
// calls, avoiding the quadratic full-patch rescans that large streamed patches
|
|
2560
|
+
// would otherwise trigger.
|
|
2561
|
+
function customToolInputDelta(
|
|
2562
|
+
state,
|
|
2563
|
+
fragment,
|
|
2564
|
+
property = CUSTOM_TOOL_INPUT_PROPERTY,
|
|
2565
|
+
) {
|
|
2566
|
+
if (typeof fragment !== "string" || state.invalid || state.closed) return undefined;
|
|
2567
|
+
let encoded = fragment;
|
|
2568
|
+
if (!state.opened) {
|
|
2569
|
+
state.opening += encoded;
|
|
2570
|
+
const openingPattern = CUSTOM_TOOL_OPENING_PATTERNS[property];
|
|
2571
|
+
if (!openingPattern) {
|
|
2572
|
+
state.opening = "";
|
|
2573
|
+
state.invalid = true;
|
|
2574
|
+
return undefined;
|
|
2575
|
+
}
|
|
2576
|
+
const opening = state.opening.match(openingPattern);
|
|
2577
|
+
if (!opening) {
|
|
2578
|
+
if (state.opening.length > CUSTOM_TOOL_OPENING_LIMIT) {
|
|
2579
|
+
state.opening = "";
|
|
2580
|
+
state.invalid = true;
|
|
2581
|
+
}
|
|
2582
|
+
return undefined;
|
|
2583
|
+
}
|
|
2584
|
+
state.opened = true;
|
|
2585
|
+
encoded = state.opening.slice(opening[0].length);
|
|
2586
|
+
state.opening = "";
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
if (state.escape) {
|
|
2590
|
+
encoded = state.escape + encoded;
|
|
2591
|
+
state.escape = "";
|
|
2592
|
+
}
|
|
2593
|
+
const decoded = [];
|
|
2594
|
+
for (let index = 0; index < encoded.length; index += 1) {
|
|
2595
|
+
const character = encoded[index];
|
|
2596
|
+
if (character === '"') {
|
|
2597
|
+
state.closed = true;
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
if (character !== "\\") {
|
|
2601
|
+
if (character.charCodeAt(0) < 0x20) {
|
|
2602
|
+
state.invalid = true;
|
|
2603
|
+
break;
|
|
2604
|
+
}
|
|
2605
|
+
decoded.push(character);
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
const escape = encoded[index + 1];
|
|
2610
|
+
if (escape === undefined) {
|
|
2611
|
+
state.escape = "\\";
|
|
2612
|
+
break;
|
|
2613
|
+
}
|
|
2614
|
+
if (escape === "u") {
|
|
2615
|
+
const digits = encoded.slice(index + 2, index + 6);
|
|
2616
|
+
if (!/^[0-9a-fA-F]*$/.test(digits)) {
|
|
2617
|
+
state.invalid = true;
|
|
2618
|
+
break;
|
|
2619
|
+
}
|
|
2620
|
+
if (digits.length < 4) {
|
|
2621
|
+
state.escape = encoded.slice(index);
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
decoded.push(String.fromCharCode(Number.parseInt(digits, 16)));
|
|
2625
|
+
index += 5;
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
if (!(escape in JSON_ESCAPES)) {
|
|
2629
|
+
state.invalid = true;
|
|
2630
|
+
break;
|
|
2631
|
+
}
|
|
2632
|
+
decoded.push(JSON_ESCAPES[escape]);
|
|
2633
|
+
index += 1;
|
|
2634
|
+
}
|
|
2635
|
+
return decoded.length ? decoded.join("") : undefined;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
class NamespaceRelayCommittedStreamError extends Error {
|
|
2639
|
+
constructor(reason) {
|
|
2640
|
+
super(
|
|
2641
|
+
`The provider response became unsafe to relay after namespace output was committed (${reason}).`,
|
|
2642
|
+
);
|
|
2643
|
+
this.name = "NamespaceRelayCommittedStreamError";
|
|
2644
|
+
this.code = "ERR_NAMESPACE_RELAY_COMMITTED_STREAM";
|
|
2645
|
+
this.status = 502;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// Rewrites LiteLLM's flattened `<namespace>__<tool>` function calls back to
|
|
2650
|
+
// the namespace + name shape Codex dispatches through its app runtime.
|
|
2651
|
+
export class NamespaceToolCallTransform extends Transform {
|
|
2652
|
+
#eventStream;
|
|
2653
|
+
#pendingParts = [];
|
|
2654
|
+
#pendingBytes = 0;
|
|
2655
|
+
#pendingTailBytes = 0;
|
|
2656
|
+
#released = false;
|
|
2657
|
+
#headerlessDetector;
|
|
2658
|
+
#sseParts = [];
|
|
2659
|
+
#sseBytes = 0;
|
|
2660
|
+
#sseTailBytes = 0;
|
|
2661
|
+
#sseNextPartBytes = INITIAL_SSE_CAPTURE_PART_BYTES;
|
|
2662
|
+
#sseLineBytes = 0;
|
|
2663
|
+
#ssePendingCr = false;
|
|
2664
|
+
#ssePendingLineWasBlank = false;
|
|
2665
|
+
#sseAtStreamStart = true;
|
|
2666
|
+
#sseLineEnding = "\n";
|
|
2667
|
+
#sseLineEndingObserved = false;
|
|
2668
|
+
#maxJsonCaptureBytes;
|
|
2669
|
+
#maxSseFrameBytes;
|
|
2670
|
+
#maxCommittedSseFrameBytes;
|
|
2671
|
+
#maxTrackedOutputItems;
|
|
2672
|
+
#maxTrackedStateBytes;
|
|
2673
|
+
#rewriteDisabled = false;
|
|
2674
|
+
#semanticMutationCommitted = false;
|
|
2675
|
+
#requiresCodec = false;
|
|
2676
|
+
#lookups;
|
|
2677
|
+
#sessionModel;
|
|
2678
|
+
#pendingInterrupts;
|
|
2679
|
+
#injectOnly = false;
|
|
2680
|
+
#interruptedTargets = new Set();
|
|
2681
|
+
#lastSequence = 0;
|
|
2682
|
+
#injectQueue = [];
|
|
2683
|
+
#injectionsDone = false;
|
|
2684
|
+
#lastInjectedCalls = [];
|
|
2685
|
+
// Every observed output-item identity reserves both ids. Special relays keep
|
|
2686
|
+
// their source and native shapes here until terminal validation so a stream
|
|
2687
|
+
// cannot change owners or fall back to raw function-call events after its
|
|
2688
|
+
// opening was rewritten.
|
|
2689
|
+
#callsByItemId = new Map();
|
|
2690
|
+
#callsByCallId = new Map();
|
|
2691
|
+
#trackedCallCount = 0;
|
|
2692
|
+
#trackedStateBytes = 0;
|
|
2693
|
+
|
|
2694
|
+
constructor(namespaces, contentType = "", sessionModel, options = {}) {
|
|
2695
|
+
super();
|
|
2696
|
+
this.#lookups = buildNamespaceLookups(namespaces);
|
|
2697
|
+
this.#sessionModel = sessionModel;
|
|
2698
|
+
this.#pendingInterrupts = Array.isArray(options.pendingInterrupts)
|
|
2699
|
+
? [...options.pendingInterrupts]
|
|
2700
|
+
: [];
|
|
2701
|
+
// Native turns attach this transform only to close finished children. A
|
|
2702
|
+
// native stream is otherwise relayed byte-identical, so inject-only mode
|
|
2703
|
+
// must not run the namespace rewrites (they exist for routed providers)
|
|
2704
|
+
// or re-serialize model-authored events it did not change.
|
|
2705
|
+
this.#injectOnly = Boolean(options.injectOnly);
|
|
2706
|
+
this.#requiresCodec = !this.#injectOnly && this.#lookups.customCodecs?.size > 0;
|
|
2707
|
+
this.#maxJsonCaptureBytes =
|
|
2708
|
+
Number.isInteger(options.maxJsonCaptureBytes) && options.maxJsonCaptureBytes > 0
|
|
2709
|
+
? options.maxJsonCaptureBytes
|
|
2710
|
+
: MAX_JSON_CAPTURE_BYTES;
|
|
2711
|
+
const configuredSseFrameBytes =
|
|
2712
|
+
Number.isInteger(options.maxSseFrameBytes) && options.maxSseFrameBytes > 0
|
|
2713
|
+
? options.maxSseFrameBytes
|
|
2714
|
+
: undefined;
|
|
2715
|
+
this.#maxSseFrameBytes = configuredSseFrameBytes ?? MAX_SSE_FRAME_BYTES;
|
|
2716
|
+
this.#maxCommittedSseFrameBytes =
|
|
2717
|
+
Number.isInteger(options.maxCommittedSseFrameBytes) &&
|
|
2718
|
+
options.maxCommittedSseFrameBytes > 0
|
|
2719
|
+
? options.maxCommittedSseFrameBytes
|
|
2720
|
+
: configuredSseFrameBytes ??
|
|
2721
|
+
Math.min(this.#maxJsonCaptureBytes, MAX_COMMITTED_SSE_FRAME_BYTES);
|
|
2722
|
+
this.#maxTrackedOutputItems =
|
|
2723
|
+
Number.isInteger(options.maxTrackedOutputItems) && options.maxTrackedOutputItems > 0
|
|
2724
|
+
? options.maxTrackedOutputItems
|
|
2725
|
+
: MAX_TRACKED_OUTPUT_ITEMS;
|
|
2726
|
+
this.#maxTrackedStateBytes =
|
|
2727
|
+
Number.isInteger(options.maxTrackedStateBytes) && options.maxTrackedStateBytes > 0
|
|
2728
|
+
? options.maxTrackedStateBytes
|
|
2729
|
+
: MAX_TRACKED_STATE_BYTES;
|
|
2730
|
+
const declared = String(contentType).toLowerCase();
|
|
2731
|
+
this.#eventStream = declared.includes("text/event-stream");
|
|
2732
|
+
this.#headerlessDetector =
|
|
2733
|
+
!this.#eventStream && !declared.includes("json")
|
|
2734
|
+
? new HeaderlessSseDetector()
|
|
2735
|
+
: undefined;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
_transform(chunk, _encoding, callback) {
|
|
2739
|
+
let error;
|
|
2740
|
+
try {
|
|
2741
|
+
if (this.#headerlessDetector) {
|
|
2742
|
+
const detected = this.#headerlessDetector.write(chunk);
|
|
2743
|
+
if (detected.decision !== "pending") {
|
|
2744
|
+
this.#headerlessDetector = undefined;
|
|
2745
|
+
this.#eventStream = detected.decision === "event-stream";
|
|
2746
|
+
for (const buffered of detected.chunks) this.#transformChunk(buffered);
|
|
2747
|
+
}
|
|
2748
|
+
} else {
|
|
2749
|
+
this.#transformChunk(chunk);
|
|
2750
|
+
}
|
|
2751
|
+
} catch (caught) {
|
|
2752
|
+
error = caught;
|
|
2753
|
+
}
|
|
2754
|
+
callback(error);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
#transformChunk(chunk) {
|
|
2758
|
+
const bytes = Buffer.from(chunk);
|
|
2759
|
+
if (!this.#eventStream) {
|
|
2760
|
+
if (this.#released) {
|
|
2761
|
+
this.push(bytes);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
this.#captureJsonBytes(bytes);
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
if (this.#rewriteDisabled) {
|
|
2768
|
+
this.push(bytes);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
this.#consumeSseChunk(bytes);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
_flush(callback) {
|
|
2775
|
+
let error;
|
|
2776
|
+
try {
|
|
2777
|
+
this.#flushTransform();
|
|
2778
|
+
} catch (caught) {
|
|
2779
|
+
error = caught;
|
|
2780
|
+
}
|
|
2781
|
+
callback(error);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
#flushTransform() {
|
|
2785
|
+
if (this.#headerlessDetector) {
|
|
2786
|
+
const detected = this.#headerlessDetector.end();
|
|
2787
|
+
this.#headerlessDetector = undefined;
|
|
2788
|
+
this.#eventStream = detected.decision === "event-stream";
|
|
2789
|
+
for (const buffered of detected.chunks) this.#transformChunk(buffered);
|
|
2790
|
+
}
|
|
2791
|
+
if (!this.#eventStream) {
|
|
2792
|
+
const body = this.#takeJsonCapture();
|
|
2793
|
+
if (this.#released || !body.length) {
|
|
2794
|
+
if (body.length) this.push(body);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
if (!isUtf8(body)) {
|
|
2798
|
+
this.#rejectCodecPassthrough("invalid UTF-8 JSON response");
|
|
2799
|
+
this.push(body);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const text = body.toString("utf8");
|
|
2803
|
+
if (!jsonIsUnambiguousForRewrite(text)) {
|
|
2804
|
+
this.#rejectCodecPassthrough("ambiguous or invalid JSON response");
|
|
2805
|
+
this.push(body);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
let original;
|
|
2809
|
+
try {
|
|
2810
|
+
original = JSON.parse(text);
|
|
2811
|
+
} catch {
|
|
2812
|
+
this.#rejectCodecPassthrough("invalid JSON response");
|
|
2813
|
+
this.push(body);
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (!embeddedFunctionArgumentsAreUnambiguous(original, this.#lookups)) {
|
|
2817
|
+
this.#rejectCodecPassthrough("ambiguous function arguments");
|
|
2818
|
+
this.push(body);
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
let payload = original;
|
|
2822
|
+
if (!this.#injectOnly) {
|
|
2823
|
+
const rewritten = rewriteNamespaceResponsePayload(
|
|
2824
|
+
payload,
|
|
2825
|
+
this.#lookups,
|
|
2826
|
+
this.#sessionModel,
|
|
2827
|
+
);
|
|
2828
|
+
if (rewritten) payload = rewritten;
|
|
2829
|
+
}
|
|
2830
|
+
if (this.#requiresCodec) {
|
|
2831
|
+
const reason = this.#validateOutputItems(original, payload, { allowAtomic: true });
|
|
2832
|
+
if (reason) this.#rejectCodecPassthrough(reason);
|
|
2833
|
+
if (original?.item) {
|
|
2834
|
+
const itemReason = this.#registerAtomicOutputItem(original.item, payload.item);
|
|
2835
|
+
if (itemReason) this.#rejectCodecPassthrough(itemReason);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
payload = this.#injectJsonInterrupts(payload);
|
|
2839
|
+
// Parsing is only permission to inspect. A response the transform did
|
|
2840
|
+
// not semantically change retains its exact original representation.
|
|
2841
|
+
if (payload !== original) this.#commitSemanticMutation();
|
|
2842
|
+
this.push(payload === original ? body : Buffer.from(JSON.stringify(payload), "utf8"));
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (this.#ssePendingCr && !this.#rewriteDisabled) {
|
|
2846
|
+
const blankLine = this.#ssePendingLineWasBlank;
|
|
2847
|
+
this.#ssePendingCr = false;
|
|
2848
|
+
this.#ssePendingLineWasBlank = false;
|
|
2849
|
+
this.#completeSseLine(blankLine);
|
|
2850
|
+
}
|
|
2851
|
+
let tailSeparator;
|
|
2852
|
+
if (!this.#rewriteDisabled && this.#sseBytes) {
|
|
2853
|
+
const tail = this.#takeSseFrame();
|
|
2854
|
+
this.#emitSseFrame(tail);
|
|
2855
|
+
tailSeparator = this.#separatorAfterSseTail(tail);
|
|
2856
|
+
}
|
|
2857
|
+
if (!this.#rewriteDisabled && this.#hasOpenSpecialCalls()) {
|
|
2858
|
+
if (this.#semanticMutationCommitted || this.#requiresCodec) {
|
|
2859
|
+
throw new NamespaceRelayCommittedStreamError("unterminated special tool call");
|
|
2860
|
+
}
|
|
2861
|
+
this.#disableSseRewriting();
|
|
2862
|
+
}
|
|
2863
|
+
// Streams that omit response.completed / [DONE] still need the closes,
|
|
2864
|
+
// unless an ambiguous frame made observing prior calls unsafe.
|
|
2865
|
+
if (!this.#rewriteDisabled) {
|
|
2866
|
+
const blocks = this.#drainInterruptBlocks();
|
|
2867
|
+
if (blocks.length && tailSeparator?.length) this.push(tailSeparator);
|
|
2868
|
+
for (const piece of blocks) this.push(piece);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
#captureJsonBytes(bytes) {
|
|
2873
|
+
// Fixed-size parts keep both copies and metadata bounded even when an
|
|
2874
|
+
// upstream fragments a body into one-byte chunks. Each byte is copied once
|
|
2875
|
+
// while capturing and at most once more for the final JSON parse.
|
|
2876
|
+
let offset = 0;
|
|
2877
|
+
while (offset < bytes.length && !this.#released) {
|
|
2878
|
+
let tail = this.#pendingParts.at(-1);
|
|
2879
|
+
if (!tail || this.#pendingTailBytes === tail.length) {
|
|
2880
|
+
const remainingUntilRelease =
|
|
2881
|
+
this.#maxJsonCaptureBytes + 1 - this.#pendingBytes;
|
|
2882
|
+
tail = Buffer.allocUnsafe(
|
|
2883
|
+
Math.min(CAPTURE_PART_BYTES, remainingUntilRelease),
|
|
2884
|
+
);
|
|
2885
|
+
this.#pendingParts.push(tail);
|
|
2886
|
+
this.#pendingTailBytes = 0;
|
|
2887
|
+
}
|
|
2888
|
+
const copied = Math.min(tail.length - this.#pendingTailBytes, bytes.length - offset);
|
|
2889
|
+
bytes.copy(
|
|
2890
|
+
tail,
|
|
2891
|
+
this.#pendingTailBytes,
|
|
2892
|
+
offset,
|
|
2893
|
+
offset + copied,
|
|
2894
|
+
);
|
|
2895
|
+
this.#pendingTailBytes += copied;
|
|
2896
|
+
this.#pendingBytes += copied;
|
|
2897
|
+
offset += copied;
|
|
2898
|
+
if (this.#pendingBytes > this.#maxJsonCaptureBytes) {
|
|
2899
|
+
this.#rejectCodecPassthrough("JSON response byte limit");
|
|
2900
|
+
for (let index = 0; index < this.#pendingParts.length; index += 1) {
|
|
2901
|
+
const part = this.#pendingParts[index];
|
|
2902
|
+
this.push(
|
|
2903
|
+
index === this.#pendingParts.length - 1
|
|
2904
|
+
? part.subarray(0, this.#pendingTailBytes)
|
|
2905
|
+
: part,
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
this.#pendingParts = [];
|
|
2909
|
+
this.#pendingBytes = 0;
|
|
2910
|
+
this.#pendingTailBytes = 0;
|
|
2911
|
+
this.#released = true;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
if (offset < bytes.length) this.push(bytes.subarray(offset));
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
#takeJsonCapture() {
|
|
2918
|
+
if (!this.#pendingParts.length) return Buffer.alloc(0);
|
|
2919
|
+
const lastIndex = this.#pendingParts.length - 1;
|
|
2920
|
+
const parts = this.#pendingParts.map((part, index) =>
|
|
2921
|
+
index === lastIndex ? part.subarray(0, this.#pendingTailBytes) : part,
|
|
2922
|
+
);
|
|
2923
|
+
const body =
|
|
2924
|
+
parts.length === 1 ? parts[0] : Buffer.concat(parts, this.#pendingBytes);
|
|
2925
|
+
this.#pendingParts = [];
|
|
2926
|
+
this.#pendingBytes = 0;
|
|
2927
|
+
this.#pendingTailBytes = 0;
|
|
2928
|
+
return body;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
#separatorAfterSseTail(frame) {
|
|
2932
|
+
if (!frame.length) return Buffer.alloc(0);
|
|
2933
|
+
const last = frame[frame.length - 1];
|
|
2934
|
+
const missingLineEndings = last === CARRIAGE_RETURN || last === LINE_FEED ? 1 : 2;
|
|
2935
|
+
return Buffer.from(this.#sseLineEnding.repeat(missingLineEndings), "utf8");
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
#consumeSseChunk(chunk) {
|
|
2939
|
+
let offset = 0;
|
|
2940
|
+
if (this.#ssePendingCr) {
|
|
2941
|
+
const followedByLf = chunk[0] === LINE_FEED;
|
|
2942
|
+
if (followedByLf) {
|
|
2943
|
+
if (!this.#appendSsePiece(chunk.subarray(0, 1))) {
|
|
2944
|
+
if (chunk.length > 1) this.push(chunk.subarray(1));
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
offset = 1;
|
|
2948
|
+
}
|
|
2949
|
+
const blankLine = this.#ssePendingLineWasBlank;
|
|
2950
|
+
this.#ssePendingCr = false;
|
|
2951
|
+
this.#ssePendingLineWasBlank = false;
|
|
2952
|
+
this.#completeSseLine(blankLine);
|
|
2953
|
+
if (this.#rewriteDisabled) {
|
|
2954
|
+
if (offset < chunk.length) this.push(chunk.subarray(offset));
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
while (offset < chunk.length) {
|
|
2960
|
+
let lineEnd = offset;
|
|
2961
|
+
while (
|
|
2962
|
+
lineEnd < chunk.length &&
|
|
2963
|
+
chunk[lineEnd] !== CARRIAGE_RETURN &&
|
|
2964
|
+
chunk[lineEnd] !== LINE_FEED
|
|
2965
|
+
) {
|
|
2966
|
+
lineEnd += 1;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
if (lineEnd === chunk.length) {
|
|
2970
|
+
const content = chunk.subarray(offset);
|
|
2971
|
+
this.#sseLineBytes += content.length;
|
|
2972
|
+
this.#appendSsePiece(content);
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
if (lineEnd > offset) {
|
|
2976
|
+
const content = chunk.subarray(offset, lineEnd);
|
|
2977
|
+
this.#sseLineBytes += content.length;
|
|
2978
|
+
if (!this.#appendSsePiece(content)) {
|
|
2979
|
+
this.push(chunk.subarray(lineEnd));
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
const blankLine = this.#sseLineBytes === 0;
|
|
2985
|
+
if (chunk[lineEnd] === CARRIAGE_RETURN) {
|
|
2986
|
+
if (!this.#appendSsePiece(chunk.subarray(lineEnd, lineEnd + 1))) {
|
|
2987
|
+
if (lineEnd + 1 < chunk.length) this.push(chunk.subarray(lineEnd + 1));
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
if (lineEnd + 1 === chunk.length) {
|
|
2991
|
+
this.#ssePendingCr = true;
|
|
2992
|
+
this.#ssePendingLineWasBlank = blankLine;
|
|
2993
|
+
this.#sseLineBytes = 0;
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
if (chunk[lineEnd + 1] === LINE_FEED) {
|
|
2997
|
+
if (!this.#appendSsePiece(chunk.subarray(lineEnd + 1, lineEnd + 2))) {
|
|
2998
|
+
if (lineEnd + 2 < chunk.length) this.push(chunk.subarray(lineEnd + 2));
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
offset = lineEnd + 2;
|
|
3002
|
+
} else {
|
|
3003
|
+
offset = lineEnd + 1;
|
|
3004
|
+
}
|
|
3005
|
+
} else {
|
|
3006
|
+
if (!this.#appendSsePiece(chunk.subarray(lineEnd, lineEnd + 1))) {
|
|
3007
|
+
if (lineEnd + 1 < chunk.length) this.push(chunk.subarray(lineEnd + 1));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
offset = lineEnd + 1;
|
|
3011
|
+
}
|
|
3012
|
+
this.#completeSseLine(blankLine);
|
|
3013
|
+
if (this.#rewriteDisabled) {
|
|
3014
|
+
if (offset < chunk.length) this.push(chunk.subarray(offset));
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
#appendSsePiece(piece) {
|
|
3021
|
+
if (!piece.length) return true;
|
|
3022
|
+
// Copy into fixed-size parts exactly as the non-streaming capture does.
|
|
3023
|
+
// Retaining one view per upstream chunk would let a one-byte-fragmented
|
|
3024
|
+
// frame allocate millions of Buffer objects before reaching its byte cap.
|
|
3025
|
+
let offset = 0;
|
|
3026
|
+
const frameByteLimit = this.#semanticMutationCommitted
|
|
3027
|
+
? this.#maxCommittedSseFrameBytes
|
|
3028
|
+
: this.#maxSseFrameBytes;
|
|
3029
|
+
while (offset < piece.length && this.#sseBytes <= frameByteLimit) {
|
|
3030
|
+
let tail = this.#sseParts.at(-1);
|
|
3031
|
+
if (!tail || this.#sseTailBytes === tail.length) {
|
|
3032
|
+
const remainingUntilRelease =
|
|
3033
|
+
frameByteLimit + 1 - this.#sseBytes;
|
|
3034
|
+
const remainingPieceBytes = piece.length - offset;
|
|
3035
|
+
const partBytes = Math.min(
|
|
3036
|
+
CAPTURE_PART_BYTES,
|
|
3037
|
+
Math.max(this.#sseNextPartBytes, Math.min(CAPTURE_PART_BYTES, remainingPieceBytes)),
|
|
3038
|
+
remainingUntilRelease,
|
|
3039
|
+
);
|
|
3040
|
+
tail = Buffer.allocUnsafe(partBytes);
|
|
3041
|
+
this.#sseParts.push(tail);
|
|
3042
|
+
this.#sseTailBytes = 0;
|
|
3043
|
+
this.#sseNextPartBytes = Math.min(CAPTURE_PART_BYTES, partBytes * 2);
|
|
3044
|
+
}
|
|
3045
|
+
const copied = Math.min(tail.length - this.#sseTailBytes, piece.length - offset);
|
|
3046
|
+
piece.copy(tail, this.#sseTailBytes, offset, offset + copied);
|
|
3047
|
+
this.#sseTailBytes += copied;
|
|
3048
|
+
this.#sseBytes += copied;
|
|
3049
|
+
offset += copied;
|
|
3050
|
+
}
|
|
3051
|
+
if (this.#sseBytes <= frameByteLimit) return true;
|
|
3052
|
+
const buffered = this.#takeSseFrame();
|
|
3053
|
+
if (this.#semanticMutationCommitted || this.#requiresCodec) {
|
|
3054
|
+
throw new NamespaceRelayCommittedStreamError("SSE frame byte limit");
|
|
3055
|
+
}
|
|
3056
|
+
this.#disableSseRewriting();
|
|
3057
|
+
this.push(buffered);
|
|
3058
|
+
if (offset < piece.length) this.push(piece.subarray(offset));
|
|
3059
|
+
return false;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
#completeSseLine(blankLine) {
|
|
3063
|
+
this.#sseLineBytes = 0;
|
|
3064
|
+
if (blankLine) this.#emitSseFrame(this.#takeSseFrame());
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
#takeSseFrame() {
|
|
3068
|
+
if (!this.#sseParts.length) return Buffer.alloc(0);
|
|
3069
|
+
const lastIndex = this.#sseParts.length - 1;
|
|
3070
|
+
const parts = this.#sseParts.map((part, index) =>
|
|
3071
|
+
index === lastIndex ? part.subarray(0, this.#sseTailBytes) : part,
|
|
3072
|
+
);
|
|
3073
|
+
const frame = parts.length === 1 ? parts[0] : Buffer.concat(parts, this.#sseBytes);
|
|
3074
|
+
this.#sseParts = [];
|
|
3075
|
+
this.#sseBytes = 0;
|
|
3076
|
+
this.#sseTailBytes = 0;
|
|
3077
|
+
this.#sseNextPartBytes = INITIAL_SSE_CAPTURE_PART_BYTES;
|
|
3078
|
+
this.#sseLineBytes = 0;
|
|
3079
|
+
this.#ssePendingCr = false;
|
|
3080
|
+
this.#ssePendingLineWasBlank = false;
|
|
3081
|
+
return frame;
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
#sseFields(frame, atStreamStart) {
|
|
3085
|
+
let eventLine;
|
|
3086
|
+
let dataLine;
|
|
3087
|
+
let lineEndingLine;
|
|
3088
|
+
let repeated = false;
|
|
3089
|
+
let start = 0;
|
|
3090
|
+
let firstLine = true;
|
|
3091
|
+
while (start < frame.length) {
|
|
3092
|
+
let contentEnd = start;
|
|
3093
|
+
while (
|
|
3094
|
+
contentEnd < frame.length &&
|
|
3095
|
+
frame[contentEnd] !== CARRIAGE_RETURN &&
|
|
3096
|
+
frame[contentEnd] !== LINE_FEED
|
|
3097
|
+
) {
|
|
3098
|
+
contentEnd += 1;
|
|
3099
|
+
}
|
|
3100
|
+
const end = contentEnd === frame.length
|
|
3101
|
+
? frame.length
|
|
3102
|
+
: frame[contentEnd] === CARRIAGE_RETURN && frame[contentEnd + 1] === LINE_FEED
|
|
3103
|
+
? contentEnd + 2
|
|
3104
|
+
: contentEnd + 1;
|
|
3105
|
+
let parseStart = start;
|
|
3106
|
+
if (
|
|
3107
|
+
firstLine &&
|
|
3108
|
+
atStreamStart &&
|
|
3109
|
+
frame.subarray(start, start + UTF8_BOM.length).equals(UTF8_BOM)
|
|
3110
|
+
) {
|
|
3111
|
+
parseStart += UTF8_BOM.length;
|
|
3112
|
+
}
|
|
3113
|
+
firstLine = false;
|
|
3114
|
+
const line = { start, parseStart, contentEnd, end };
|
|
3115
|
+
if (!lineEndingLine && end > contentEnd) lineEndingLine = line;
|
|
3116
|
+
const lineLength = contentEnd - parseStart;
|
|
3117
|
+
const fieldLine = (name) =>
|
|
3118
|
+
lineLength >= name.length &&
|
|
3119
|
+
frame.subarray(parseStart, parseStart + name.length).equals(name) &&
|
|
3120
|
+
(lineLength === name.length || frame[parseStart + name.length] === 0x3a);
|
|
3121
|
+
if (fieldLine(SSE_EVENT_FIELD)) {
|
|
3122
|
+
if (eventLine) repeated = true;
|
|
3123
|
+
else eventLine = line;
|
|
3124
|
+
}
|
|
3125
|
+
if (fieldLine(SSE_DATA_FIELD)) {
|
|
3126
|
+
if (dataLine) repeated = true;
|
|
3127
|
+
else dataLine = line;
|
|
3128
|
+
}
|
|
3129
|
+
if (repeated || contentEnd === frame.length) break;
|
|
3130
|
+
start = end;
|
|
3131
|
+
}
|
|
3132
|
+
return { eventLine, dataLine, lineEndingLine, repeated };
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
#rememberSseLineEnding(frame, line) {
|
|
3136
|
+
if (this.#sseLineEndingObserved || !line || line.end === line.contentEnd) return;
|
|
3137
|
+
const terminator = frame.subarray(line.contentEnd, line.end);
|
|
3138
|
+
if (terminator.equals(Buffer.from("\r\n"))) this.#sseLineEnding = "\r\n";
|
|
3139
|
+
else if (terminator.equals(Buffer.from("\n"))) this.#sseLineEnding = "\n";
|
|
3140
|
+
else if (terminator.equals(Buffer.from("\r"))) this.#sseLineEnding = "\r";
|
|
3141
|
+
else return;
|
|
3142
|
+
this.#sseLineEndingObserved = true;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
#emitSseFrame(frame) {
|
|
3146
|
+
for (const piece of this.#rewriteSseFrame(frame)) this.push(piece);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
#commitSemanticMutation() {
|
|
3150
|
+
this.#semanticMutationCommitted = true;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
#unsafeSseFrame(frame, reason) {
|
|
3154
|
+
if (this.#semanticMutationCommitted || this.#requiresCodec) {
|
|
3155
|
+
throw new NamespaceRelayCommittedStreamError(reason);
|
|
3156
|
+
}
|
|
3157
|
+
this.#disableSseRewriting();
|
|
3158
|
+
return [frame];
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
#rejectCodecPassthrough(reason) {
|
|
3162
|
+
if (this.#requiresCodec) throw new NamespaceRelayCommittedStreamError(reason);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
#nativeCodecBypass(item) {
|
|
3166
|
+
if (!this.#requiresCodec || item?.type !== "custom_tool_call") return false;
|
|
3167
|
+
let codecBackedName = false;
|
|
3168
|
+
for (const [providerName, native] of this.#lookups.customTools) {
|
|
3169
|
+
if (typeof native === "object") {
|
|
3170
|
+
// A declared namespaced custom tool owns its identity and has no codec.
|
|
3171
|
+
if (native.namespace === item.namespace && native.name === item.name) return false;
|
|
3172
|
+
} else if (native === item.name && this.#lookups.customCodecs.has(providerName)) {
|
|
3173
|
+
codecBackedName = true;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
// A raw call carrying a codec-backed bare name, with or without a namespace
|
|
3177
|
+
// nobody declared, would reach the client without the codec's checks.
|
|
3178
|
+
return codecBackedName;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
#disableSseRewriting() {
|
|
3182
|
+
this.#rewriteDisabled = true;
|
|
3183
|
+
this.#injectionsDone = true;
|
|
3184
|
+
this.#lastInjectedCalls = [];
|
|
3185
|
+
this.#callsByItemId.clear();
|
|
3186
|
+
this.#callsByCallId.clear();
|
|
3187
|
+
this.#trackedCallCount = 0;
|
|
3188
|
+
this.#trackedStateBytes = 0;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
#specialCallKind(item) {
|
|
3192
|
+
if (item?.type === "custom_tool_call") return "custom";
|
|
3193
|
+
if (item?.type === "tool_search_call") return "tool_search";
|
|
3194
|
+
return undefined;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
#sourceSpecialCallKind(item) {
|
|
3198
|
+
const nativeKind = this.#specialCallKind(item);
|
|
3199
|
+
if (nativeKind) return nativeKind;
|
|
3200
|
+
if (item?.type !== "function_call") return undefined;
|
|
3201
|
+
if (this.#lookups.customTools instanceof Map && this.#lookups.customTools.has(item.name)) {
|
|
3202
|
+
return "custom";
|
|
3203
|
+
}
|
|
3204
|
+
if (this.#lookups.functionRelays instanceof Map && this.#lookups.functionRelays.has(item.name)) {
|
|
3205
|
+
return "function_codec";
|
|
3206
|
+
}
|
|
3207
|
+
if (item.name === this.#lookups.toolSearch?.providerName) return "tool_search";
|
|
3208
|
+
return undefined;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
#hasOpenSpecialCalls() {
|
|
3212
|
+
for (const state of this.#callsByItemId.values()) {
|
|
3213
|
+
if (state.kind && !state.closed) return true;
|
|
3214
|
+
}
|
|
3215
|
+
return false;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
#openingIdentityConflict(item) {
|
|
3219
|
+
const byItemId =
|
|
3220
|
+
typeof item?.id === "string" ? this.#callsByItemId.get(item.id) : undefined;
|
|
3221
|
+
const byCallId =
|
|
3222
|
+
typeof item?.call_id === "string"
|
|
3223
|
+
? this.#callsByCallId.get(item.call_id)
|
|
3224
|
+
: undefined;
|
|
3225
|
+
if (byItemId || byCallId) return "duplicate output item identity";
|
|
3226
|
+
return undefined;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
#storeCallState(state) {
|
|
3230
|
+
if (this.#trackedCallCount >= this.#maxTrackedOutputItems) {
|
|
3231
|
+
return "output item identity limit";
|
|
3232
|
+
}
|
|
3233
|
+
const retainedBytes = trackedStateBytes(state);
|
|
3234
|
+
if (retainedBytes > this.#maxTrackedStateBytes - this.#trackedStateBytes) {
|
|
3235
|
+
return "output item state byte limit";
|
|
3236
|
+
}
|
|
3237
|
+
if (state.itemId) this.#callsByItemId.set(state.itemId, state);
|
|
3238
|
+
if (state.callId) this.#callsByCallId.set(state.callId, state);
|
|
3239
|
+
this.#trackedCallCount += 1;
|
|
3240
|
+
this.#trackedStateBytes += retainedBytes;
|
|
3241
|
+
return undefined;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
#registerCall(sourceItem, item) {
|
|
3245
|
+
if (this.#nativeCodecBypass(sourceItem)) return "structured tool bypassed its declared codec";
|
|
3246
|
+
const sourceKind = this.#sourceSpecialCallKind(sourceItem);
|
|
3247
|
+
const kind = sourceKind === "function_codec" ? "function_codec" : this.#specialCallKind(item);
|
|
3248
|
+
if ((sourceKind || kind) && sourceKind !== kind) {
|
|
3249
|
+
return "special tool call opening was not restored consistently";
|
|
3250
|
+
}
|
|
3251
|
+
const functionRelay = sourceKind === "function_codec"
|
|
3252
|
+
? this.#lookups.functionRelays.get(sourceItem.name)
|
|
3253
|
+
: undefined;
|
|
3254
|
+
if (
|
|
3255
|
+
(kind === "custom" &&
|
|
3256
|
+
!customCallIdentityMatches(sourceItem, item, this.#lookups)) ||
|
|
3257
|
+
(kind === "function_codec" &&
|
|
3258
|
+
!functionRelayIdentityMatches(item, functionRelay)) ||
|
|
3259
|
+
(kind === "tool_search" &&
|
|
3260
|
+
(item.name !== undefined ||
|
|
3261
|
+
item.namespace !== undefined ||
|
|
3262
|
+
item.execution !== "client" ||
|
|
3263
|
+
!plainObject(item.arguments)))
|
|
3264
|
+
) {
|
|
3265
|
+
return "special tool call opening is incomplete";
|
|
3266
|
+
}
|
|
3267
|
+
const itemId = typeof item?.id === "string" && item.id ? item.id : undefined;
|
|
3268
|
+
const callId =
|
|
3269
|
+
typeof item?.call_id === "string" && item.call_id ? item.call_id : undefined;
|
|
3270
|
+
if (
|
|
3271
|
+
kind &&
|
|
3272
|
+
(!itemId ||
|
|
3273
|
+
!callId ||
|
|
3274
|
+
sourceItem?.id !== itemId ||
|
|
3275
|
+
sourceItem?.call_id !== callId)
|
|
3276
|
+
) {
|
|
3277
|
+
return "special tool call opening lacks stable identity";
|
|
3278
|
+
}
|
|
3279
|
+
if (!itemId && !callId) return undefined;
|
|
3280
|
+
const conflict = this.#openingIdentityConflict(item);
|
|
3281
|
+
if (conflict) return conflict;
|
|
3282
|
+
const state = {
|
|
3283
|
+
kind,
|
|
3284
|
+
itemId,
|
|
3285
|
+
callId,
|
|
3286
|
+
sourceType: sourceItem?.type,
|
|
3287
|
+
sourceName: sourceItem?.name,
|
|
3288
|
+
sourceNamespace: sourceItem?.namespace,
|
|
3289
|
+
outputType: item.type,
|
|
3290
|
+
outputName: item.name,
|
|
3291
|
+
outputNamespace: item.namespace,
|
|
3292
|
+
argumentsDone: false,
|
|
3293
|
+
finalInputLength: undefined,
|
|
3294
|
+
finalInputDigest: undefined,
|
|
3295
|
+
finalArgumentsLength: undefined,
|
|
3296
|
+
finalArgumentsDigest: undefined,
|
|
3297
|
+
sawArgumentDelta: false,
|
|
3298
|
+
deltaCharacters: 0,
|
|
3299
|
+
deltaHash: kind === "custom" ? createHash("sha256") : undefined,
|
|
3300
|
+
closed: false,
|
|
3301
|
+
summarySeen: false,
|
|
3302
|
+
deltaState:
|
|
3303
|
+
kind === "custom"
|
|
3304
|
+
? {
|
|
3305
|
+
opening: "",
|
|
3306
|
+
opened: false,
|
|
3307
|
+
escape: "",
|
|
3308
|
+
closed: false,
|
|
3309
|
+
invalid: false,
|
|
3310
|
+
}
|
|
3311
|
+
: undefined,
|
|
3312
|
+
functionRelay,
|
|
3313
|
+
codec: sourceItem?.type === "function_call"
|
|
3314
|
+
? this.#lookups.customCodecs?.get(sourceItem.name) ||
|
|
3315
|
+
(functionRelay
|
|
3316
|
+
? { maxArgumentBytes: functionRelay.maxArgumentBytes }
|
|
3317
|
+
: undefined)
|
|
3318
|
+
: undefined,
|
|
3319
|
+
codecSourceHash: undefined,
|
|
3320
|
+
codecSourceCharacters: 0,
|
|
3321
|
+
codecSourceSeen: false,
|
|
3322
|
+
codecFinalLength: undefined,
|
|
3323
|
+
codecFinalDigest: undefined,
|
|
3324
|
+
codecOpening: undefined,
|
|
3325
|
+
};
|
|
3326
|
+
if (state.codec) {
|
|
3327
|
+
state.codecSourceHash = createHash("sha256");
|
|
3328
|
+
if (typeof sourceItem.arguments === "string" && sourceItem.arguments) {
|
|
3329
|
+
state.codecOpening = stringFingerprint(sourceItem.arguments);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
return this.#storeCallState(state);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// Some Responses bridges omit output_item.added and emit only one complete
|
|
3336
|
+
// done item or a terminal output summary. Treat that self-contained item as
|
|
3337
|
+
// a closed lifecycle, but reserve its identities exactly like a streamed
|
|
3338
|
+
// opening so later events cannot change owners or replay it.
|
|
3339
|
+
#registerAtomicSpecialCall(sourceItem, item, { summarySeen = false } = {}) {
|
|
3340
|
+
if (this.#nativeCodecBypass(sourceItem)) return "structured tool bypassed its declared codec";
|
|
3341
|
+
const sourceKind = this.#sourceSpecialCallKind(sourceItem);
|
|
3342
|
+
const kind = sourceKind === "function_codec" ? "function_codec" : this.#specialCallKind(item);
|
|
3343
|
+
if (!sourceKind || sourceKind !== kind) {
|
|
3344
|
+
return "atomic special tool call was incomplete or restored inconsistently";
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
const callId = typeof item?.call_id === "string" && item.call_id
|
|
3348
|
+
? item.call_id
|
|
3349
|
+
: undefined;
|
|
3350
|
+
if (!callId || sourceItem?.call_id !== callId) {
|
|
3351
|
+
return "atomic special tool call lacks stable identity";
|
|
3352
|
+
}
|
|
3353
|
+
const sourceHasItemId = Object.hasOwn(sourceItem, "id");
|
|
3354
|
+
const outputHasItemId = Object.hasOwn(item, "id");
|
|
3355
|
+
if (
|
|
3356
|
+
sourceHasItemId !== outputHasItemId ||
|
|
3357
|
+
(sourceHasItemId &&
|
|
3358
|
+
(typeof item.id !== "string" || !item.id || sourceItem.id !== item.id))
|
|
3359
|
+
) {
|
|
3360
|
+
return "atomic special tool call lacks stable identity";
|
|
3361
|
+
}
|
|
3362
|
+
const itemId = outputHasItemId ? item.id : undefined;
|
|
3363
|
+
|
|
3364
|
+
if (kind === "custom") {
|
|
3365
|
+
if (
|
|
3366
|
+
!customCallIdentityMatches(sourceItem, item, this.#lookups) ||
|
|
3367
|
+
typeof item.input !== "string"
|
|
3368
|
+
) {
|
|
3369
|
+
return "incomplete atomic custom tool call";
|
|
3370
|
+
}
|
|
3371
|
+
if (sourceItem.type === "function_call") {
|
|
3372
|
+
if (
|
|
3373
|
+
customToolInput(sourceItem.arguments, false, CUSTOM_TOOL_INPUT_PROPERTY, this.#lookups.customCodecs?.get(sourceItem.name)) !== item.input
|
|
3374
|
+
) {
|
|
3375
|
+
return "atomic custom tool call was not restored consistently";
|
|
3376
|
+
}
|
|
3377
|
+
} else if (
|
|
3378
|
+
sourceItem.input !== item.input
|
|
3379
|
+
) {
|
|
3380
|
+
return "atomic custom tool call changed native content";
|
|
3381
|
+
}
|
|
3382
|
+
} else if (kind === "function_codec") {
|
|
3383
|
+
const relay = this.#lookups.functionRelays?.get(sourceItem.name);
|
|
3384
|
+
if (
|
|
3385
|
+
!functionRelayIdentityMatches(item, relay) ||
|
|
3386
|
+
typeof item.arguments !== "string"
|
|
3387
|
+
) {
|
|
3388
|
+
return "incomplete atomic function relay call";
|
|
3389
|
+
}
|
|
3390
|
+
if (relay.rewriteArguments(sourceItem.arguments) !== item.arguments) {
|
|
3391
|
+
return "atomic function relay call was not restored consistently";
|
|
3392
|
+
}
|
|
3393
|
+
} else {
|
|
3394
|
+
if (
|
|
3395
|
+
item.name !== undefined ||
|
|
3396
|
+
item.namespace !== undefined ||
|
|
3397
|
+
item.execution !== "client" ||
|
|
3398
|
+
!plainObject(item.arguments)
|
|
3399
|
+
) {
|
|
3400
|
+
return "incomplete atomic tool search call";
|
|
3401
|
+
}
|
|
3402
|
+
if (sourceItem.type === "function_call") {
|
|
3403
|
+
const sourceArguments = toolSearchArguments(sourceItem.arguments, false);
|
|
3404
|
+
if (
|
|
3405
|
+
sourceItem.name !== this.#lookups.toolSearch?.providerName ||
|
|
3406
|
+
!sourceArguments ||
|
|
3407
|
+
!isDeepStrictEqual(sourceArguments, item.arguments)
|
|
3408
|
+
) {
|
|
3409
|
+
return "atomic tool search call was not restored consistently";
|
|
3410
|
+
}
|
|
3411
|
+
} else if (
|
|
3412
|
+
sourceItem.execution !== item.execution ||
|
|
3413
|
+
!isDeepStrictEqual(sourceItem.arguments, item.arguments)
|
|
3414
|
+
) {
|
|
3415
|
+
return "atomic tool search call changed native content";
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
const conflict = this.#openingIdentityConflict(item);
|
|
3420
|
+
if (conflict) return conflict;
|
|
3421
|
+
const finalInputFingerprint = kind === "custom"
|
|
3422
|
+
? stringFingerprint(item.input)
|
|
3423
|
+
: undefined;
|
|
3424
|
+
const finalArgumentsFingerprint = kind === "tool_search"
|
|
3425
|
+
? canonicalJsonFingerprint(item.arguments)
|
|
3426
|
+
: kind === "function_codec"
|
|
3427
|
+
? stringFingerprint(item.arguments)
|
|
3428
|
+
: undefined;
|
|
3429
|
+
const codec = sourceItem.type === "function_call"
|
|
3430
|
+
? this.#lookups.customCodecs?.get(sourceItem.name) ||
|
|
3431
|
+
(kind === "function_codec"
|
|
3432
|
+
? { maxArgumentBytes: this.#lookups.functionRelays?.get(sourceItem.name)?.maxArgumentBytes }
|
|
3433
|
+
: undefined)
|
|
3434
|
+
: undefined;
|
|
3435
|
+
const codecFingerprint = codec ? stringFingerprint(sourceItem.arguments) : undefined;
|
|
3436
|
+
const state = {
|
|
3437
|
+
kind,
|
|
3438
|
+
itemId,
|
|
3439
|
+
callId,
|
|
3440
|
+
sourceType: sourceItem.type,
|
|
3441
|
+
sourceName: sourceItem.name,
|
|
3442
|
+
sourceNamespace: sourceItem.namespace,
|
|
3443
|
+
outputType: item.type,
|
|
3444
|
+
outputName: item.name,
|
|
3445
|
+
outputNamespace: item.namespace,
|
|
3446
|
+
argumentsDone: true,
|
|
3447
|
+
finalInputLength: finalInputFingerprint?.length,
|
|
3448
|
+
finalInputDigest: finalInputFingerprint?.digest,
|
|
3449
|
+
finalArgumentsLength: finalArgumentsFingerprint?.length,
|
|
3450
|
+
finalArgumentsDigest: finalArgumentsFingerprint?.digest,
|
|
3451
|
+
sawArgumentDelta: false,
|
|
3452
|
+
deltaCharacters: 0,
|
|
3453
|
+
deltaHash: undefined,
|
|
3454
|
+
closed: true,
|
|
3455
|
+
summarySeen,
|
|
3456
|
+
deltaState: undefined,
|
|
3457
|
+
functionRelay: kind === "function_codec" ? this.#lookups.functionRelays?.get(sourceItem.name) : undefined,
|
|
3458
|
+
codec,
|
|
3459
|
+
codecFinalLength: codecFingerprint?.length,
|
|
3460
|
+
codecFinalDigest: codecFingerprint?.digest,
|
|
3461
|
+
};
|
|
3462
|
+
return this.#storeCallState(state);
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
#registerOrdinaryOutputItem(
|
|
3466
|
+
sourceItem,
|
|
3467
|
+
item,
|
|
3468
|
+
{ closed = true, summarySeen = false } = {},
|
|
3469
|
+
) {
|
|
3470
|
+
// Ordinary items reserve the same id domains as special relays. This also
|
|
3471
|
+
// covers nonterminal response snapshots, which can precede an explicit
|
|
3472
|
+
// output_item.done event and therefore must not be marked closed yet.
|
|
3473
|
+
const itemId = typeof item?.id === "string" && item.id ? item.id : undefined;
|
|
3474
|
+
const callId = typeof item?.call_id === "string" && item.call_id
|
|
3475
|
+
? item.call_id
|
|
3476
|
+
: undefined;
|
|
3477
|
+
const sourceItemId = typeof sourceItem?.id === "string" && sourceItem.id
|
|
3478
|
+
? sourceItem.id
|
|
3479
|
+
: undefined;
|
|
3480
|
+
const sourceCallId = typeof sourceItem?.call_id === "string" && sourceItem.call_id
|
|
3481
|
+
? sourceItem.call_id
|
|
3482
|
+
: undefined;
|
|
3483
|
+
const sourceHasItemId = Boolean(
|
|
3484
|
+
sourceItem && typeof sourceItem === "object" && Object.hasOwn(sourceItem, "id"),
|
|
3485
|
+
);
|
|
3486
|
+
const outputHasItemId = Boolean(
|
|
3487
|
+
item && typeof item === "object" && Object.hasOwn(item, "id"),
|
|
3488
|
+
);
|
|
3489
|
+
const sourceHasCallId = Boolean(
|
|
3490
|
+
sourceItem && typeof sourceItem === "object" && Object.hasOwn(sourceItem, "call_id"),
|
|
3491
|
+
);
|
|
3492
|
+
const outputHasCallId = Boolean(
|
|
3493
|
+
item && typeof item === "object" && Object.hasOwn(item, "call_id"),
|
|
3494
|
+
);
|
|
3495
|
+
if (
|
|
3496
|
+
sourceItemId !== itemId ||
|
|
3497
|
+
sourceCallId !== callId ||
|
|
3498
|
+
sourceHasItemId !== outputHasItemId ||
|
|
3499
|
+
sourceHasCallId !== outputHasCallId ||
|
|
3500
|
+
(sourceHasItemId && !itemId) ||
|
|
3501
|
+
(sourceHasCallId && !callId)
|
|
3502
|
+
) {
|
|
3503
|
+
return "output item lacks stable identity";
|
|
3504
|
+
}
|
|
3505
|
+
if (!itemId && !callId) return undefined;
|
|
3506
|
+
const conflict = this.#openingIdentityConflict(item);
|
|
3507
|
+
if (conflict) return conflict;
|
|
3508
|
+
const state = {
|
|
3509
|
+
kind: undefined,
|
|
3510
|
+
itemId,
|
|
3511
|
+
callId,
|
|
3512
|
+
sourceType: sourceItem?.type,
|
|
3513
|
+
sourceName: sourceItem?.name,
|
|
3514
|
+
sourceNamespace: sourceItem?.namespace,
|
|
3515
|
+
outputType: item?.type,
|
|
3516
|
+
outputName: item?.name,
|
|
3517
|
+
outputNamespace: item?.namespace,
|
|
3518
|
+
argumentsDone: true,
|
|
3519
|
+
finalInputLength: undefined,
|
|
3520
|
+
finalInputDigest: undefined,
|
|
3521
|
+
finalArgumentsLength: undefined,
|
|
3522
|
+
finalArgumentsDigest: undefined,
|
|
3523
|
+
sawArgumentDelta: false,
|
|
3524
|
+
deltaCharacters: 0,
|
|
3525
|
+
deltaHash: undefined,
|
|
3526
|
+
closed,
|
|
3527
|
+
summarySeen,
|
|
3528
|
+
deltaState: undefined,
|
|
3529
|
+
};
|
|
3530
|
+
return this.#storeCallState(state);
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
#registerAtomicOutputItem(sourceItem, item, { summarySeen = false } = {}) {
|
|
3534
|
+
const sourceKind = this.#sourceSpecialCallKind(sourceItem);
|
|
3535
|
+
const outputKind = this.#specialCallKind(item);
|
|
3536
|
+
if (sourceKind || outputKind) {
|
|
3537
|
+
return this.#registerAtomicSpecialCall(sourceItem, item, { summarySeen });
|
|
3538
|
+
}
|
|
3539
|
+
return this.#registerOrdinaryOutputItem(sourceItem, item, {
|
|
3540
|
+
closed: true,
|
|
3541
|
+
summarySeen,
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
#outputItemMatchesState(sourceItem, item, state) {
|
|
3546
|
+
return (
|
|
3547
|
+
sourceItem?.id === state.itemId &&
|
|
3548
|
+
sourceItem?.call_id === state.callId &&
|
|
3549
|
+
sourceItem?.type === state.sourceType &&
|
|
3550
|
+
sourceItem?.name === state.sourceName &&
|
|
3551
|
+
sourceItem?.namespace === state.sourceNamespace &&
|
|
3552
|
+
item?.id === state.itemId &&
|
|
3553
|
+
item?.call_id === state.callId &&
|
|
3554
|
+
item?.type === state.outputType &&
|
|
3555
|
+
item?.name === state.outputName &&
|
|
3556
|
+
item?.namespace === state.outputNamespace
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
#specialCallForArgumentsEvent(event) {
|
|
3561
|
+
const byItemId =
|
|
3562
|
+
typeof event?.item_id === "string"
|
|
3563
|
+
? this.#callsByItemId.get(event.item_id)
|
|
3564
|
+
: undefined;
|
|
3565
|
+
const byCallId =
|
|
3566
|
+
typeof event?.call_id === "string"
|
|
3567
|
+
? this.#callsByCallId.get(event.call_id)
|
|
3568
|
+
: undefined;
|
|
3569
|
+
if (byItemId && byCallId && byItemId !== byCallId) {
|
|
3570
|
+
return { reason: "conflicting special tool call identity" };
|
|
3571
|
+
}
|
|
3572
|
+
const state = byItemId || byCallId;
|
|
3573
|
+
if (!state) return this.#requiresCodec ? { reason: "arguments without a known output item" } : {};
|
|
3574
|
+
if (!state.kind) return {};
|
|
3575
|
+
if (event.item_id !== state.itemId) {
|
|
3576
|
+
return { reason: "mismatched special tool call item id" };
|
|
3577
|
+
}
|
|
3578
|
+
if (event.call_id !== undefined && event.call_id !== state.callId) {
|
|
3579
|
+
return { reason: "mismatched special tool call call id" };
|
|
3580
|
+
}
|
|
3581
|
+
if (state.closed) return { reason: "special tool call event after close" };
|
|
3582
|
+
return { state };
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
#customDeltaMismatch(state, inputFingerprint) {
|
|
3586
|
+
if (state.codec) return undefined; // Source JSON is checked separately; no patch delta was emitted.
|
|
3587
|
+
if (!state.sawArgumentDelta) return undefined;
|
|
3588
|
+
// LiteLLM keeps arguments that are not a leading `content` wrapper verbatim,
|
|
3589
|
+
// so the incremental decoder cannot follow them. When it emitted no input
|
|
3590
|
+
// text, the client saw nothing the completed input could contradict.
|
|
3591
|
+
if (state.sourceType === "custom_tool_call" && state.deltaCharacters === 0) return undefined;
|
|
3592
|
+
if (
|
|
3593
|
+
state.deltaState.invalid ||
|
|
3594
|
+
!state.deltaState.opened ||
|
|
3595
|
+
!state.deltaState.closed ||
|
|
3596
|
+
state.deltaState.escape
|
|
3597
|
+
) {
|
|
3598
|
+
return "incomplete custom tool argument delta sequence";
|
|
3599
|
+
}
|
|
3600
|
+
if (state.deltaCharacters !== inputFingerprint.length) {
|
|
3601
|
+
return "custom tool argument deltas disagree with completed input";
|
|
3602
|
+
}
|
|
3603
|
+
const streamed = state.deltaHash.copy().digest();
|
|
3604
|
+
return streamed.equals(inputFingerprint.digest)
|
|
3605
|
+
? undefined
|
|
3606
|
+
: "custom tool argument deltas disagree with completed input";
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
#validateCodecSource(state, argumentsText) {
|
|
3610
|
+
if (!state.codec) return undefined;
|
|
3611
|
+
if (typeof argumentsText !== "string" || Buffer.byteLength(argumentsText, "utf8") > state.codec.maxArgumentBytes) {
|
|
3612
|
+
return "invalid or oversized structured arguments";
|
|
3613
|
+
}
|
|
3614
|
+
const fingerprint = stringFingerprint(argumentsText);
|
|
3615
|
+
if (state.codecOpening && !fingerprintMatches(fingerprint, state.codecOpening.length, state.codecOpening.digest)) {
|
|
3616
|
+
return "structured arguments changed after opening";
|
|
3617
|
+
}
|
|
3618
|
+
if (state.codecFinalDigest) {
|
|
3619
|
+
return fingerprintMatches(fingerprint, state.codecFinalLength, state.codecFinalDigest)
|
|
3620
|
+
? undefined : "structured arguments changed after completion";
|
|
3621
|
+
}
|
|
3622
|
+
if (state.codecSourceSeen && (
|
|
3623
|
+
state.codecSourceCharacters !== fingerprint.length ||
|
|
3624
|
+
!state.codecSourceHash.copy().digest().equals(fingerprint.digest)
|
|
3625
|
+
)) return "structured argument deltas disagree with completed arguments";
|
|
3626
|
+
state.codecFinalLength = fingerprint.length;
|
|
3627
|
+
state.codecFinalDigest = fingerprint.digest;
|
|
3628
|
+
state.codecSourceHash = undefined;
|
|
3629
|
+
state.codecOpening = undefined;
|
|
3630
|
+
return undefined;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
#closeOutputItem(sourceItem, item) {
|
|
3634
|
+
const byItemId =
|
|
3635
|
+
typeof sourceItem?.id === "string"
|
|
3636
|
+
? this.#callsByItemId.get(sourceItem.id)
|
|
3637
|
+
: undefined;
|
|
3638
|
+
const byCallId =
|
|
3639
|
+
typeof sourceItem?.call_id === "string"
|
|
3640
|
+
? this.#callsByCallId.get(sourceItem.call_id)
|
|
3641
|
+
: undefined;
|
|
3642
|
+
if (byItemId && byCallId && byItemId !== byCallId) {
|
|
3643
|
+
return "conflicting special tool call close identity";
|
|
3644
|
+
}
|
|
3645
|
+
const state = byItemId || byCallId;
|
|
3646
|
+
const sourceKind = this.#sourceSpecialCallKind(sourceItem);
|
|
3647
|
+
const outputKind = this.#specialCallKind(item);
|
|
3648
|
+
if (!state) {
|
|
3649
|
+
return this.#registerAtomicOutputItem(sourceItem, item);
|
|
3650
|
+
}
|
|
3651
|
+
if (state.closed) return "duplicate output item close";
|
|
3652
|
+
if (!this.#outputItemMatchesState(sourceItem, item, state)) {
|
|
3653
|
+
return state.kind || sourceKind || outputKind
|
|
3654
|
+
? "mismatched special tool call close identity"
|
|
3655
|
+
: "mismatched output item close identity";
|
|
3656
|
+
}
|
|
3657
|
+
if (!state.kind) {
|
|
3658
|
+
state.closed = true;
|
|
3659
|
+
return undefined;
|
|
3660
|
+
}
|
|
3661
|
+
if (state.kind === "function_codec") {
|
|
3662
|
+
const codecReason = this.#validateCodecSource(state, sourceItem.arguments);
|
|
3663
|
+
if (codecReason) return codecReason;
|
|
3664
|
+
if (typeof item.arguments !== "string") {
|
|
3665
|
+
return "function relay arguments changed before close";
|
|
3666
|
+
}
|
|
3667
|
+
const argumentsFingerprint = stringFingerprint(item.arguments);
|
|
3668
|
+
if (
|
|
3669
|
+
state.argumentsDone &&
|
|
3670
|
+
!fingerprintMatches(
|
|
3671
|
+
argumentsFingerprint,
|
|
3672
|
+
state.finalArgumentsLength,
|
|
3673
|
+
state.finalArgumentsDigest,
|
|
3674
|
+
)
|
|
3675
|
+
) {
|
|
3676
|
+
return "function relay arguments changed before close";
|
|
3677
|
+
}
|
|
3678
|
+
if (!state.argumentsDone) {
|
|
3679
|
+
const expected = state.functionRelay?.rewriteArguments(sourceItem.arguments);
|
|
3680
|
+
if (expected !== item.arguments) {
|
|
3681
|
+
return "function relay arguments changed before close";
|
|
3682
|
+
}
|
|
3683
|
+
state.finalArgumentsLength = argumentsFingerprint.length;
|
|
3684
|
+
state.finalArgumentsDigest = argumentsFingerprint.digest;
|
|
3685
|
+
}
|
|
3686
|
+
state.argumentsDone = true;
|
|
3687
|
+
state.deltaHash = undefined;
|
|
3688
|
+
state.deltaState = undefined;
|
|
3689
|
+
}
|
|
3690
|
+
if (state.kind === "custom") {
|
|
3691
|
+
const codecReason = this.#validateCodecSource(state, sourceItem.arguments);
|
|
3692
|
+
if (codecReason) return codecReason;
|
|
3693
|
+
if (typeof item.input !== "string") {
|
|
3694
|
+
return "custom tool call input changed before close";
|
|
3695
|
+
}
|
|
3696
|
+
const inputFingerprint = stringFingerprint(item.input);
|
|
3697
|
+
if (
|
|
3698
|
+
state.argumentsDone &&
|
|
3699
|
+
!fingerprintMatches(
|
|
3700
|
+
inputFingerprint,
|
|
3701
|
+
state.finalInputLength,
|
|
3702
|
+
state.finalInputDigest,
|
|
3703
|
+
)
|
|
3704
|
+
) {
|
|
3705
|
+
return "custom tool call input changed before close";
|
|
3706
|
+
}
|
|
3707
|
+
if (!state.argumentsDone) {
|
|
3708
|
+
const reason = this.#customDeltaMismatch(state, inputFingerprint);
|
|
3709
|
+
if (reason) return reason;
|
|
3710
|
+
state.finalInputLength = inputFingerprint.length;
|
|
3711
|
+
state.finalInputDigest = inputFingerprint.digest;
|
|
3712
|
+
}
|
|
3713
|
+
state.argumentsDone = true;
|
|
3714
|
+
state.deltaHash = undefined;
|
|
3715
|
+
state.deltaState = undefined;
|
|
3716
|
+
}
|
|
3717
|
+
if (state.kind === "tool_search") {
|
|
3718
|
+
const argumentsFingerprint = canonicalJsonFingerprint(item.arguments);
|
|
3719
|
+
if (
|
|
3720
|
+
state.argumentsDone &&
|
|
3721
|
+
!fingerprintMatches(
|
|
3722
|
+
argumentsFingerprint,
|
|
3723
|
+
state.finalArgumentsLength,
|
|
3724
|
+
state.finalArgumentsDigest,
|
|
3725
|
+
)
|
|
3726
|
+
) {
|
|
3727
|
+
return "tool search arguments changed before close";
|
|
3728
|
+
}
|
|
3729
|
+
state.finalArgumentsLength = argumentsFingerprint.length;
|
|
3730
|
+
state.finalArgumentsDigest = argumentsFingerprint.digest;
|
|
3731
|
+
state.argumentsDone = true;
|
|
3732
|
+
}
|
|
3733
|
+
state.closed = true;
|
|
3734
|
+
return undefined;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
#validateOutputSummaryItem(sourceItem, item, { allowAtomic = false } = {}) {
|
|
3738
|
+
const byItemId =
|
|
3739
|
+
typeof sourceItem?.id === "string"
|
|
3740
|
+
? this.#callsByItemId.get(sourceItem.id)
|
|
3741
|
+
: undefined;
|
|
3742
|
+
const byCallId =
|
|
3743
|
+
typeof sourceItem?.call_id === "string"
|
|
3744
|
+
? this.#callsByCallId.get(sourceItem.call_id)
|
|
3745
|
+
: undefined;
|
|
3746
|
+
if (byItemId && byCallId && byItemId !== byCallId) {
|
|
3747
|
+
return "conflicting special tool call summary identity";
|
|
3748
|
+
}
|
|
3749
|
+
const state = byItemId || byCallId;
|
|
3750
|
+
const sourceKind = this.#sourceSpecialCallKind(sourceItem);
|
|
3751
|
+
const outputKind = this.#specialCallKind(item);
|
|
3752
|
+
if (!state) {
|
|
3753
|
+
if (!allowAtomic) {
|
|
3754
|
+
if (sourceKind || outputKind) {
|
|
3755
|
+
return "special tool call summary without a matching opening";
|
|
3756
|
+
}
|
|
3757
|
+
return this.#registerOrdinaryOutputItem(sourceItem, item, {
|
|
3758
|
+
closed: false,
|
|
3759
|
+
summarySeen: false,
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
return this.#registerAtomicOutputItem(sourceItem, item, { summarySeen: true });
|
|
3763
|
+
}
|
|
3764
|
+
if (!state.closed && state.kind) return "special tool call summary before close";
|
|
3765
|
+
if (state.summarySeen) return "duplicate output item summary";
|
|
3766
|
+
if (!this.#outputItemMatchesState(sourceItem, item, state)) {
|
|
3767
|
+
return state.kind || sourceKind || outputKind
|
|
3768
|
+
? "mismatched special tool call summary identity"
|
|
3769
|
+
: "mismatched output item summary identity";
|
|
3770
|
+
}
|
|
3771
|
+
if (!state.kind) {
|
|
3772
|
+
// Progress snapshots may repeat the same ordinary item while its content
|
|
3773
|
+
// grows. They reserve and validate ownership, but only a terminal output
|
|
3774
|
+
// summary closes and consumes the one allowed summary transition.
|
|
3775
|
+
if (allowAtomic) {
|
|
3776
|
+
state.closed = true;
|
|
3777
|
+
state.summarySeen = true;
|
|
3778
|
+
}
|
|
3779
|
+
return undefined;
|
|
3780
|
+
}
|
|
3781
|
+
if (state.kind === "custom") {
|
|
3782
|
+
const codecReason = this.#validateCodecSource(state, sourceItem.arguments);
|
|
3783
|
+
if (codecReason) return codecReason;
|
|
3784
|
+
if (typeof item.input !== "string") {
|
|
3785
|
+
return "custom tool call summary input changed after close";
|
|
3786
|
+
}
|
|
3787
|
+
const inputFingerprint = stringFingerprint(item.input);
|
|
3788
|
+
if (
|
|
3789
|
+
!fingerprintMatches(
|
|
3790
|
+
inputFingerprint,
|
|
3791
|
+
state.finalInputLength,
|
|
3792
|
+
state.finalInputDigest,
|
|
3793
|
+
)
|
|
3794
|
+
) {
|
|
3795
|
+
return "custom tool call summary input changed after close";
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
if (state.kind === "tool_search") {
|
|
3799
|
+
const argumentsFingerprint = canonicalJsonFingerprint(item.arguments);
|
|
3800
|
+
if (
|
|
3801
|
+
!fingerprintMatches(
|
|
3802
|
+
argumentsFingerprint,
|
|
3803
|
+
state.finalArgumentsLength,
|
|
3804
|
+
state.finalArgumentsDigest,
|
|
3805
|
+
)
|
|
3806
|
+
) {
|
|
3807
|
+
return "tool search arguments changed after close";
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
if (state.kind === "function_codec") {
|
|
3811
|
+
const codecReason = this.#validateCodecSource(state, sourceItem.arguments);
|
|
3812
|
+
if (codecReason) return codecReason;
|
|
3813
|
+
if (typeof item.arguments !== "string") {
|
|
3814
|
+
return "function relay arguments changed after close";
|
|
3815
|
+
}
|
|
3816
|
+
const argumentsFingerprint = stringFingerprint(item.arguments);
|
|
3817
|
+
if (
|
|
3818
|
+
!fingerprintMatches(
|
|
3819
|
+
argumentsFingerprint,
|
|
3820
|
+
state.finalArgumentsLength,
|
|
3821
|
+
state.finalArgumentsDigest,
|
|
3822
|
+
)
|
|
3823
|
+
) {
|
|
3824
|
+
return "function relay arguments changed after close";
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
if (allowAtomic) state.summarySeen = true;
|
|
3828
|
+
return undefined;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
#validateOutputItems(sourceEvent, event, { allowAtomic = false } = {}) {
|
|
3832
|
+
for (const [sourceOutput, output] of [
|
|
3833
|
+
[sourceEvent?.output, event?.output],
|
|
3834
|
+
[sourceEvent?.response?.output, event?.response?.output],
|
|
3835
|
+
]) {
|
|
3836
|
+
if (!Array.isArray(sourceOutput) || !Array.isArray(output)) continue;
|
|
3837
|
+
for (let index = 0; index < sourceOutput.length; index += 1) {
|
|
3838
|
+
const reason = this.#validateOutputSummaryItem(sourceOutput[index], output[index], {
|
|
3839
|
+
allowAtomic,
|
|
3840
|
+
});
|
|
3841
|
+
if (reason) return reason;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
return undefined;
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
#rewrittenSseFrame(frame, replacements) {
|
|
3848
|
+
const pieces = [];
|
|
3849
|
+
let cursor = 0;
|
|
3850
|
+
for (const [line, replacement] of [...replacements].sort(
|
|
3851
|
+
([left], [right]) => left.start - right.start,
|
|
3852
|
+
)) {
|
|
3853
|
+
if (!line) continue;
|
|
3854
|
+
if (line.start > cursor) pieces.push(frame.subarray(cursor, line.start));
|
|
3855
|
+
if (line.parseStart > line.start) {
|
|
3856
|
+
pieces.push(frame.subarray(line.start, line.parseStart));
|
|
3857
|
+
}
|
|
3858
|
+
pieces.push(Buffer.from(replacement, "utf8"));
|
|
3859
|
+
cursor = line.contentEnd;
|
|
3860
|
+
}
|
|
3861
|
+
if (cursor < frame.length) pieces.push(frame.subarray(cursor));
|
|
3862
|
+
return Buffer.concat(pieces);
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
#rewriteSseFrame(frame) {
|
|
3866
|
+
const atStreamStart = this.#sseAtStreamStart;
|
|
3867
|
+
this.#sseAtStreamStart = false;
|
|
3868
|
+
// No decoder is allowed to see an undecided frame. Its replacement
|
|
3869
|
+
// character would make fail-open lossy before we knew whether to rewrite.
|
|
3870
|
+
if (!isUtf8(frame)) {
|
|
3871
|
+
return this.#unsafeSseFrame(frame, "invalid UTF-8");
|
|
3872
|
+
}
|
|
3873
|
+
const { eventLine, dataLine, lineEndingLine, repeated } = this.#sseFields(
|
|
3874
|
+
frame,
|
|
3875
|
+
atStreamStart,
|
|
3876
|
+
);
|
|
3877
|
+
this.#rememberSseLineEnding(frame, lineEndingLine);
|
|
3878
|
+
// SSE formally concatenates multiple data fields and gives repeated event
|
|
3879
|
+
// fields ordering semantics. Rewriting just one field would create bytes
|
|
3880
|
+
// whose EventSource meaning differs from the JSON we inspected. The
|
|
3881
|
+
// Responses wire uses exactly one of each, so anything else is preserved
|
|
3882
|
+
// and disables stateful rewriting for the remainder of this response.
|
|
3883
|
+
if (repeated) {
|
|
3884
|
+
return this.#unsafeSseFrame(frame, "repeated SSE event or data field");
|
|
3885
|
+
}
|
|
3886
|
+
const eventText = eventLine
|
|
3887
|
+
? frame.subarray(eventLine.parseStart, eventLine.contentEnd).toString("utf8")
|
|
3888
|
+
: undefined;
|
|
3889
|
+
const eventName = eventText === undefined
|
|
3890
|
+
? undefined
|
|
3891
|
+
: sseLineFieldValue(eventText, "event");
|
|
3892
|
+
const dataTextLine = dataLine
|
|
3893
|
+
? frame.subarray(dataLine.parseStart, dataLine.contentEnd).toString("utf8")
|
|
3894
|
+
: undefined;
|
|
3895
|
+
const dataText = dataTextLine === undefined
|
|
3896
|
+
? ""
|
|
3897
|
+
: sseLineFieldValue(dataTextLine, "data");
|
|
3898
|
+
if (!dataLine) return [frame];
|
|
3899
|
+
if (!dataText || dataText === "[DONE]") {
|
|
3900
|
+
if (eventName && eventName !== "message") {
|
|
3901
|
+
return this.#unsafeSseFrame(
|
|
3902
|
+
frame,
|
|
3903
|
+
"non-generic SSE event without a matching JSON type",
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
// Inject before the stream terminator so Codex still executes the calls.
|
|
3907
|
+
if (dataText === "[DONE]") {
|
|
3908
|
+
if (this.#hasOpenSpecialCalls()) {
|
|
3909
|
+
return this.#unsafeSseFrame(frame, "stream ended before special tool call close");
|
|
3910
|
+
}
|
|
3911
|
+
return [...this.#drainInterruptBlocks(), frame];
|
|
3912
|
+
}
|
|
3913
|
+
return [frame];
|
|
3914
|
+
}
|
|
3915
|
+
if (!jsonIsUnambiguousForRewrite(dataText)) {
|
|
3916
|
+
return this.#unsafeSseFrame(frame, "ambiguous or malformed JSON event");
|
|
3917
|
+
}
|
|
3918
|
+
try {
|
|
3919
|
+
let event = JSON.parse(dataText);
|
|
3920
|
+
const payloadType = event?.type;
|
|
3921
|
+
const genericEventName = !eventName || eventName === "message";
|
|
3922
|
+
if (!genericEventName && payloadType !== eventName) {
|
|
3923
|
+
// The event field and JSON body are two claims about the same Responses
|
|
3924
|
+
// event. Do not choose whichever terminal/rewrite behavior is more
|
|
3925
|
+
// convenient when both claims are present and disagree.
|
|
3926
|
+
return this.#unsafeSseFrame(frame, "conflicting SSE event and JSON type");
|
|
3927
|
+
}
|
|
3928
|
+
const rawDoneMatch = event?.type === "response.function_call_arguments.done"
|
|
3929
|
+
? this.#specialCallForArgumentsEvent(event) : undefined;
|
|
3930
|
+
// LiteLLM's native custom lifecycle keeps provider arguments verbatim when
|
|
3931
|
+
// they are not its JSON wrapper. They are not function arguments to
|
|
3932
|
+
// rewrite; the custom-tool check below validates them instead.
|
|
3933
|
+
const rawArgumentsDone = !rawDoneMatch?.reason && (
|
|
3934
|
+
rawDoneMatch?.state?.codec?.preserveRawArguments === true ||
|
|
3935
|
+
(rawDoneMatch?.state?.kind === "custom" &&
|
|
3936
|
+
rawDoneMatch.state.sourceType === "custom_tool_call" &&
|
|
3937
|
+
!rawDoneMatch.state.codec)
|
|
3938
|
+
);
|
|
3939
|
+
if (!embeddedFunctionArgumentsAreUnambiguous(event, this.#lookups, rawArgumentsDone)) {
|
|
3940
|
+
return this.#unsafeSseFrame(frame, "ambiguous function arguments");
|
|
3941
|
+
}
|
|
3942
|
+
const sourceEvent = event;
|
|
3943
|
+
const originalEventType = event?.type;
|
|
3944
|
+
let changed = false;
|
|
3945
|
+
if (sourceEvent?.type === "response.output_item.added") {
|
|
3946
|
+
const conflict = this.#openingIdentityConflict(sourceEvent.item);
|
|
3947
|
+
if (conflict) return this.#unsafeSseFrame(frame, conflict);
|
|
3948
|
+
}
|
|
3949
|
+
if (
|
|
3950
|
+
!this.#injectOnly &&
|
|
3951
|
+
(sourceEvent?.type === "response.custom_tool_call_input.delta" ||
|
|
3952
|
+
sourceEvent?.type === "response.custom_tool_call_input.done")
|
|
3953
|
+
) {
|
|
3954
|
+
const matched = this.#specialCallForArgumentsEvent(sourceEvent);
|
|
3955
|
+
if (matched.reason) return this.#unsafeSseFrame(frame, matched.reason);
|
|
3956
|
+
if (matched.state?.sourceType === "function_call") {
|
|
3957
|
+
return this.#unsafeSseFrame(
|
|
3958
|
+
frame,
|
|
3959
|
+
"native custom input event inside a bridged function lifecycle",
|
|
3960
|
+
);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
if (
|
|
3964
|
+
!this.#injectOnly &&
|
|
3965
|
+
event?.type === "response.function_call_arguments.delta"
|
|
3966
|
+
) {
|
|
3967
|
+
const matched = this.#specialCallForArgumentsEvent(event);
|
|
3968
|
+
if (matched.reason) return this.#unsafeSseFrame(frame, matched.reason);
|
|
3969
|
+
if (matched.state) {
|
|
3970
|
+
if (matched.state.argumentsDone) {
|
|
3971
|
+
return this.#unsafeSseFrame(frame, "special tool call delta after arguments done");
|
|
3972
|
+
}
|
|
3973
|
+
if (matched.state.kind === "tool_search") {
|
|
3974
|
+
this.#commitSemanticMutation();
|
|
3975
|
+
return [];
|
|
3976
|
+
}
|
|
3977
|
+
if (matched.state.codec || matched.state.kind === "function_codec") {
|
|
3978
|
+
// Hash the original JSON incrementally instead of retaining it or
|
|
3979
|
+
// interpreting partial operations as executable patch text.
|
|
3980
|
+
if (typeof event.delta !== "string") return this.#unsafeSseFrame(frame, "invalid structured argument delta");
|
|
3981
|
+
matched.state.codecSourceSeen = true;
|
|
3982
|
+
matched.state.codecSourceCharacters += event.delta.length;
|
|
3983
|
+
const maxBytes = matched.state.codec?.maxArgumentBytes ?? matched.state.functionRelay?.maxArgumentBytes;
|
|
3984
|
+
if (maxBytes && matched.state.codecSourceCharacters > maxBytes) {
|
|
3985
|
+
return this.#unsafeSseFrame(frame, "structured argument delta limit");
|
|
3986
|
+
}
|
|
3987
|
+
matched.state.codecSourceHash.update(Buffer.from(event.delta, "utf16le"));
|
|
3988
|
+
this.#commitSemanticMutation();
|
|
3989
|
+
return [];
|
|
3990
|
+
}
|
|
3991
|
+
matched.state.sawArgumentDelta = true;
|
|
3992
|
+
const argumentProperty =
|
|
3993
|
+
matched.state.sourceType === "custom_tool_call"
|
|
3994
|
+
? LITELLM_CUSTOM_TOOL_INPUT_PROPERTY
|
|
3995
|
+
: CUSTOM_TOOL_INPUT_PROPERTY;
|
|
3996
|
+
const delta = customToolInputDelta(
|
|
3997
|
+
matched.state.deltaState,
|
|
3998
|
+
event.delta,
|
|
3999
|
+
argumentProperty,
|
|
4000
|
+
);
|
|
4001
|
+
if (delta === undefined) {
|
|
4002
|
+
this.#commitSemanticMutation();
|
|
4003
|
+
return [];
|
|
4004
|
+
}
|
|
4005
|
+
matched.state.deltaHash.update(Buffer.from(delta, "utf16le"));
|
|
4006
|
+
matched.state.deltaCharacters += delta.length;
|
|
4007
|
+
event = {
|
|
4008
|
+
...event,
|
|
4009
|
+
type: "response.custom_tool_call_input.delta",
|
|
4010
|
+
delta,
|
|
4011
|
+
};
|
|
4012
|
+
changed = true;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
if (
|
|
4016
|
+
!this.#injectOnly &&
|
|
4017
|
+
event?.type === "response.function_call_arguments.done"
|
|
4018
|
+
) {
|
|
4019
|
+
const matched = this.#specialCallForArgumentsEvent(event);
|
|
4020
|
+
if (matched.reason) return this.#unsafeSseFrame(frame, matched.reason);
|
|
4021
|
+
if (matched.state) {
|
|
4022
|
+
if (matched.state.argumentsDone) {
|
|
4023
|
+
return this.#unsafeSseFrame(frame, "duplicate special tool call arguments done");
|
|
4024
|
+
}
|
|
4025
|
+
if (matched.state.kind === "tool_search") {
|
|
4026
|
+
const argumentsObject = toolSearchArguments(event.arguments, false);
|
|
4027
|
+
if (!argumentsObject) {
|
|
4028
|
+
return this.#unsafeSseFrame(frame, "invalid tool search arguments done");
|
|
4029
|
+
}
|
|
4030
|
+
const argumentsFingerprint = canonicalJsonFingerprint(argumentsObject);
|
|
4031
|
+
matched.state.argumentsDone = true;
|
|
4032
|
+
matched.state.finalArgumentsLength = argumentsFingerprint.length;
|
|
4033
|
+
matched.state.finalArgumentsDigest = argumentsFingerprint.digest;
|
|
4034
|
+
this.#commitSemanticMutation();
|
|
4035
|
+
return [];
|
|
4036
|
+
}
|
|
4037
|
+
if (matched.state.kind === "function_codec") {
|
|
4038
|
+
const rewrittenArguments = matched.state.functionRelay?.rewriteArguments(event.arguments);
|
|
4039
|
+
if (typeof rewrittenArguments !== "string") {
|
|
4040
|
+
return this.#unsafeSseFrame(frame, "invalid function relay arguments done");
|
|
4041
|
+
}
|
|
4042
|
+
const codecReason = this.#validateCodecSource(matched.state, event.arguments);
|
|
4043
|
+
if (codecReason) return this.#unsafeSseFrame(frame, codecReason);
|
|
4044
|
+
const argumentsFingerprint = stringFingerprint(rewrittenArguments);
|
|
4045
|
+
event = { ...event, arguments: rewrittenArguments };
|
|
4046
|
+
matched.state.argumentsDone = true;
|
|
4047
|
+
matched.state.finalArgumentsLength = argumentsFingerprint.length;
|
|
4048
|
+
matched.state.finalArgumentsDigest = argumentsFingerprint.digest;
|
|
4049
|
+
matched.state.deltaHash = undefined;
|
|
4050
|
+
matched.state.deltaState = undefined;
|
|
4051
|
+
changed = true;
|
|
4052
|
+
} else {
|
|
4053
|
+
const argumentProperty =
|
|
4054
|
+
matched.state.sourceType === "custom_tool_call"
|
|
4055
|
+
? LITELLM_CUSTOM_TOOL_INPUT_PROPERTY
|
|
4056
|
+
: CUSTOM_TOOL_INPUT_PROPERTY;
|
|
4057
|
+
const input = matched.state.sourceType === "custom_tool_call" && !matched.state.codec
|
|
4058
|
+
? litellmCustomToolInput(event.arguments)
|
|
4059
|
+
: customToolInput(event.arguments, false, argumentProperty, matched.state.codec);
|
|
4060
|
+
if (input === undefined) {
|
|
4061
|
+
return this.#unsafeSseFrame(frame, "invalid custom tool arguments done");
|
|
4062
|
+
}
|
|
4063
|
+
const codecReason = this.#validateCodecSource(matched.state, event.arguments);
|
|
4064
|
+
if (codecReason) return this.#unsafeSseFrame(frame, codecReason);
|
|
4065
|
+
const inputFingerprint = stringFingerprint(input);
|
|
4066
|
+
const deltaReason = this.#customDeltaMismatch(
|
|
4067
|
+
matched.state,
|
|
4068
|
+
inputFingerprint,
|
|
4069
|
+
);
|
|
4070
|
+
if (deltaReason) return this.#unsafeSseFrame(frame, deltaReason);
|
|
4071
|
+
const { arguments: _arguments, ...rest } = event;
|
|
4072
|
+
event = {
|
|
4073
|
+
...rest,
|
|
4074
|
+
type: "response.custom_tool_call_input.done",
|
|
4075
|
+
input,
|
|
4076
|
+
};
|
|
4077
|
+
matched.state.argumentsDone = true;
|
|
4078
|
+
matched.state.finalInputLength = inputFingerprint.length;
|
|
4079
|
+
matched.state.finalInputDigest = inputFingerprint.digest;
|
|
4080
|
+
matched.state.deltaHash = undefined;
|
|
4081
|
+
matched.state.deltaState = undefined;
|
|
4082
|
+
changed = true;
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
if (!this.#injectOnly) {
|
|
4087
|
+
const next = rewriteNamespaceResponsePayload(event, this.#lookups, this.#sessionModel);
|
|
4088
|
+
if (next) {
|
|
4089
|
+
event = next;
|
|
4090
|
+
changed = true;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
if (sourceEvent?.type === "response.output_item.added") {
|
|
4094
|
+
const reason = this.#registerCall(sourceEvent.item, event.item);
|
|
4095
|
+
if (reason) return this.#unsafeSseFrame(frame, reason);
|
|
4096
|
+
}
|
|
4097
|
+
if (sourceEvent?.type === "response.output_item.done") {
|
|
4098
|
+
const reason = this.#closeOutputItem(sourceEvent.item, event.item);
|
|
4099
|
+
if (reason) return this.#unsafeSseFrame(frame, reason);
|
|
4100
|
+
}
|
|
4101
|
+
const terminalEvent =
|
|
4102
|
+
event?.type === "response.completed" ||
|
|
4103
|
+
event?.type === "response.done" ||
|
|
4104
|
+
eventName === "response.completed" ||
|
|
4105
|
+
eventName === "response.done";
|
|
4106
|
+
const summaryReason = this.#validateOutputItems(sourceEvent, event, {
|
|
4107
|
+
allowAtomic: terminalEvent,
|
|
4108
|
+
});
|
|
4109
|
+
if (summaryReason) return this.#unsafeSseFrame(frame, summaryReason);
|
|
4110
|
+
if (terminalEvent) {
|
|
4111
|
+
if (this.#hasOpenSpecialCalls()) {
|
|
4112
|
+
return this.#unsafeSseFrame(frame, "terminal event before special tool call close");
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
this.#observeEvent(event);
|
|
4116
|
+
const replacements = [];
|
|
4117
|
+
if (
|
|
4118
|
+
eventLine &&
|
|
4119
|
+
typeof event?.type === "string" &&
|
|
4120
|
+
event.type !== originalEventType
|
|
4121
|
+
) {
|
|
4122
|
+
replacements.push([eventLine, `event: ${event.type}`]);
|
|
4123
|
+
}
|
|
4124
|
+
// Inject finished-child interrupts before the response closes so Codex
|
|
4125
|
+
// still executes them as ordinary tool calls in this turn.
|
|
4126
|
+
if (event?.type === "response.completed" || eventName === "response.completed") {
|
|
4127
|
+
const interruptBlocks = this.#drainInterruptBlocks();
|
|
4128
|
+
const withOutput = this.#mergeInjectedIntoCompleted(event);
|
|
4129
|
+
if (withOutput !== event) {
|
|
4130
|
+
event = withOutput;
|
|
4131
|
+
changed = true;
|
|
4132
|
+
}
|
|
4133
|
+
if (!changed) return [...interruptBlocks, frame];
|
|
4134
|
+
this.#commitSemanticMutation();
|
|
4135
|
+
replacements.push([dataLine, `data: ${JSON.stringify(event)}`]);
|
|
4136
|
+
return [
|
|
4137
|
+
...interruptBlocks,
|
|
4138
|
+
this.#rewrittenSseFrame(frame, replacements),
|
|
4139
|
+
];
|
|
4140
|
+
}
|
|
4141
|
+
if (event?.type === "response.done" || eventName === "response.done") {
|
|
4142
|
+
const interruptBlocks = this.#drainInterruptBlocks();
|
|
4143
|
+
if (!changed) return [...interruptBlocks, frame];
|
|
4144
|
+
this.#commitSemanticMutation();
|
|
4145
|
+
replacements.push([dataLine, `data: ${JSON.stringify(event)}`]);
|
|
4146
|
+
return [
|
|
4147
|
+
...interruptBlocks,
|
|
4148
|
+
this.#rewrittenSseFrame(frame, replacements),
|
|
4149
|
+
];
|
|
4150
|
+
}
|
|
4151
|
+
if (!changed) return [frame];
|
|
4152
|
+
this.#commitSemanticMutation();
|
|
4153
|
+
replacements.push([dataLine, `data: ${JSON.stringify(event)}`]);
|
|
4154
|
+
return [this.#rewrittenSseFrame(frame, replacements)];
|
|
4155
|
+
} catch (error) {
|
|
4156
|
+
if (error instanceof NamespaceRelayCommittedStreamError) throw error;
|
|
4157
|
+
return this.#unsafeSseFrame(frame, "event rewrite failure");
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
#observeEvent(event) {
|
|
4162
|
+
if (!event || typeof event !== "object") return;
|
|
4163
|
+
this.#lastSequence = nextSequence(event, this.#lastSequence);
|
|
4164
|
+
trackInterruptFromItem(event.item, this.#interruptedTargets);
|
|
4165
|
+
if (Array.isArray(event.output)) {
|
|
4166
|
+
for (const item of event.output) trackInterruptFromItem(item, this.#interruptedTargets);
|
|
4167
|
+
}
|
|
4168
|
+
if (Array.isArray(event.response?.output)) {
|
|
4169
|
+
for (const item of event.response.output) {
|
|
4170
|
+
trackInterruptFromItem(item, this.#interruptedTargets);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
#remainingInterrupts() {
|
|
4176
|
+
return filterAlreadyInterrupted(this.#pendingInterrupts, this.#interruptedTargets);
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
#drainInterruptBlocks() {
|
|
4180
|
+
if (this.#injectionsDone) return [];
|
|
4181
|
+
const remaining = this.#remainingInterrupts();
|
|
4182
|
+
if (!remaining.length) {
|
|
4183
|
+
this.#injectionsDone = true;
|
|
4184
|
+
this.#lastInjectedCalls = [];
|
|
4185
|
+
return [];
|
|
4186
|
+
}
|
|
4187
|
+
this.#commitSemanticMutation();
|
|
4188
|
+
const blocks = [];
|
|
4189
|
+
const injectedCalls = [];
|
|
4190
|
+
for (const target of remaining) {
|
|
4191
|
+
// Each transform is request-scoped, so a local counter would restart on
|
|
4192
|
+
// every turn and reuse call IDs that remain in Codex conversation history.
|
|
4193
|
+
// Use the same fresh-ID helper as the non-stream injection path instead.
|
|
4194
|
+
const call = buildInterruptAgentCall(target);
|
|
4195
|
+
this.#interruptedTargets.add(target);
|
|
4196
|
+
injectedCalls.push(call);
|
|
4197
|
+
const addedSeq = this.#lastSequence + 1;
|
|
4198
|
+
const doneSeq = this.#lastSequence + 2;
|
|
4199
|
+
this.#lastSequence = doneSeq;
|
|
4200
|
+
const added = {
|
|
4201
|
+
type: "response.output_item.added",
|
|
4202
|
+
sequence_number: addedSeq,
|
|
4203
|
+
item: {
|
|
4204
|
+
type: "function_call",
|
|
4205
|
+
name: call.name,
|
|
4206
|
+
namespace: call.namespace,
|
|
4207
|
+
call_id: call.call_id,
|
|
4208
|
+
arguments: "",
|
|
4209
|
+
},
|
|
4210
|
+
};
|
|
4211
|
+
const done = {
|
|
4212
|
+
type: "response.output_item.done",
|
|
4213
|
+
sequence_number: doneSeq,
|
|
4214
|
+
item: {
|
|
4215
|
+
type: "function_call",
|
|
4216
|
+
name: call.name,
|
|
4217
|
+
namespace: call.namespace,
|
|
4218
|
+
call_id: call.call_id,
|
|
4219
|
+
arguments: call.arguments,
|
|
4220
|
+
},
|
|
4221
|
+
};
|
|
4222
|
+
blocks.push(
|
|
4223
|
+
Buffer.from(
|
|
4224
|
+
`event: response.output_item.added${this.#sseLineEnding}` +
|
|
4225
|
+
`data: ${JSON.stringify(added)}${this.#sseLineEnding}${this.#sseLineEnding}`,
|
|
4226
|
+
"utf8",
|
|
4227
|
+
),
|
|
4228
|
+
);
|
|
4229
|
+
blocks.push(
|
|
4230
|
+
Buffer.from(
|
|
4231
|
+
`event: response.output_item.done${this.#sseLineEnding}` +
|
|
4232
|
+
`data: ${JSON.stringify(done)}${this.#sseLineEnding}${this.#sseLineEnding}`,
|
|
4233
|
+
"utf8",
|
|
4234
|
+
),
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
this.#lastInjectedCalls = injectedCalls;
|
|
4238
|
+
this.#injectionsDone = true;
|
|
4239
|
+
return blocks;
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
#mergeInjectedIntoCompleted(event) {
|
|
4243
|
+
// drainInterruptBlocks already marked targets interrupted and emitted the
|
|
4244
|
+
// SSE tool calls. Mirror those calls into response.completed.output so
|
|
4245
|
+
// non-incremental consumers still see them.
|
|
4246
|
+
if (!event || typeof event !== "object") return event;
|
|
4247
|
+
const injected = this.#lastInjectedCalls;
|
|
4248
|
+
if (!Array.isArray(injected) || !injected.length) return event;
|
|
4249
|
+
if (Array.isArray(event.response?.output)) {
|
|
4250
|
+
return {
|
|
4251
|
+
...event,
|
|
4252
|
+
response: {
|
|
4253
|
+
...event.response,
|
|
4254
|
+
output: [...event.response.output, ...injected],
|
|
4255
|
+
},
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
if (Array.isArray(event.output)) {
|
|
4259
|
+
return { ...event, output: [...event.output, ...injected] };
|
|
4260
|
+
}
|
|
4261
|
+
return event;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
#injectJsonInterrupts(payload) {
|
|
4265
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
4266
|
+
// Non-streaming Responses put completed function calls in `output`.
|
|
4267
|
+
if (Array.isArray(payload.output)) {
|
|
4268
|
+
for (const item of payload.output) trackInterruptFromItem(item, this.#interruptedTargets);
|
|
4269
|
+
const result = appendInterruptCallsToOutput(
|
|
4270
|
+
payload.output,
|
|
4271
|
+
this.#pendingInterrupts,
|
|
4272
|
+
this.#interruptedTargets,
|
|
4273
|
+
);
|
|
4274
|
+
if (result.injected) payload = { ...payload, output: result.output };
|
|
4275
|
+
}
|
|
4276
|
+
if (payload.response && Array.isArray(payload.response.output)) {
|
|
4277
|
+
for (const item of payload.response.output) {
|
|
4278
|
+
trackInterruptFromItem(item, this.#interruptedTargets);
|
|
4279
|
+
}
|
|
4280
|
+
const result = appendInterruptCallsToOutput(
|
|
4281
|
+
payload.response.output,
|
|
4282
|
+
this.#pendingInterrupts,
|
|
4283
|
+
this.#interruptedTargets,
|
|
4284
|
+
);
|
|
4285
|
+
if (result.injected) {
|
|
4286
|
+
payload = {
|
|
4287
|
+
...payload,
|
|
4288
|
+
response: { ...payload.response, output: result.output },
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
return payload;
|
|
4293
|
+
}
|
|
4294
|
+
}
|