@jsonstudio/llms 0.6.203 → 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.
Files changed (32) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +128 -4
  2. package/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
  3. package/dist/conversion/compat/actions/glm-web-search.js +66 -0
  4. package/dist/conversion/compat/profiles/chat-glm.json +4 -1
  5. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  6. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +11 -3
  8. package/dist/conversion/hub/process/chat-process.js +131 -1
  9. package/dist/conversion/hub/response/provider-response.d.ts +22 -0
  10. package/dist/conversion/hub/response/provider-response.js +12 -1
  11. package/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
  12. package/dist/conversion/hub/response/server-side-tools.js +326 -0
  13. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +118 -11
  14. package/dist/conversion/hub/types/standardized.d.ts +1 -0
  15. package/dist/conversion/responses/responses-openai-bridge.js +49 -3
  16. package/dist/conversion/shared/snapshot-utils.js +17 -47
  17. package/dist/conversion/shared/tool-mapping.js +25 -2
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/router/virtual-router/bootstrap.js +273 -40
  21. package/dist/router/virtual-router/context-advisor.d.ts +0 -2
  22. package/dist/router/virtual-router/context-advisor.js +0 -12
  23. package/dist/router/virtual-router/engine.d.ts +8 -2
  24. package/dist/router/virtual-router/engine.js +176 -81
  25. package/dist/router/virtual-router/types.d.ts +21 -2
  26. package/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  27. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  28. package/dist/sse/types/gemini-types.d.ts +20 -1
  29. package/dist/sse/types/responses-types.js +1 -1
  30. package/dist/telemetry/stats-center.d.ts +73 -0
  31. package/dist/telemetry/stats-center.js +280 -0
  32. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { normalizeChatMessageContent } from '../shared/chat-output-normalizer.js
4
4
  import { mapBridgeToolsToChat } from '../shared/tool-mapping.js';
5
5
  import { prepareGeminiToolsForBridge } from '../shared/gemini-tool-utils.js';
6
6
  import { registerResponsesReasoning, consumeResponsesReasoning, registerResponsesOutputTextMeta, consumeResponsesOutputTextMeta, consumeResponsesPayloadSnapshot, registerResponsesPayloadSnapshot, consumeResponsesPassthrough, registerResponsesPassthrough } from '../shared/responses-reasoning-registry.js';
7
+ const DUMMY_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
7
8
  function isObject(v) {
8
9
  return !!v && typeof v === 'object' && !Array.isArray(v);
9
10
  }
@@ -54,6 +55,32 @@ function mapChatRoleToGemini(role) {
54
55
  return 'tool';
55
56
  return 'user';
56
57
  }
58
+ function coerceThoughtSignature(value) {
59
+ if (typeof value === 'string' && value.trim().length) {
60
+ return value.trim();
61
+ }
62
+ return undefined;
63
+ }
64
+ function extractThoughtSignatureFromToolCall(tc) {
65
+ if (!tc || typeof tc !== 'object') {
66
+ return undefined;
67
+ }
68
+ const direct = coerceThoughtSignature(tc.thought_signature ?? tc.thoughtSignature);
69
+ if (direct) {
70
+ return direct;
71
+ }
72
+ const extraContent = tc.extra_content ?? tc.extraContent;
73
+ if (extraContent && typeof extraContent === 'object') {
74
+ const googleNode = extraContent.google ?? extraContent.Google;
75
+ if (googleNode && typeof googleNode === 'object') {
76
+ const googleSig = coerceThoughtSignature(googleNode.thought_signature ?? googleNode.thoughtSignature);
77
+ if (googleSig) {
78
+ return googleSig;
79
+ }
80
+ }
81
+ }
82
+ return undefined;
83
+ }
57
84
  export function buildOpenAIChatFromGeminiRequest(payload) {
58
85
  const messages = [];
59
86
  // systemInstruction → Chat system 消息
@@ -156,16 +183,20 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
156
183
  const textParts = [];
157
184
  const reasoningParts = [];
158
185
  const toolCalls = [];
186
+ const toolResultTexts = [];
187
+ const toolOutputs = [];
159
188
  for (const part of parts) {
160
189
  if (!part || typeof part !== 'object')
161
190
  continue;
162
191
  const pObj = part;
192
+ // 1. Text part
163
193
  if (typeof pObj.text === 'string') {
164
194
  const t = pObj.text;
165
195
  if (t && t.trim().length)
166
196
  textParts.push(t);
167
197
  continue;
168
198
  }
199
+ // 2. Content array (nested structure)
169
200
  if (Array.isArray(pObj.content)) {
170
201
  for (const inner of pObj.content) {
171
202
  if (typeof inner === 'string') {
@@ -177,10 +208,20 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
177
208
  }
178
209
  continue;
179
210
  }
211
+ // 3. Reasoning part (channel mode)
180
212
  if (typeof pObj.reasoning === 'string') {
181
213
  reasoningParts.push(pObj.reasoning);
182
214
  continue;
183
215
  }
216
+ // 4. Thought part (thinking/extended thinking)
217
+ if (typeof pObj.thought === 'string') {
218
+ const thoughtText = pObj.thought.trim();
219
+ if (thoughtText.length) {
220
+ reasoningParts.push(thoughtText);
221
+ }
222
+ continue;
223
+ }
224
+ // 5. Function call (tool call)
184
225
  if (pObj.functionCall && typeof pObj.functionCall === 'object') {
185
226
  const fc = pObj.functionCall;
186
227
  const name = typeof fc.name === 'string' ? String(fc.name) : undefined;
@@ -195,15 +236,85 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
195
236
  else {
196
237
  argsStr = safeJson(argsRaw);
197
238
  }
198
- toolCalls.push({
239
+ const thoughtSignature = coerceThoughtSignature(pObj.thoughtSignature);
240
+ const toolCall = {
199
241
  id,
200
242
  type: 'function',
201
243
  function: { name, arguments: argsStr }
202
- });
244
+ };
245
+ if (thoughtSignature) {
246
+ toolCall.thought_signature = thoughtSignature;
247
+ toolCall.extra_content = {
248
+ google: {
249
+ thought_signature: thoughtSignature
250
+ }
251
+ };
252
+ }
253
+ toolCalls.push(toolCall);
254
+ continue;
255
+ }
256
+ // 6. Function response (tool result)
257
+ if (pObj.functionResponse && typeof pObj.functionResponse === 'object') {
258
+ const fr = pObj.functionResponse;
259
+ const callId = typeof fr.id === 'string' && fr.id.trim().length ? String(fr.id) : undefined;
260
+ const name = typeof fr.name === 'string' && fr.name.trim().length ? String(fr.name) : undefined;
261
+ const resp = fr.response;
262
+ let contentStr = '';
263
+ if (typeof resp === 'string') {
264
+ contentStr = resp;
265
+ }
266
+ else if (resp != null) {
267
+ try {
268
+ contentStr = JSON.stringify(resp);
269
+ }
270
+ catch {
271
+ contentStr = String(resp);
272
+ }
273
+ }
274
+ if (contentStr && contentStr.trim().length) {
275
+ toolResultTexts.push(contentStr);
276
+ if (callId || name) {
277
+ const entry = {
278
+ tool_call_id: callId ?? undefined,
279
+ id: callId ?? undefined,
280
+ content: contentStr
281
+ };
282
+ if (name) {
283
+ entry.name = name;
284
+ }
285
+ toolOutputs.push(entry);
286
+ }
287
+ }
288
+ continue;
289
+ }
290
+ // 7. Executable code (code_interpreter)
291
+ if (pObj.executableCode && typeof pObj.executableCode === 'object') {
292
+ const code = pObj.executableCode;
293
+ const language = typeof code.language === 'string' ? code.language : 'python';
294
+ const codeText = typeof code.code === 'string' ? code.code : '';
295
+ if (codeText.trim().length) {
296
+ // Append as text with code block formatting
297
+ textParts.push(`\`\`\`${language}\n${codeText}\n\`\`\``);
298
+ }
299
+ continue;
300
+ }
301
+ // 8. Code execution result
302
+ if (pObj.codeExecutionResult && typeof pObj.codeExecutionResult === 'object') {
303
+ const result = pObj.codeExecutionResult;
304
+ const outcome = typeof result.outcome === 'string' ? result.outcome : '';
305
+ const output = typeof result.output === 'string' ? result.output : '';
306
+ if (output.trim().length) {
307
+ textParts.push(`[Code Output${outcome ? ` (${outcome})` : ''}]:\n${output}`);
308
+ }
203
309
  continue;
204
310
  }
205
311
  }
312
+ const hasToolCalls = toolCalls.length > 0;
206
313
  const finish_reason = (() => {
314
+ // If the model is emitting tool calls, treat this turn as a tool_calls
315
+ // completion so downstream tool governance can continue the loop.
316
+ if (hasToolCalls)
317
+ return 'tool_calls';
207
318
  const fr = String(primary?.finishReason || '').toUpperCase();
208
319
  if (fr === 'MAX_TOKENS')
209
320
  return 'length';
@@ -228,9 +339,14 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
228
339
  usage.total_tokens = totalTokens;
229
340
  const combinedText = textParts.join('\n');
230
341
  const normalized = combinedText.length ? normalizeChatMessageContent(combinedText) : { contentText: undefined, reasoningText: undefined };
342
+ const baseContent = normalized.contentText ?? combinedText ?? '';
343
+ const toolResultBlock = toolResultTexts.length ? toolResultTexts.join('\n') : '';
344
+ const finalContent = toolResultBlock && baseContent
345
+ ? `${baseContent}\n${toolResultBlock}`
346
+ : baseContent || toolResultBlock;
231
347
  const chatMsg = {
232
348
  role,
233
- content: normalized.contentText ?? combinedText ?? ''
349
+ content: finalContent
234
350
  };
235
351
  if (typeof normalized.reasoningText === 'string' && normalized.reasoningText.trim().length) {
236
352
  reasoningParts.push(normalized.reasoningText.trim());
@@ -277,6 +393,9 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
277
393
  if (Object.keys(usage).length > 0) {
278
394
  chatResp.usage = usage;
279
395
  }
396
+ if (toolOutputs.length > 0) {
397
+ chatResp.tool_outputs = toolOutputs;
398
+ }
280
399
  const preservedReasoning = consumeResponsesReasoning(chatResp.id);
281
400
  if (preservedReasoning && preservedReasoning.length) {
282
401
  chatResp.__responses_reasoning = preservedReasoning;
@@ -398,7 +517,12 @@ export function buildGeminiFromOpenAIChat(chatResp) {
398
517
  const id = typeof tc.id === 'string' ? String(tc.id) : undefined;
399
518
  if (id)
400
519
  functionCall.id = id;
401
- parts.push({ functionCall });
520
+ const thoughtSignature = extractThoughtSignatureFromToolCall(tc) ?? DUMMY_THOUGHT_SIGNATURE;
521
+ const partEntry = { functionCall };
522
+ if (thoughtSignature) {
523
+ partEntry.thoughtSignature = thoughtSignature;
524
+ }
525
+ parts.push(partEntry);
402
526
  }
403
527
  const candidate = {
404
528
  content: {
@@ -0,0 +1,2 @@
1
+ import type { JsonObject } from '../../hub/types/json.js';
2
+ export declare function applyGlmWebSearchRequestTransform(payload: JsonObject): JsonObject;
@@ -0,0 +1,66 @@
1
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
+ export function applyGlmWebSearchRequestTransform(payload) {
3
+ const root = structuredClone(payload);
4
+ const webSearchRaw = root.web_search;
5
+ if (!isRecord(webSearchRaw)) {
6
+ return root;
7
+ }
8
+ const webSearch = webSearchRaw;
9
+ const queryValue = webSearch.query;
10
+ const recencyValue = webSearch.recency;
11
+ const countValue = webSearch.count;
12
+ const query = typeof queryValue === 'string' ? queryValue.trim() : '';
13
+ const recency = typeof recencyValue === 'string' ? recencyValue.trim() : undefined;
14
+ let count;
15
+ if (typeof countValue === 'number' && Number.isFinite(countValue)) {
16
+ const normalized = Math.floor(countValue);
17
+ if (normalized >= 1 && normalized <= 50) {
18
+ count = normalized;
19
+ }
20
+ }
21
+ if (!query) {
22
+ // No meaningful search query, drop the helper object and passthrough.
23
+ delete root.web_search;
24
+ return root;
25
+ }
26
+ const toolsValue = root.tools;
27
+ const tools = Array.isArray(toolsValue) ? [...toolsValue] : [];
28
+ let existingIndex = -1;
29
+ for (let i = 0; i < tools.length; i += 1) {
30
+ const tool = tools[i];
31
+ if (isRecord(tool) && typeof tool.type === 'string' && tool.type === 'web_search') {
32
+ existingIndex = i;
33
+ break;
34
+ }
35
+ }
36
+ const webSearchConfig = {
37
+ enable: true,
38
+ search_query: query
39
+ };
40
+ if (recency) {
41
+ webSearchConfig.search_recency_filter = recency;
42
+ }
43
+ if (typeof count === 'number') {
44
+ webSearchConfig.count = count;
45
+ }
46
+ const baseTool = existingIndex >= 0 && isRecord(tools[existingIndex])
47
+ ? { ...tools[existingIndex] }
48
+ : {};
49
+ baseTool.type = 'web_search';
50
+ const existingWebSearch = isRecord(baseTool.web_search)
51
+ ? baseTool.web_search
52
+ : {};
53
+ baseTool.web_search = {
54
+ ...existingWebSearch,
55
+ ...webSearchConfig
56
+ };
57
+ if (existingIndex >= 0) {
58
+ tools[existingIndex] = baseTool;
59
+ }
60
+ else {
61
+ tools.push(baseTool);
62
+ }
63
+ root.tools = tools;
64
+ delete root.web_search;
65
+ return root;
66
+ }
@@ -21,7 +21,7 @@
21
21
  "request": {
22
22
  "allowTopLevel": [
23
23
  "model", "messages", "stream", "thinking", "do_sample", "temperature", "top_p",
24
- "max_tokens", "tools", "tool_choice", "stop", "response_format"
24
+ "max_tokens", "tools", "tool_choice", "stop", "response_format", "web_search"
25
25
  ],
26
26
  "messages": {
27
27
  "allowedRoles": ["system", "user", "assistant", "tool"],
@@ -157,6 +157,9 @@
157
157
  }
158
158
  }
159
159
  },
160
+ {
161
+ "action": "glm_web_search_request"
162
+ },
160
163
  {
161
164
  "action": "field_map",
162
165
  "direction": "outgoing",
@@ -10,6 +10,7 @@ import { validateResponsePayload } from '../../../compat/actions/response-valida
10
10
  import { writeCompatSnapshot } from '../../../compat/actions/snapshot.js';
11
11
  import { applyQwenRequestTransform, applyQwenResponseTransform } from '../../../compat/actions/qwen-transform.js';
12
12
  import { extractGlmToolMarkup } from '../../../compat/actions/glm-tool-extraction.js';
13
+ import { applyGlmWebSearchRequestTransform } from '../../../compat/actions/glm-web-search.js';
13
14
  const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
14
15
  const INTERNAL_STATE = Symbol('compat.internal_state');
15
16
  export function runRequestCompatPipeline(profileId, payload, options) {
@@ -157,6 +158,11 @@ function applyMapping(root, mapping, state) {
157
158
  case 'qwen_response_transform':
158
159
  replaceRoot(root, applyQwenResponseTransform(root));
159
160
  break;
161
+ case 'glm_web_search_request':
162
+ if (state.direction === 'request') {
163
+ replaceRoot(root, applyGlmWebSearchRequestTransform(root));
164
+ }
165
+ break;
160
166
  default:
161
167
  break;
162
168
  }
@@ -98,6 +98,8 @@ export type MappingInstruction = {
98
98
  action: 'qwen_request_transform';
99
99
  } | {
100
100
  action: 'qwen_response_transform';
101
+ } | {
102
+ action: 'glm_web_search_request';
101
103
  };
102
104
  export type FilterInstruction = {
103
105
  action: 'rate_limit_text';
@@ -11,6 +11,7 @@ import { GeminiSemanticMapper } from '../semantic-mappers/gemini-mapper.js';
11
11
  import { ChatFormatAdapter } from '../format-adapters/chat-format-adapter.js';
12
12
  import { ChatSemanticMapper } from '../semantic-mappers/chat-mapper.js';
13
13
  import { createSnapshotRecorder } from '../snapshot-recorder.js';
14
+ import { shouldRecordSnapshots } from '../../shared/snapshot-utils.js';
14
15
  import { runReqInboundStage1FormatParse } from './stages/req_inbound/req_inbound_stage1_format_parse/index.js';
15
16
  import { runReqInboundStage2SemanticMap } from './stages/req_inbound/req_inbound_stage2_semantic_map/index.js';
16
17
  import { runChatContextCapture, captureResponsesContextSnapshot } from './stages/req_inbound/req_inbound_stage3_context_capture/index.js';
@@ -93,10 +94,18 @@ export class HubPipeline {
93
94
  });
94
95
  let processedRequest;
95
96
  if (normalized.processMode !== 'passthrough') {
97
+ const processMetadata = {
98
+ ...(normalized.metadata ?? {})
99
+ };
100
+ const webSearchConfig = this.config.virtualRouter?.webSearch;
101
+ if (webSearchConfig) {
102
+ processMetadata.webSearch = webSearchConfig;
103
+ }
104
+ normalized.metadata = processMetadata;
96
105
  const processResult = await runReqProcessStage1ToolGovernance({
97
106
  request: standardizedRequest,
98
107
  rawPayload: rawRequest,
99
- metadata: normalized.metadata,
108
+ metadata: processMetadata,
100
109
  entryEndpoint: normalized.entryEndpoint,
101
110
  requestId: normalized.id,
102
111
  stageRecorder: inboundRecorder
@@ -348,8 +357,7 @@ export class HubPipeline {
348
357
  return adapterContext;
349
358
  }
350
359
  maybeCreateStageRecorder(context, endpoint) {
351
- const flag = (process.env.ROUTECODEX_HUB_SNAPSHOTS || '').trim();
352
- if (flag === '0') {
360
+ if (!shouldRecordSnapshots()) {
353
361
  return undefined;
354
362
  }
355
363
  const effectiveEndpoint = endpoint || context.entryEndpoint || '/v1/chat/completions';
@@ -51,7 +51,7 @@ async function applyRequestToolGovernance(request, context) {
51
51
  });
52
52
  const governed = normalizeRecord(governedPayload);
53
53
  const providerStreamIntent = typeof governed.stream === 'boolean' ? governed.stream : undefined;
54
- const merged = {
54
+ let merged = {
55
55
  ...request,
56
56
  messages: Array.isArray(governed.messages)
57
57
  ? governed.messages
@@ -92,6 +92,8 @@ async function applyRequestToolGovernance(request, context) {
92
92
  if (typeof governed.model === 'string' && governed.model.trim()) {
93
93
  merged.model = governed.model.trim();
94
94
  }
95
+ // Server-side web_search tool injection (config-driven, best-effort).
96
+ merged = maybeInjectWebSearchTool(merged, metadata);
95
97
  const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
96
98
  if (summary.applied) {
97
99
  sanitized.metadata = {
@@ -274,3 +276,131 @@ function readToolChoice(value) {
274
276
  function isRecord(value) {
275
277
  return !!value && typeof value === 'object' && !Array.isArray(value);
276
278
  }
279
+ function maybeInjectWebSearchTool(request, metadata) {
280
+ const rawConfig = metadata.webSearch;
281
+ if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
282
+ return request;
283
+ }
284
+ const injectPolicy = (rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective')
285
+ ? rawConfig.injectPolicy
286
+ : 'selective';
287
+ if (injectPolicy === 'selective' && !detectWebSearchIntent(request)) {
288
+ return request;
289
+ }
290
+ const existingTools = Array.isArray(request.tools) ? request.tools : [];
291
+ const hasWebSearch = existingTools.some((tool) => {
292
+ if (!tool || typeof tool !== 'object')
293
+ return false;
294
+ const fn = tool.function;
295
+ return typeof fn?.name === 'string' && fn.name.trim() === 'web_search';
296
+ });
297
+ if (hasWebSearch) {
298
+ return request;
299
+ }
300
+ const engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim());
301
+ if (!engines.length) {
302
+ return request;
303
+ }
304
+ const engineIds = engines.map((engine) => engine.id.trim());
305
+ const engineDescriptions = engines
306
+ .map((engine) => {
307
+ const id = engine.id.trim();
308
+ const desc = typeof engine.description === 'string' && engine.description.trim()
309
+ ? engine.description.trim()
310
+ : '';
311
+ return desc ? `${id}: ${desc}` : id;
312
+ })
313
+ .join('; ');
314
+ const hasMultipleEngines = engineIds.length > 1;
315
+ const parameters = {
316
+ type: 'object',
317
+ properties: {
318
+ ...(hasMultipleEngines
319
+ ? {
320
+ engine: {
321
+ type: 'string',
322
+ enum: engineIds,
323
+ description: engineDescriptions
324
+ }
325
+ }
326
+ : {}),
327
+ query: {
328
+ type: 'string',
329
+ description: 'Search query or user question.'
330
+ },
331
+ recency: {
332
+ type: 'string',
333
+ enum: ['oneDay', 'oneWeek', 'oneMonth', 'oneYear', 'noLimit'],
334
+ description: 'Optional recency filter for web search results.'
335
+ },
336
+ count: {
337
+ type: 'integer',
338
+ minimum: 1,
339
+ maximum: 50,
340
+ description: 'Number of results to retrieve.'
341
+ }
342
+ },
343
+ required: ['query'],
344
+ additionalProperties: false
345
+ };
346
+ const webSearchTool = {
347
+ type: 'function',
348
+ function: {
349
+ name: 'web_search',
350
+ description: 'Perform web search using configured search engines. Use this when the user asks for up-to-date information or news.',
351
+ parameters,
352
+ strict: true
353
+ }
354
+ };
355
+ const nextMetadata = {
356
+ ...(request.metadata ?? {}),
357
+ webSearchEnabled: true
358
+ };
359
+ return {
360
+ ...request,
361
+ metadata: nextMetadata,
362
+ tools: [...existingTools, webSearchTool]
363
+ };
364
+ }
365
+ function detectWebSearchIntent(request) {
366
+ const messages = Array.isArray(request.messages) ? request.messages : [];
367
+ if (!messages.length) {
368
+ return false;
369
+ }
370
+ const last = messages[messages.length - 1];
371
+ if (!last || last.role !== 'user') {
372
+ return false;
373
+ }
374
+ const content = typeof last.content === 'string' ? last.content : '';
375
+ if (!content) {
376
+ return false;
377
+ }
378
+ const text = content.toLowerCase();
379
+ const keywords = [
380
+ // English
381
+ 'web search',
382
+ 'web_search',
383
+ 'websearch',
384
+ 'internet search',
385
+ 'search the web',
386
+ 'online search',
387
+ 'search online',
388
+ 'search on the internet',
389
+ 'search the internet',
390
+ 'web-search',
391
+ 'online-search',
392
+ 'internet-search',
393
+ // Chinese
394
+ '联网搜索',
395
+ '网络搜索',
396
+ '上网搜索',
397
+ '网上搜索',
398
+ '网上查',
399
+ '网上查找',
400
+ '上网查',
401
+ '上网搜',
402
+ // Command-style
403
+ '/search'
404
+ ];
405
+ return keywords.some((keyword) => text.includes(keyword.toLowerCase()));
406
+ }
@@ -0,0 +1,22 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { AdapterContext } from '../types/chat-envelope.js';
3
+ import type { JsonObject } from '../types/json.js';
4
+ import type { StageRecorder } from '../format-adapters/index.js';
5
+ import type { ProviderInvoker } from './server-side-tools.js';
6
+ type ProviderProtocol = 'openai-chat' | 'openai-responses' | 'anthropic-messages' | 'gemini-chat';
7
+ export interface ProviderResponseConversionOptions {
8
+ providerProtocol: ProviderProtocol;
9
+ providerResponse: JsonObject;
10
+ context: AdapterContext;
11
+ entryEndpoint: string;
12
+ wantsStream: boolean;
13
+ stageRecorder?: StageRecorder;
14
+ providerInvoker?: ProviderInvoker;
15
+ }
16
+ export interface ProviderResponseConversionResult {
17
+ body?: JsonObject;
18
+ __sse_responses?: Readable;
19
+ format?: string;
20
+ }
21
+ export declare function convertProviderResponse(options: ProviderResponseConversionOptions): Promise<ProviderResponseConversionResult>;
22
+ export {};
@@ -12,6 +12,7 @@ import { runRespProcessStage2Finalize } from '../pipeline/stages/resp_process/re
12
12
  import { runRespOutboundStage1ClientRemap } from '../pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js';
13
13
  import { runRespOutboundStage2SseStream } from '../pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js';
14
14
  import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
15
+ import { runServerSideToolEngine } from './server-side-tools.js';
15
16
  function resolveChatReasoningMode(entryEndpoint) {
16
17
  const envRaw = (process.env.ROUTECODEX_CHAT_REASONING_MODE || process.env.RCC_CHAT_REASONING_MODE || '').trim().toLowerCase();
17
18
  const map = {
@@ -137,8 +138,18 @@ export async function convertProviderResponse(options) {
137
138
  mapper,
138
139
  stageRecorder: options.stageRecorder
139
140
  });
141
+ // Server-side tool orchestration hook (web_search, etc.).
142
+ const serverSideResult = await runServerSideToolEngine({
143
+ chatResponse,
144
+ adapterContext: options.context,
145
+ entryEndpoint: options.entryEndpoint,
146
+ requestId: options.context.requestId,
147
+ providerProtocol: options.providerProtocol,
148
+ providerInvoker: options.providerInvoker
149
+ });
150
+ const chatForGovernance = serverSideResult.finalChatResponse;
140
151
  const governanceResult = await runRespProcessStage1ToolGovernance({
141
- payload: chatResponse,
152
+ payload: chatForGovernance,
142
153
  entryEndpoint: options.entryEndpoint,
143
154
  requestId: options.context.requestId,
144
155
  clientProtocol,
@@ -0,0 +1,26 @@
1
+ import type { AdapterContext } from '../types/chat-envelope.js';
2
+ import type { JsonObject } from '../types/json.js';
3
+ export type ProviderInvoker = (options: {
4
+ providerKey: string;
5
+ providerType?: string;
6
+ modelId?: string;
7
+ providerProtocol: string;
8
+ payload: JsonObject;
9
+ entryEndpoint: string;
10
+ requestId: string;
11
+ }) => Promise<{
12
+ providerResponse: JsonObject;
13
+ }>;
14
+ export interface ServerSideToolEngineOptions {
15
+ chatResponse: JsonObject;
16
+ adapterContext: AdapterContext;
17
+ entryEndpoint: string;
18
+ requestId: string;
19
+ providerProtocol: string;
20
+ providerInvoker?: ProviderInvoker;
21
+ }
22
+ export interface ServerSideToolEngineResult {
23
+ mode: 'passthrough' | 'web_search_flow';
24
+ finalChatResponse: JsonObject;
25
+ }
26
+ export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;