@jsonstudio/llms 0.6.230 → 0.6.467

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +2 -0
  2. package/dist/conversion/codecs/gemini-openai-codec.js +24 -2
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  4. package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  5. package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  6. package/dist/conversion/compat/actions/glm-image-content.js +83 -0
  7. package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  8. package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  9. package/dist/conversion/compat/actions/glm-web-search.js +25 -28
  10. package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
  11. package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
  12. package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  13. package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  14. package/dist/conversion/compat/profiles/chat-glm.json +194 -184
  15. package/dist/conversion/compat/profiles/chat-iflow.json +199 -195
  16. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  17. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  18. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  19. package/dist/conversion/config/sample-config.json +1 -1
  20. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  21. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline.js +32 -1
  23. package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
  25. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
  26. package/dist/conversion/hub/pipeline/target-utils.js +6 -0
  27. package/dist/conversion/hub/process/chat-process.js +186 -40
  28. package/dist/conversion/hub/response/provider-response.d.ts +13 -1
  29. package/dist/conversion/hub/response/provider-response.js +84 -35
  30. package/dist/conversion/hub/response/server-side-tools.js +61 -4
  31. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
  32. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  33. package/dist/conversion/hub/standardized-bridge.js +14 -0
  34. package/dist/conversion/responses/responses-openai-bridge.js +110 -6
  35. package/dist/conversion/shared/anthropic-message-utils.js +133 -9
  36. package/dist/conversion/shared/bridge-message-utils.js +137 -10
  37. package/dist/conversion/shared/errors.d.ts +20 -0
  38. package/dist/conversion/shared/errors.js +28 -0
  39. package/dist/conversion/shared/responses-conversation-store.js +30 -3
  40. package/dist/conversion/shared/responses-output-builder.js +111 -8
  41. package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  42. package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +103 -3
  44. package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
  45. package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
  46. package/dist/router/virtual-router/bootstrap.js +44 -12
  47. package/dist/router/virtual-router/classifier.js +13 -17
  48. package/dist/router/virtual-router/engine.d.ts +39 -0
  49. package/dist/router/virtual-router/engine.js +755 -55
  50. package/dist/router/virtual-router/features.js +1 -1
  51. package/dist/router/virtual-router/message-utils.js +36 -24
  52. package/dist/router/virtual-router/provider-registry.d.ts +15 -0
  53. package/dist/router/virtual-router/provider-registry.js +42 -1
  54. package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
  55. package/dist/router/virtual-router/routing-instructions.js +383 -0
  56. package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
  57. package/dist/router/virtual-router/sticky-session-store.js +110 -0
  58. package/dist/router/virtual-router/token-counter.js +14 -3
  59. package/dist/router/virtual-router/tool-signals.js +0 -22
  60. package/dist/router/virtual-router/types.d.ts +80 -0
  61. package/dist/router/virtual-router/types.js +2 -1
  62. package/dist/servertool/engine.d.ts +27 -0
  63. package/dist/servertool/engine.js +101 -0
  64. package/dist/servertool/flow-types.d.ts +40 -0
  65. package/dist/servertool/flow-types.js +1 -0
  66. package/dist/servertool/handlers/vision.d.ts +1 -0
  67. package/dist/servertool/handlers/vision.js +194 -0
  68. package/dist/servertool/handlers/web-search.d.ts +1 -0
  69. package/dist/servertool/handlers/web-search.js +791 -0
  70. package/dist/servertool/orchestration-types.d.ts +33 -0
  71. package/dist/servertool/orchestration-types.js +1 -0
  72. package/dist/servertool/registry.d.ts +18 -0
  73. package/dist/servertool/registry.js +27 -0
  74. package/dist/servertool/server-side-tools.d.ts +8 -0
  75. package/dist/servertool/server-side-tools.js +208 -0
  76. package/dist/servertool/types.d.ts +94 -0
  77. package/dist/servertool/types.js +1 -0
  78. package/dist/servertool/vision-tool.d.ts +2 -0
  79. package/dist/servertool/vision-tool.js +185 -0
  80. package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  81. package/package.json +1 -1
@@ -0,0 +1,791 @@
1
+ import { buildOpenAIChatFromGeminiResponse } from '../../conversion/codecs/gemini-openai-codec.js';
2
+ import { registerServerToolHandler } from '../registry.js';
3
+ import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
4
+ const FLOW_ID = 'web_search_flow';
5
+ const handler = async (ctx) => {
6
+ const toolCall = ctx.toolCall;
7
+ if (!toolCall) {
8
+ return null;
9
+ }
10
+ if (!ctx.options.providerInvoker && !ctx.options.reenterPipeline) {
11
+ return null;
12
+ }
13
+ const webSearchConfig = getWebSearchConfig(ctx.adapterContext);
14
+ const hasConfig = !!webSearchConfig && Array.isArray(webSearchConfig.engines) && webSearchConfig.engines.length > 0;
15
+ const forceMode = webSearchConfig?.force === true;
16
+ const envEnabled = resolveEnvServerSideToolsEnabled();
17
+ if (!hasConfig && !forceMode && !envEnabled) {
18
+ return null;
19
+ }
20
+ const parsedArgs = parseToolArguments(toolCall);
21
+ const query = typeof parsedArgs.query === 'string' && parsedArgs.query.trim() ? parsedArgs.query.trim() : undefined;
22
+ if (!query) {
23
+ return null;
24
+ }
25
+ const engines = webSearchConfig ? buildEnginePriorityList(webSearchConfig, parsedArgs.engine) : [];
26
+ if (!engines.length) {
27
+ return null;
28
+ }
29
+ const resultCount = normalizeResultCount(parsedArgs.count);
30
+ let chosenEngine;
31
+ let chosenResult;
32
+ let lastFailure;
33
+ for (const engine of engines) {
34
+ const backendResult = await executeWebSearchBackend({
35
+ ctx,
36
+ engine,
37
+ query,
38
+ recency: parsedArgs.recency,
39
+ resultCount
40
+ });
41
+ if (backendResult.ok) {
42
+ chosenEngine = engine;
43
+ chosenResult = backendResult;
44
+ break;
45
+ }
46
+ lastFailure = { engine, result: backendResult };
47
+ }
48
+ if (!chosenEngine || !chosenResult) {
49
+ if (!lastFailure) {
50
+ return null;
51
+ }
52
+ chosenEngine = lastFailure.engine;
53
+ chosenResult = lastFailure.result;
54
+ }
55
+ const patched = injectWebSearchToolResult(ctx.base, toolCall, chosenEngine, query, chosenResult);
56
+ const followupPayload = buildWebSearchFollowupPayload(ctx.adapterContext, patched);
57
+ const execution = {
58
+ flowId: FLOW_ID,
59
+ followup: followupPayload
60
+ ? {
61
+ requestIdSuffix: ':web_search_followup',
62
+ payload: followupPayload,
63
+ metadata: buildFollowupMetadata(ctx.adapterContext, 'web_search')
64
+ }
65
+ : undefined,
66
+ context: {
67
+ web_search: {
68
+ engineId: chosenEngine.id,
69
+ providerKey: chosenEngine.providerKey,
70
+ summary: chosenResult.summary
71
+ }
72
+ }
73
+ };
74
+ return {
75
+ chatResponse: patched,
76
+ execution
77
+ };
78
+ };
79
+ registerServerToolHandler('web_search', handler);
80
+ function parseToolArguments(toolCall) {
81
+ if (!toolCall.arguments || typeof toolCall.arguments !== 'string') {
82
+ return {};
83
+ }
84
+ try {
85
+ return JSON.parse(toolCall.arguments);
86
+ }
87
+ catch {
88
+ return {};
89
+ }
90
+ }
91
+ function getWebSearchConfig(ctx) {
92
+ const raw = ctx && typeof ctx === 'object' ? ctx.webSearch : undefined;
93
+ const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
94
+ if (!record)
95
+ return undefined;
96
+ const enginesRaw = Array.isArray(record.engines) ? record.engines : [];
97
+ const engines = [];
98
+ for (const entry of enginesRaw) {
99
+ const obj = entry && typeof entry === 'object' && !Array.isArray(entry)
100
+ ? entry
101
+ : null;
102
+ if (!obj)
103
+ continue;
104
+ const id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : undefined;
105
+ const providerKey = typeof obj.providerKey === 'string' && obj.providerKey.trim()
106
+ ? obj.providerKey.trim()
107
+ : undefined;
108
+ if (!id || !providerKey)
109
+ continue;
110
+ const serverToolsDisabled = obj.serverToolsDisabled === true ||
111
+ (typeof obj.serverToolsDisabled === 'string' &&
112
+ obj.serverToolsDisabled.trim().toLowerCase() === 'true') ||
113
+ (obj.serverTools &&
114
+ typeof obj.serverTools === 'object' &&
115
+ obj.serverTools.enabled === false);
116
+ let searchEngineList;
117
+ const rawSearchList = obj.searchEngineList;
118
+ if (Array.isArray(rawSearchList)) {
119
+ const normalizedList = [];
120
+ for (const item of rawSearchList) {
121
+ if (typeof item === 'string' && item.trim().length) {
122
+ normalizedList.push(item.trim());
123
+ }
124
+ }
125
+ if (normalizedList.length) {
126
+ searchEngineList = normalizedList;
127
+ }
128
+ }
129
+ engines.push({
130
+ id,
131
+ providerKey,
132
+ description: typeof obj.description === 'string' && obj.description.trim() ? obj.description.trim() : undefined,
133
+ default: obj.default === true,
134
+ ...(serverToolsDisabled ? { serverToolsDisabled: true } : {}),
135
+ ...(searchEngineList ? { searchEngineList } : {})
136
+ });
137
+ }
138
+ if (!engines.length) {
139
+ return undefined;
140
+ }
141
+ const config = { engines };
142
+ if (typeof record.injectPolicy === 'string') {
143
+ const val = String(record.injectPolicy).trim().toLowerCase();
144
+ if (val === 'always' || val === 'selective') {
145
+ config.injectPolicy = val;
146
+ }
147
+ }
148
+ if (record.force === true ||
149
+ (typeof record.force === 'string' && String(record.force).trim().toLowerCase() === 'true')) {
150
+ config.force = true;
151
+ }
152
+ return config;
153
+ }
154
+ function resolveWebSearchEngine(config, engineId) {
155
+ const trimmedId = typeof engineId === 'string' && engineId.trim() ? engineId.trim() : undefined;
156
+ if (trimmedId) {
157
+ const byId = config.engines.find((e) => e.id === trimmedId);
158
+ if (byId && !byId.serverToolsDisabled) {
159
+ return byId;
160
+ }
161
+ }
162
+ const byDefault = config.engines.find((e) => e.default && !e.serverToolsDisabled);
163
+ if (byDefault && !byDefault.serverToolsDisabled) {
164
+ return byDefault;
165
+ }
166
+ if (config.engines.length === 1) {
167
+ const single = config.engines[0];
168
+ return single.serverToolsDisabled ? undefined : single;
169
+ }
170
+ return undefined;
171
+ }
172
+ function buildEnginePriorityList(config, engineId) {
173
+ const engines = (Array.isArray(config.engines) ? config.engines : []).filter((engine) => !engine.serverToolsDisabled);
174
+ if (!engines.length) {
175
+ return [];
176
+ }
177
+ const primary = resolveWebSearchEngine(config, engineId);
178
+ if (!primary) {
179
+ return [...engines];
180
+ }
181
+ const ordered = [primary];
182
+ for (const engine of engines) {
183
+ if (engine !== primary) {
184
+ ordered.push(engine);
185
+ }
186
+ }
187
+ return ordered;
188
+ }
189
+ function resolveEnvServerSideToolsEnabled() {
190
+ const raw = (process.env.ROUTECODEX_SERVER_SIDE_TOOLS || process.env.RCC_SERVER_SIDE_TOOLS || '').trim().toLowerCase();
191
+ if (!raw)
192
+ return false;
193
+ if (raw === '1' || raw === 'true' || raw === 'yes')
194
+ return true;
195
+ if (raw === 'web_search')
196
+ return true;
197
+ return false;
198
+ }
199
+ function isGeminiWebSearchEngine(engine) {
200
+ const key = engine.providerKey.toLowerCase();
201
+ return (key.startsWith('gemini-cli.') ||
202
+ key.startsWith('antigravity.') ||
203
+ key.startsWith('gemini.'));
204
+ }
205
+ function isIflowWebSearchEngine(engine) {
206
+ const key = engine.providerKey.toLowerCase();
207
+ return key.startsWith('iflow.');
208
+ }
209
+ function normalizeResultCount(value) {
210
+ if (typeof value === 'number' && Number.isFinite(value)) {
211
+ const normalized = Math.trunc(value);
212
+ if (normalized >= 5 && normalized <= 15) {
213
+ return normalized;
214
+ }
215
+ }
216
+ return 10;
217
+ }
218
+ async function executeWebSearchBackend(args) {
219
+ const { ctx, engine, query } = args;
220
+ const recency = typeof args.recency === 'string' && args.recency.trim() ? args.recency.trim() : undefined;
221
+ let summary = '';
222
+ let hits = [];
223
+ let ok = true;
224
+ try {
225
+ logServerToolWebSearch(engine, ctx.options.requestId, query);
226
+ const requestSuffix = `:web_search:${engine.id}`;
227
+ // 对于 iFlow,直接通过 providerInvoker 调用 /chat/retrieve,
228
+ // 即使 reenterPipeline 可用,也不走 Chat 模型 + tools。
229
+ if (isIflowWebSearchEngine(engine) && ctx.options.providerInvoker) {
230
+ const backendResult = await executeIflowWebSearchViaProvider({
231
+ ctx,
232
+ engine,
233
+ query,
234
+ recency,
235
+ count: args.resultCount,
236
+ requestSuffix
237
+ });
238
+ summary = backendResult.summary;
239
+ hits = backendResult.hits;
240
+ ok = backendResult.ok;
241
+ }
242
+ else if (ctx.options.reenterPipeline) {
243
+ const payload = buildWebSearchReenterPayload(engine, query, recency, args.resultCount);
244
+ const followup = await ctx.options.reenterPipeline({
245
+ entryEndpoint: '/v1/chat/completions',
246
+ requestId: `${ctx.options.requestId}${requestSuffix}`,
247
+ body: payload,
248
+ metadata: {
249
+ routeHint: 'web_search',
250
+ serverToolFollowup: true,
251
+ stream: false
252
+ }
253
+ });
254
+ const body = followup.body && typeof followup.body === 'object' ? followup.body : null;
255
+ if (body) {
256
+ summary = extractTextFromChatLike(body);
257
+ hits = collectWebSearchHits(body);
258
+ if (!summary && process.env.ROUTECODEX_DEBUG_GLM_WEB_SEARCH === '1') {
259
+ try {
260
+ // eslint-disable-next-line no-console
261
+ console.log('\x1b[38;5;27m[server-tool][web_search][backend_debug]' +
262
+ ` requestId=${ctx.options.requestId}${requestSuffix} payload=${JSON.stringify(body).slice(0, 2000)}\x1b[0m`);
263
+ }
264
+ catch {
265
+ /* logging best-effort */
266
+ }
267
+ }
268
+ }
269
+ }
270
+ else if (ctx.options.providerInvoker) {
271
+ summary = await executeWebSearchViaProvider({
272
+ ctx,
273
+ engine,
274
+ query,
275
+ recency,
276
+ count: args.resultCount,
277
+ requestSuffix
278
+ });
279
+ hits = [];
280
+ }
281
+ }
282
+ catch (error) {
283
+ ok = false;
284
+ const message = error instanceof Error && typeof error.message === 'string' && error.message.trim()
285
+ ? sanitizeBackendError(error.message.trim())
286
+ : 'web_search backend failed';
287
+ summary =
288
+ `web_search failed: ${message}. ` +
289
+ '请调整搜索关键词(例如减少敏感描述或换一种说法)后重试,这只是当前查询被阻止。';
290
+ }
291
+ if (!summary) {
292
+ if (hits.length) {
293
+ summary = formatHitsSummary(hits);
294
+ }
295
+ else {
296
+ summary =
297
+ 'web_search completed but returned no textual summary. ' +
298
+ '可以尝试修改搜索词、增加时间范围或换一个更具体的描述后再次搜索。';
299
+ }
300
+ }
301
+ try {
302
+ const preview = summary.length > 120 ? `${summary.slice(0, 117)}...` : summary;
303
+ // eslint-disable-next-line no-console
304
+ console.log(`\x1b[38;5;27m[server-tool][web_search][result] requestId=${ctx.options.requestId} ` +
305
+ `engine=${engine.id} chars=${summary.length} preview=${JSON.stringify(preview)}\x1b[0m`);
306
+ }
307
+ catch {
308
+ /* logging best-effort */
309
+ }
310
+ const finalHits = limitHits(hits);
311
+ if (!summary) {
312
+ summary = formatHitsSummary(finalHits);
313
+ }
314
+ if (finalHits.length > 0 && finalHits.length < 5) {
315
+ summary = `${summary}\n(当前仅返回${finalHits.length}条可用结果,可尝试调整关键词或时间范围以获取更多。)`;
316
+ }
317
+ return { summary, hits: finalHits, ok };
318
+ }
319
+ function buildWebSearchReenterPayload(engine, query, recency, resultCount) {
320
+ const systemPrompt = buildWebSearchSystemPrompt(resultCount);
321
+ const basePayload = {
322
+ model: engine.id,
323
+ messages: [
324
+ {
325
+ role: 'system',
326
+ content: systemPrompt
327
+ },
328
+ {
329
+ role: 'user',
330
+ content: query
331
+ }
332
+ ],
333
+ stream: false
334
+ };
335
+ if (isGeminiWebSearchEngine(engine)) {
336
+ return basePayload;
337
+ }
338
+ return {
339
+ ...basePayload,
340
+ web_search: {
341
+ query,
342
+ ...(recency ? { recency } : {}),
343
+ count: resultCount,
344
+ engine: engine.id
345
+ }
346
+ };
347
+ }
348
+ async function executeWebSearchViaProvider(args) {
349
+ const { ctx, engine, query, recency, count, requestSuffix } = args;
350
+ if (!ctx.options.providerInvoker) {
351
+ return '';
352
+ }
353
+ if (isGeminiWebSearchEngine(engine)) {
354
+ const geminiPayload = {
355
+ model: engine.id,
356
+ contents: [
357
+ {
358
+ role: 'user',
359
+ parts: [
360
+ {
361
+ text: query
362
+ }
363
+ ]
364
+ }
365
+ ],
366
+ tools: [
367
+ {
368
+ googleSearch: {}
369
+ }
370
+ ]
371
+ };
372
+ const backend = await ctx.options.providerInvoker({
373
+ providerKey: engine.providerKey,
374
+ providerType: undefined,
375
+ modelId: engine.id,
376
+ providerProtocol: 'gemini-chat',
377
+ payload: geminiPayload,
378
+ entryEndpoint: '/v1/models/gemini:generateContent',
379
+ requestId: `${ctx.options.requestId}${requestSuffix}`,
380
+ routeHint: 'web_search'
381
+ });
382
+ const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
383
+ ? backend.providerResponse
384
+ : null;
385
+ if (!providerResponse) {
386
+ return '';
387
+ }
388
+ const chatLike = buildOpenAIChatFromGeminiResponse(providerResponse);
389
+ return chatLike ? extractTextFromChatLike(chatLike) : '';
390
+ }
391
+ const backendPayload = {
392
+ model: engine.id,
393
+ messages: [
394
+ {
395
+ role: 'system',
396
+ content: 'You are a web search engine. Answer with up-to-date information based on the open internet.'
397
+ },
398
+ {
399
+ role: 'user',
400
+ content: query
401
+ }
402
+ ],
403
+ stream: false,
404
+ web_search: {
405
+ query,
406
+ ...(recency ? { recency } : {}),
407
+ count: args.count,
408
+ engine: engine.id
409
+ }
410
+ };
411
+ const backend = await ctx.options.providerInvoker({
412
+ providerKey: engine.providerKey,
413
+ providerType: undefined,
414
+ modelId: undefined,
415
+ providerProtocol: ctx.options.providerProtocol,
416
+ payload: backendPayload,
417
+ entryEndpoint: '/v1/chat/completions',
418
+ requestId: `${ctx.options.requestId}${requestSuffix}`,
419
+ routeHint: 'web_search'
420
+ });
421
+ const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
422
+ ? backend.providerResponse
423
+ : null;
424
+ if (!providerResponse) {
425
+ return '';
426
+ }
427
+ return extractTextFromChatLike(providerResponse);
428
+ }
429
+ async function executeIflowWebSearchViaProvider(args) {
430
+ const { ctx, engine, query, count, requestSuffix } = args;
431
+ if (!ctx.options.providerInvoker) {
432
+ return {
433
+ summary: '',
434
+ hits: [],
435
+ ok: false
436
+ };
437
+ }
438
+ const searchEngineList = Array.isArray(engine.searchEngineList) && engine.searchEngineList.length
439
+ ? engine.searchEngineList
440
+ : ['GOOGLE', 'BING', 'SCHOLAR', 'AIPGC', 'PDF'];
441
+ const searchBody = {
442
+ query,
443
+ history: {},
444
+ userId: 2,
445
+ userIp: '42.120.74.197',
446
+ appCode: 'SEARCH_CHATBOT',
447
+ chatId: Date.now(),
448
+ phase: 'UNIFY',
449
+ enableQueryRewrite: false,
450
+ enableRetrievalSecurity: false,
451
+ enableIntention: false,
452
+ searchEngineList
453
+ };
454
+ let providerKey = engine.providerKey;
455
+ try {
456
+ const adapter = ctx.adapterContext && typeof ctx.adapterContext === 'object'
457
+ ? ctx.adapterContext
458
+ : null;
459
+ const target = adapter && adapter.target && typeof adapter.target === 'object'
460
+ ? adapter.target
461
+ : null;
462
+ const targetProviderKey = target && typeof target.providerKey === 'string' && target.providerKey.trim()
463
+ ? target.providerKey.trim()
464
+ : undefined;
465
+ if (targetProviderKey) {
466
+ providerKey = targetProviderKey;
467
+ }
468
+ }
469
+ catch {
470
+ // best-effort: fallback to engine.providerKey
471
+ }
472
+ const payload = {
473
+ data: searchBody,
474
+ metadata: {
475
+ entryEndpoint: '/chat/retrieve',
476
+ iflowWebSearch: true,
477
+ routeName: 'web_search'
478
+ }
479
+ };
480
+ const backend = await ctx.options.providerInvoker({
481
+ providerKey,
482
+ providerType: undefined,
483
+ modelId: undefined,
484
+ providerProtocol: ctx.options.providerProtocol,
485
+ payload,
486
+ entryEndpoint: '/v1/chat/retrieve',
487
+ requestId: `${ctx.options.requestId}${requestSuffix}`,
488
+ routeHint: 'web_search'
489
+ });
490
+ const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
491
+ ? backend.providerResponse
492
+ : null;
493
+ if (!providerResponse) {
494
+ return {
495
+ summary: '',
496
+ hits: [],
497
+ ok: false
498
+ };
499
+ }
500
+ const container = providerResponse;
501
+ const rawHits = Array.isArray(container.data) ? container.data : [];
502
+ const hits = [];
503
+ for (const item of rawHits) {
504
+ if (!item || typeof item !== 'object' || Array.isArray(item))
505
+ continue;
506
+ const record = item;
507
+ const link = typeof record.url === 'string' && record.url.trim() ? record.url.trim() : '';
508
+ if (!link)
509
+ continue;
510
+ const title = typeof record.title === 'string' && record.title.trim() ? record.title.trim() : undefined;
511
+ const publishDate = typeof record.time === 'string' && record.time.trim() ? record.time.trim() : undefined;
512
+ const content = typeof record.abstractInfo === 'string' && record.abstractInfo.trim()
513
+ ? record.abstractInfo.trim()
514
+ : undefined;
515
+ hits.push({
516
+ title,
517
+ link,
518
+ publish_date: publishDate,
519
+ content
520
+ });
521
+ if (hits.length >= count) {
522
+ break;
523
+ }
524
+ }
525
+ let summary = '';
526
+ if (typeof container.message === 'string' && container.message.trim()) {
527
+ summary = container.message.trim();
528
+ }
529
+ if (!summary && hits.length) {
530
+ summary = formatHitsSummary(hits);
531
+ }
532
+ const successField = container.success;
533
+ const ok = typeof successField === 'boolean' ? successField : hits.length > 0;
534
+ return {
535
+ summary,
536
+ hits,
537
+ ok
538
+ };
539
+ }
540
+ function injectWebSearchToolResult(base, toolCall, engine, query, backendResult) {
541
+ const cloned = cloneJson(base);
542
+ const existingOutputs = Array.isArray(cloned.tool_outputs)
543
+ ? cloned.tool_outputs
544
+ : [];
545
+ const resultsPayload = backendResult.hits.map((hit, index) => ({
546
+ index: index + 1,
547
+ title: hit.title ?? '',
548
+ link: hit.link,
549
+ snippet: hit.content ?? '',
550
+ source: hit.media ?? '',
551
+ publish_date: hit.publish_date ?? ''
552
+ }));
553
+ cloned.tool_outputs = [
554
+ ...existingOutputs,
555
+ {
556
+ tool_call_id: toolCall.id,
557
+ name: 'web_search',
558
+ content: JSON.stringify({
559
+ engine: engine.id,
560
+ query,
561
+ summary: backendResult.summary,
562
+ results: resultsPayload
563
+ })
564
+ }
565
+ ];
566
+ return cloned;
567
+ }
568
+ function buildWebSearchFollowupPayload(adapterContext, chatResponse) {
569
+ const captured = adapterContext && typeof adapterContext === 'object'
570
+ ? adapterContext.capturedChatRequest
571
+ : undefined;
572
+ if (!captured || typeof captured !== 'object') {
573
+ return null;
574
+ }
575
+ const originalMessages = Array.isArray(captured.messages)
576
+ ? cloneJson(captured.messages)
577
+ : [];
578
+ const originalTools = Array.isArray(captured.tools)
579
+ ? cloneJson(captured.tools)
580
+ : [];
581
+ const assistantMessage = extractAssistantMessage(chatResponse);
582
+ if (!assistantMessage) {
583
+ return null;
584
+ }
585
+ const toolMessages = buildToolMessages(chatResponse);
586
+ if (!toolMessages.length) {
587
+ return null;
588
+ }
589
+ const reconstructed = [...originalMessages, assistantMessage, ...toolMessages];
590
+ const filteredTools = originalTools.filter((tool) => {
591
+ if (!tool || typeof tool !== 'object')
592
+ return false;
593
+ const fn = tool.function;
594
+ const name = fn && typeof fn.name === 'string'
595
+ ? fn.name
596
+ : '';
597
+ if (!name)
598
+ return true;
599
+ return name !== 'web_search';
600
+ });
601
+ const payload = {
602
+ messages: reconstructed
603
+ };
604
+ if (typeof captured.model === 'string' && captured.model.trim()) {
605
+ payload.model = captured.model.trim();
606
+ }
607
+ if (filteredTools.length) {
608
+ payload.tools = filteredTools;
609
+ }
610
+ const parameters = captured.parameters;
611
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
612
+ Object.assign(payload, cloneJson(parameters));
613
+ }
614
+ return payload;
615
+ }
616
+ function extractAssistantMessage(chatResponse) {
617
+ const choices = Array.isArray(chatResponse.choices)
618
+ ? chatResponse.choices
619
+ : [];
620
+ if (!choices.length)
621
+ return null;
622
+ const firstChoice = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
623
+ ? choices[0]
624
+ : null;
625
+ if (!firstChoice)
626
+ return null;
627
+ const assistantMessage = firstChoice.message && typeof firstChoice.message === 'object'
628
+ ? firstChoice.message
629
+ : null;
630
+ return assistantMessage;
631
+ }
632
+ function buildToolMessages(chatResponse) {
633
+ const toolOutputs = Array.isArray(chatResponse.tool_outputs)
634
+ ? chatResponse.tool_outputs
635
+ : [];
636
+ const messages = [];
637
+ for (const entry of toolOutputs) {
638
+ if (!entry || typeof entry !== 'object')
639
+ continue;
640
+ const toolCallId = typeof entry.tool_call_id === 'string'
641
+ ? entry.tool_call_id
642
+ : undefined;
643
+ if (!toolCallId)
644
+ continue;
645
+ const name = typeof entry.name === 'string'
646
+ ? entry.name
647
+ : 'web_search';
648
+ const rawContent = entry.content;
649
+ let contentText;
650
+ if (typeof rawContent === 'string') {
651
+ contentText = rawContent;
652
+ }
653
+ else {
654
+ try {
655
+ contentText = JSON.stringify(rawContent ?? {});
656
+ }
657
+ catch {
658
+ contentText = String(rawContent ?? '');
659
+ }
660
+ }
661
+ messages.push({
662
+ role: 'tool',
663
+ tool_call_id: toolCallId,
664
+ name,
665
+ content: contentText
666
+ });
667
+ }
668
+ return messages;
669
+ }
670
+ function logServerToolWebSearch(engine, requestId, query) {
671
+ const providerAlias = engine.providerKey.split('.')[0] || engine.providerKey;
672
+ const backendLabel = `${providerAlias}:${engine.id}`;
673
+ const prefix = `[server-tool][web_search][${backendLabel}]`;
674
+ const line = `${prefix} requestId=${requestId} query=${JSON.stringify(query)}`;
675
+ // eslint-disable-next-line no-console
676
+ console.log(`\x1b[38;5;27m${line}\x1b[0m`);
677
+ const vrPrefix = `[virtual-router][servertool][web_search]`;
678
+ const vrBackend = `${engine.providerKey}:${engine.id}`;
679
+ const vrLine = `${vrPrefix} requestId=${requestId} backend=${vrBackend}`;
680
+ // eslint-disable-next-line no-console
681
+ console.log(`\x1b[31m${vrLine}\x1b[0m`);
682
+ }
683
+ function buildFollowupMetadata(adapterContext, toolName) {
684
+ const ctx = adapterContext && typeof adapterContext === 'object' ? adapterContext : null;
685
+ const routeId = ctx && typeof ctx.routeId === 'string' && ctx.routeId.trim() ? ctx.routeId.trim() : '';
686
+ if (!routeId || routeId.toLowerCase() === toolName.toLowerCase()) {
687
+ return undefined;
688
+ }
689
+ return { routeHint: routeId };
690
+ }
691
+ function findWebSearchArray(payload) {
692
+ let current = payload;
693
+ const visited = new Set();
694
+ while (current && !visited.has(current)) {
695
+ visited.add(current);
696
+ const hits = getArray(current.web_search);
697
+ if (hits.length) {
698
+ return hits;
699
+ }
700
+ const nextData = current.data;
701
+ if (nextData && typeof nextData === 'object' && !Array.isArray(nextData)) {
702
+ current = nextData;
703
+ continue;
704
+ }
705
+ const nextResponse = current.response;
706
+ if (nextResponse && typeof nextResponse === 'object' && !Array.isArray(nextResponse)) {
707
+ current = nextResponse;
708
+ continue;
709
+ }
710
+ break;
711
+ }
712
+ return undefined;
713
+ }
714
+ function collectWebSearchHits(payload) {
715
+ const array = findWebSearchArray(payload);
716
+ if (!array) {
717
+ return [];
718
+ }
719
+ const hits = [];
720
+ for (const entry of array) {
721
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
722
+ continue;
723
+ const record = entry;
724
+ const link = typeof record.link === 'string' && record.link.trim() ? record.link.trim() : '';
725
+ if (!link)
726
+ continue;
727
+ hits.push({
728
+ title: typeof record.title === 'string' ? record.title : undefined,
729
+ link,
730
+ media: typeof record.media === 'string' ? record.media : undefined,
731
+ publish_date: typeof record.publish_date === 'string' ? record.publish_date : undefined,
732
+ content: typeof record.content === 'string' ? record.content : undefined,
733
+ refer: typeof record.refer === 'string' ? record.refer : undefined
734
+ });
735
+ }
736
+ return hits;
737
+ }
738
+ function limitHits(hits) {
739
+ if (!hits.length)
740
+ return [];
741
+ const filtered = hits.slice(0, 15);
742
+ if (filtered.length >= 5) {
743
+ return filtered;
744
+ }
745
+ return filtered;
746
+ }
747
+ function formatHitsSummary(hits) {
748
+ if (!hits.length) {
749
+ return '';
750
+ }
751
+ const segments = [];
752
+ hits.forEach((hit, index) => {
753
+ const idx = hit.refer && hit.refer.trim() ? hit.refer.trim() : String(index + 1);
754
+ const headerParts = [];
755
+ if (hit.title)
756
+ headerParts.push(hit.title);
757
+ if (hit.media)
758
+ headerParts.push(hit.media);
759
+ if (hit.publish_date)
760
+ headerParts.push(hit.publish_date);
761
+ const header = headerParts.length ? headerParts.join(' · ') : '搜索结果';
762
+ const details = [hit.content, hit.link].filter(Boolean).join('\n');
763
+ segments.push(`【${idx}】${header}\n${details}`);
764
+ });
765
+ return segments.join('\n\n');
766
+ }
767
+ function getArray(value) {
768
+ return Array.isArray(value) ? value : [];
769
+ }
770
+ function sanitizeBackendError(message) {
771
+ const lowered = message.toLowerCase();
772
+ if (lowered.includes('contentfilter')) {
773
+ return '搜索请求被后端暂时拒绝';
774
+ }
775
+ if (lowered.includes('instructions are not valid')) {
776
+ return '搜索指令格式未被后端接受';
777
+ }
778
+ return message;
779
+ }
780
+ function buildWebSearchSystemPrompt(targetCount) {
781
+ const normalizedTarget = Math.max(5, Math.min(15, targetCount));
782
+ const instructions = [
783
+ 'You are an up-to-date web search engine that aggregates public internet results.',
784
+ `Return between 5 and 15 high-quality search results (aim for about ${normalizedTarget} when available).`,
785
+ 'Each result must include: title, source/media, publish date if available, a concise summary (<=200 characters), and a direct URL that users can click for verification.',
786
+ 'Prefer de-duplicated sources and include diverse outlets. If fewer than 5 results exist, return what you can find and explain the limitation.',
787
+ 'Only mention that the query was blocked when the backend explicitly rejects it, and encourage the user to adjust their keywords before retrying.',
788
+ 'Structure the answer so downstream systems can extract each result cleanly.'
789
+ ];
790
+ return instructions.join('\n');
791
+ }