@jsonstudio/llms 0.6.215 → 0.6.230
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/dist/conversion/codecs/gemini-openai-codec.js +83 -1
- package/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-web-search.js +66 -0
- package/dist/conversion/compat/profiles/chat-glm.json +4 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +9 -1
- package/dist/conversion/hub/process/chat-process.js +131 -1
- package/dist/conversion/hub/response/provider-response.d.ts +22 -0
- package/dist/conversion/hub/response/provider-response.js +12 -1
- package/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
- package/dist/conversion/hub/response/server-side-tools.js +326 -0
- package/dist/conversion/hub/types/standardized.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.js +49 -3
- package/dist/conversion/shared/tool-mapping.js +25 -2
- package/dist/router/virtual-router/bootstrap.js +273 -40
- package/dist/router/virtual-router/context-advisor.d.ts +0 -2
- package/dist/router/virtual-router/context-advisor.js +0 -12
- package/dist/router/virtual-router/engine.d.ts +7 -2
- package/dist/router/virtual-router/engine.js +161 -82
- package/dist/router/virtual-router/types.d.ts +21 -2
- package/dist/sse/json-to-sse/event-generators/responses.js +15 -3
- package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
- package/dist/sse/types/gemini-types.d.ts +20 -1
- package/dist/sse/types/responses-types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { buildOpenAIChatFromGeminiResponse } from '../../codecs/gemini-openai-codec.js';
|
|
2
|
+
function asObject(value) {
|
|
3
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
4
|
+
}
|
|
5
|
+
function getArray(value) {
|
|
6
|
+
return Array.isArray(value) ? value : [];
|
|
7
|
+
}
|
|
8
|
+
function extractToolCalls(chatResponse) {
|
|
9
|
+
const choices = getArray(chatResponse.choices);
|
|
10
|
+
const calls = [];
|
|
11
|
+
for (const choice of choices) {
|
|
12
|
+
const choiceObj = asObject(choice);
|
|
13
|
+
if (!choiceObj)
|
|
14
|
+
continue;
|
|
15
|
+
const message = asObject(choiceObj.message);
|
|
16
|
+
if (!message)
|
|
17
|
+
continue;
|
|
18
|
+
const toolCalls = getArray(message.tool_calls);
|
|
19
|
+
for (const raw of toolCalls) {
|
|
20
|
+
const tc = asObject(raw);
|
|
21
|
+
if (!tc)
|
|
22
|
+
continue;
|
|
23
|
+
const id = typeof tc.id === 'string' && tc.id.trim() ? tc.id.trim() : '';
|
|
24
|
+
const fn = asObject(tc.function);
|
|
25
|
+
const name = fn && typeof fn.name === 'string' && fn.name.trim() ? fn.name.trim() : '';
|
|
26
|
+
const args = fn && typeof fn.arguments === 'string' ? fn.arguments : '';
|
|
27
|
+
if (!id || !name)
|
|
28
|
+
continue;
|
|
29
|
+
calls.push({ id, name, arguments: args });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return calls;
|
|
33
|
+
}
|
|
34
|
+
function extractTextFromChatLike(payload) {
|
|
35
|
+
const choices = getArray(payload.choices);
|
|
36
|
+
if (!choices.length)
|
|
37
|
+
return '';
|
|
38
|
+
const first = asObject(choices[0]);
|
|
39
|
+
if (!first)
|
|
40
|
+
return '';
|
|
41
|
+
const message = asObject(first.message);
|
|
42
|
+
if (!message)
|
|
43
|
+
return '';
|
|
44
|
+
const content = message.content;
|
|
45
|
+
if (typeof content === 'string')
|
|
46
|
+
return content;
|
|
47
|
+
const parts = getArray(content);
|
|
48
|
+
const texts = [];
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
if (typeof part === 'string') {
|
|
51
|
+
texts.push(part);
|
|
52
|
+
}
|
|
53
|
+
else if (part && typeof part === 'object') {
|
|
54
|
+
const record = part;
|
|
55
|
+
if (typeof record.text === 'string') {
|
|
56
|
+
texts.push(record.text);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return texts.join('\n').trim();
|
|
61
|
+
}
|
|
62
|
+
function getWebSearchConfig(ctx) {
|
|
63
|
+
const raw = ctx.webSearch;
|
|
64
|
+
const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
|
|
65
|
+
if (!record)
|
|
66
|
+
return undefined;
|
|
67
|
+
const enginesRaw = Array.isArray(record.engines) ? record.engines : [];
|
|
68
|
+
const engines = [];
|
|
69
|
+
for (const entry of enginesRaw) {
|
|
70
|
+
const obj = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
|
|
71
|
+
if (!obj)
|
|
72
|
+
continue;
|
|
73
|
+
const id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : undefined;
|
|
74
|
+
const providerKey = typeof obj.providerKey === 'string' && obj.providerKey.trim()
|
|
75
|
+
? obj.providerKey.trim()
|
|
76
|
+
: undefined;
|
|
77
|
+
if (!id || !providerKey)
|
|
78
|
+
continue;
|
|
79
|
+
engines.push({
|
|
80
|
+
id,
|
|
81
|
+
providerKey,
|
|
82
|
+
description: typeof obj.description === 'string' && obj.description.trim() ? obj.description.trim() : undefined,
|
|
83
|
+
default: obj.default === true
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (!engines.length) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const config = { engines };
|
|
90
|
+
if (typeof record.injectPolicy === 'string') {
|
|
91
|
+
const val = String(record.injectPolicy).trim().toLowerCase();
|
|
92
|
+
if (val === 'always' || val === 'selective') {
|
|
93
|
+
config.injectPolicy = val;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
function resolveWebSearchEngine(config, engineId) {
|
|
99
|
+
const trimmedId = engineId && typeof engineId === 'string' && engineId.trim() ? engineId.trim() : undefined;
|
|
100
|
+
if (trimmedId) {
|
|
101
|
+
const byId = config.engines.find((e) => e.id === trimmedId);
|
|
102
|
+
if (byId) {
|
|
103
|
+
return byId;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const byDefault = config.engines.find((e) => e.default);
|
|
107
|
+
if (byDefault) {
|
|
108
|
+
return byDefault;
|
|
109
|
+
}
|
|
110
|
+
if (config.engines.length === 1) {
|
|
111
|
+
return config.engines[0];
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function isGeminiWebSearchEngine(engine) {
|
|
116
|
+
const key = engine.providerKey.toLowerCase();
|
|
117
|
+
return key.startsWith('gemini-cli.') || key.startsWith('antigravity.');
|
|
118
|
+
}
|
|
119
|
+
function logServerToolWebSearch(engine, requestId, query) {
|
|
120
|
+
const providerAlias = engine.providerKey.split('.')[0] || engine.providerKey;
|
|
121
|
+
const backendLabel = `${providerAlias}:${engine.id}`;
|
|
122
|
+
const prefix = `[server-tool][web_search][${backendLabel}]`;
|
|
123
|
+
const line = `${prefix} requestId=${requestId} query=${JSON.stringify(query)}`;
|
|
124
|
+
// 深蓝色输出
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.log(`\x1b[38;5;27m${line}\x1b[0m`);
|
|
127
|
+
}
|
|
128
|
+
function resolveEnvServerSideToolsEnabled() {
|
|
129
|
+
const raw = (process.env.ROUTECODEX_SERVER_SIDE_TOOLS || process.env.RCC_SERVER_SIDE_TOOLS || '').trim().toLowerCase();
|
|
130
|
+
if (!raw)
|
|
131
|
+
return false;
|
|
132
|
+
if (raw === '1' || raw === 'true' || raw === 'yes')
|
|
133
|
+
return true;
|
|
134
|
+
if (raw === 'web_search' || raw === 'websearch')
|
|
135
|
+
return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
export async function runServerSideToolEngine(options) {
|
|
139
|
+
const base = asObject(options.chatResponse);
|
|
140
|
+
if (!base) {
|
|
141
|
+
return { mode: 'passthrough', finalChatResponse: options.chatResponse };
|
|
142
|
+
}
|
|
143
|
+
// 内建 OpenAI Responses `/v1/responses` 已经支持 server-side web_search。
|
|
144
|
+
// 仅当“入口端点为 /v1/responses 且 providerProtocol 也是 openai-responses”时保持透传,
|
|
145
|
+
// 避免对 Responses→Responses 的链路重复执行搜索回环;
|
|
146
|
+
// 其它场景(例如 /v1/responses → gemini/glm 后端)仍允许 server-side web_search 生效。
|
|
147
|
+
const entry = (options.entryEndpoint || '').toLowerCase();
|
|
148
|
+
if (options.providerProtocol === 'openai-responses' && entry.includes('/v1/responses')) {
|
|
149
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
150
|
+
}
|
|
151
|
+
// Feature flag: keep behaviour fully backwards-compatible unless explicitly enabled.
|
|
152
|
+
const toolsEnabled = resolveEnvServerSideToolsEnabled();
|
|
153
|
+
const toolCalls = extractToolCalls(base);
|
|
154
|
+
const webSearchCall = toolCalls.find((tc) => tc.name === 'web_search');
|
|
155
|
+
if (!toolsEnabled || !webSearchCall || !options.providerInvoker) {
|
|
156
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
157
|
+
}
|
|
158
|
+
const webSearchConfig = getWebSearchConfig(options.adapterContext);
|
|
159
|
+
if (!webSearchConfig) {
|
|
160
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
161
|
+
}
|
|
162
|
+
let parsedArgs = {};
|
|
163
|
+
if (webSearchCall.arguments && typeof webSearchCall.arguments === 'string') {
|
|
164
|
+
try {
|
|
165
|
+
parsedArgs = JSON.parse(webSearchCall.arguments);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
parsedArgs = {};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const query = typeof parsedArgs?.query === 'string' && parsedArgs.query.trim()
|
|
172
|
+
? parsedArgs.query.trim()
|
|
173
|
+
: undefined;
|
|
174
|
+
const engineId = typeof parsedArgs?.engine === 'string' && parsedArgs.engine.trim()
|
|
175
|
+
? parsedArgs.engine.trim()
|
|
176
|
+
: undefined;
|
|
177
|
+
const recency = typeof parsedArgs?.recency === 'string' && parsedArgs.recency.trim()
|
|
178
|
+
? parsedArgs.recency.trim()
|
|
179
|
+
: undefined;
|
|
180
|
+
const count = typeof parsedArgs?.count === 'number' && Number.isFinite(parsedArgs.count)
|
|
181
|
+
? Math.floor(parsedArgs.count)
|
|
182
|
+
: undefined;
|
|
183
|
+
if (!query) {
|
|
184
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
185
|
+
}
|
|
186
|
+
const engine = resolveWebSearchEngine(webSearchConfig, engineId);
|
|
187
|
+
if (!engine) {
|
|
188
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
189
|
+
}
|
|
190
|
+
const providerInvoker = options.providerInvoker;
|
|
191
|
+
// 1) Call search backend (secondary request)
|
|
192
|
+
let searchSummary = '';
|
|
193
|
+
try {
|
|
194
|
+
logServerToolWebSearch(engine, options.requestId, query);
|
|
195
|
+
if (isGeminiWebSearchEngine(engine)) {
|
|
196
|
+
const geminiPayload = {
|
|
197
|
+
model: engine.id,
|
|
198
|
+
contents: [
|
|
199
|
+
{
|
|
200
|
+
role: 'user',
|
|
201
|
+
parts: [
|
|
202
|
+
{
|
|
203
|
+
text: query
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
// Cloud Code search models treat googleSearch as the web search tool.
|
|
209
|
+
tools: [
|
|
210
|
+
{
|
|
211
|
+
googleSearch: {}
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
const backend = await providerInvoker({
|
|
216
|
+
providerKey: engine.providerKey,
|
|
217
|
+
providerType: undefined,
|
|
218
|
+
modelId: engine.id,
|
|
219
|
+
providerProtocol: 'gemini-chat',
|
|
220
|
+
payload: geminiPayload,
|
|
221
|
+
entryEndpoint: '/v1/models/gemini:generateContent',
|
|
222
|
+
requestId: `${options.requestId}:web_search:${engine.id}`
|
|
223
|
+
});
|
|
224
|
+
const backendResponse = asObject(backend.providerResponse);
|
|
225
|
+
if (backendResponse) {
|
|
226
|
+
const chatLike = asObject(buildOpenAIChatFromGeminiResponse(backendResponse));
|
|
227
|
+
if (chatLike) {
|
|
228
|
+
searchSummary = extractTextFromChatLike(chatLike);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const backendPayload = {
|
|
234
|
+
model: engine.providerKey,
|
|
235
|
+
messages: [
|
|
236
|
+
{
|
|
237
|
+
role: 'system',
|
|
238
|
+
content: 'You are a web search engine. Answer with up-to-date information based on the open internet.'
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
role: 'user',
|
|
242
|
+
content: query
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
stream: false,
|
|
246
|
+
web_search: {
|
|
247
|
+
query,
|
|
248
|
+
...(recency ? { recency } : {}),
|
|
249
|
+
...(typeof count === 'number' ? { count } : {}),
|
|
250
|
+
engine: engine.id
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const backend = await providerInvoker({
|
|
254
|
+
providerKey: engine.providerKey,
|
|
255
|
+
providerType: undefined,
|
|
256
|
+
modelId: undefined,
|
|
257
|
+
providerProtocol: options.providerProtocol,
|
|
258
|
+
payload: backendPayload,
|
|
259
|
+
entryEndpoint: '/v1/chat/completions',
|
|
260
|
+
requestId: `${options.requestId}:web_search:${engine.id}`
|
|
261
|
+
});
|
|
262
|
+
const backendResponse = asObject(backend.providerResponse);
|
|
263
|
+
if (backendResponse) {
|
|
264
|
+
searchSummary = extractTextFromChatLike(backendResponse);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// fall back to passthrough if backend fails
|
|
270
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
271
|
+
}
|
|
272
|
+
if (!searchSummary) {
|
|
273
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
274
|
+
}
|
|
275
|
+
// 2) Call main provider again with synthesized prompt (third request)
|
|
276
|
+
const ctxTarget = options.adapterContext.target;
|
|
277
|
+
const target = ctxTarget && typeof ctxTarget === 'object' && !Array.isArray(ctxTarget)
|
|
278
|
+
? ctxTarget
|
|
279
|
+
: undefined;
|
|
280
|
+
const mainProviderKey = typeof target?.providerKey === 'string' && target.providerKey.trim()
|
|
281
|
+
? target.providerKey.trim()
|
|
282
|
+
: undefined;
|
|
283
|
+
const mainModelId = (typeof base.model === 'string' && base.model.trim()
|
|
284
|
+
? base.model.trim()
|
|
285
|
+
: typeof target?.modelId === 'string' && target.modelId.trim()
|
|
286
|
+
? target.modelId.trim()
|
|
287
|
+
: undefined) ?? 'gpt-4o-mini';
|
|
288
|
+
if (!mainProviderKey) {
|
|
289
|
+
return { mode: 'passthrough', finalChatResponse: base };
|
|
290
|
+
}
|
|
291
|
+
let finalResponse = base;
|
|
292
|
+
try {
|
|
293
|
+
const followupPayload = {
|
|
294
|
+
model: mainModelId,
|
|
295
|
+
messages: [
|
|
296
|
+
{
|
|
297
|
+
role: 'system',
|
|
298
|
+
content: 'You are an assistant that answers using the provided web_search results. Use them as the primary source.'
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
role: 'user',
|
|
302
|
+
content: `User question: ${query}\n\nWeb search results:\n${searchSummary}`
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
stream: false
|
|
306
|
+
};
|
|
307
|
+
const followup = await providerInvoker({
|
|
308
|
+
providerKey: mainProviderKey,
|
|
309
|
+
providerType: undefined,
|
|
310
|
+
modelId: mainModelId,
|
|
311
|
+
providerProtocol: options.providerProtocol,
|
|
312
|
+
payload: followupPayload,
|
|
313
|
+
entryEndpoint: '/v1/chat/completions',
|
|
314
|
+
requestId: `${options.requestId}:web_search_followup`
|
|
315
|
+
});
|
|
316
|
+
const followupResponse = asObject(followup.providerResponse);
|
|
317
|
+
if (followupResponse) {
|
|
318
|
+
finalResponse = followupResponse;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// If follow-up fails, keep original base response to avoid breaking clients.
|
|
323
|
+
finalResponse = base;
|
|
324
|
+
}
|
|
325
|
+
return { mode: 'web_search_flow', finalChatResponse: finalResponse };
|
|
326
|
+
}
|
|
@@ -146,17 +146,63 @@ function normalizeBridgeHistory(seed) {
|
|
|
146
146
|
originalSystemMessages: systemMessages
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
|
+
function mergeResponsesTools(originalTools, fromChat) {
|
|
150
|
+
const result = [];
|
|
151
|
+
const byKey = new Map();
|
|
152
|
+
const norm = (value) => {
|
|
153
|
+
if (typeof value !== 'string')
|
|
154
|
+
return undefined;
|
|
155
|
+
const trimmed = value.trim();
|
|
156
|
+
return trimmed.length ? trimmed.toLowerCase() : undefined;
|
|
157
|
+
};
|
|
158
|
+
const register = (tool) => {
|
|
159
|
+
const fn = tool.function ?? tool.function;
|
|
160
|
+
const baseName = norm(fn?.name ?? tool.name);
|
|
161
|
+
const typeName = norm(tool.type);
|
|
162
|
+
const key = baseName || typeName;
|
|
163
|
+
if (!key || byKey.has(key))
|
|
164
|
+
return;
|
|
165
|
+
byKey.set(key, tool);
|
|
166
|
+
result.push(tool);
|
|
167
|
+
};
|
|
168
|
+
if (Array.isArray(fromChat)) {
|
|
169
|
+
for (const t of fromChat) {
|
|
170
|
+
if (t && typeof t === 'object') {
|
|
171
|
+
register(t);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(originalTools)) {
|
|
176
|
+
for (const t of originalTools) {
|
|
177
|
+
if (!t || typeof t !== 'object')
|
|
178
|
+
continue;
|
|
179
|
+
const typeName = norm(t.type);
|
|
180
|
+
const isWebSearch = typeName === 'web_search' || (typeof typeName === 'string' && typeName.startsWith('web_search'));
|
|
181
|
+
if (!isWebSearch) {
|
|
182
|
+
// 目前仅恢复原始载荷中的 builtin web_search 工具,其它非函数工具保持忽略,避免意外改变行为。
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
register(t);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result.length ? result : undefined;
|
|
189
|
+
}
|
|
149
190
|
export function buildResponsesRequestFromChat(payload, ctx, extras) {
|
|
150
191
|
const chat = unwrapData(payload);
|
|
151
192
|
const out = {};
|
|
152
193
|
// 基本字段
|
|
153
194
|
out.model = chat.model;
|
|
154
195
|
// tools: 反向映射为 ResponsesToolDefinition 形状
|
|
155
|
-
const
|
|
196
|
+
const responsesToolsFromChat = mapChatToolsToBridge(chat.tools, {
|
|
156
197
|
sanitizeName: sanitizeResponsesFunctionName
|
|
157
198
|
});
|
|
158
|
-
if
|
|
159
|
-
|
|
199
|
+
// Prefer Chat‑normalized tools, but if the original Responses payload carried
|
|
200
|
+
// non‑function tools (such as builtin `web_search`), merge them back so that
|
|
201
|
+
// upstream `/v1/responses` providers see their original tool definitions.
|
|
202
|
+
const originalTools = Array.isArray(ctx?.toolsRaw) ? ctx.toolsRaw : undefined;
|
|
203
|
+
const mergedTools = mergeResponsesTools(originalTools, responsesToolsFromChat);
|
|
204
|
+
if (mergedTools?.length) {
|
|
205
|
+
out.tools = mergedTools;
|
|
160
206
|
}
|
|
161
207
|
const passthroughKeys = [
|
|
162
208
|
'tool_choice',
|
|
@@ -56,6 +56,19 @@ function enforceBuiltinToolSchema(name, candidate) {
|
|
|
56
56
|
const base = asSchema(candidate);
|
|
57
57
|
return ensureApplyPatchSchema(base);
|
|
58
58
|
}
|
|
59
|
+
if (normalizedName === 'web_search') {
|
|
60
|
+
// For web_search we currently accept any incoming schema and fall back to a
|
|
61
|
+
// minimal object definition. Server-side web_search execution only relies
|
|
62
|
+
// on the function name + JSON arguments, so tool schema is best-effort.
|
|
63
|
+
const base = asSchema(candidate) ?? {};
|
|
64
|
+
if (!base.type) {
|
|
65
|
+
base.type = 'object';
|
|
66
|
+
}
|
|
67
|
+
if (!Object.prototype.hasOwnProperty.call(base, 'properties')) {
|
|
68
|
+
base.properties = {};
|
|
69
|
+
}
|
|
70
|
+
return base;
|
|
71
|
+
}
|
|
59
72
|
return asSchema(candidate);
|
|
60
73
|
}
|
|
61
74
|
const DEFAULT_SANITIZER = (value) => {
|
|
@@ -111,10 +124,20 @@ export function bridgeToolToChatDefinition(rawTool, options) {
|
|
|
111
124
|
}
|
|
112
125
|
const tool = rawTool;
|
|
113
126
|
const fnNode = tool.function && typeof tool.function === 'object' ? tool.function : undefined;
|
|
114
|
-
|
|
127
|
+
let name = pickToolName([fnNode?.name, tool.name], options);
|
|
128
|
+
// Special case for Responses builtin web_search tools:
|
|
129
|
+
// Codex / Claude‑code may send tools shaped as `{ type: "web_search", ... }`
|
|
130
|
+
// without a nested `function` node. Treat these as a canonical `web_search`
|
|
131
|
+
// function tool so downstream Chat / Standardized layers can reason over a
|
|
132
|
+
// single function-style web_search surface.
|
|
115
133
|
if (!name) {
|
|
116
|
-
|
|
134
|
+
const rawType = typeof tool.type === 'string' ? tool.type.trim().toLowerCase() : '';
|
|
135
|
+
if (rawType === 'web_search' || rawType.startsWith('web_search')) {
|
|
136
|
+
name = 'web_search';
|
|
137
|
+
}
|
|
117
138
|
}
|
|
139
|
+
if (!name)
|
|
140
|
+
return null;
|
|
118
141
|
const description = resolveToolDescription(fnNode?.description ?? tool.description);
|
|
119
142
|
const parameters = enforceBuiltinToolSchema(name, resolveToolParameters(fnNode, tool));
|
|
120
143
|
const strict = resolveToolStrict(fnNode, tool);
|