@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.
Files changed (26) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +83 -1
  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 +9 -1
  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/types/standardized.d.ts +1 -0
  14. package/dist/conversion/responses/responses-openai-bridge.js +49 -3
  15. package/dist/conversion/shared/tool-mapping.js +25 -2
  16. package/dist/router/virtual-router/bootstrap.js +273 -40
  17. package/dist/router/virtual-router/context-advisor.d.ts +0 -2
  18. package/dist/router/virtual-router/context-advisor.js +0 -12
  19. package/dist/router/virtual-router/engine.d.ts +7 -2
  20. package/dist/router/virtual-router/engine.js +161 -82
  21. package/dist/router/virtual-router/types.d.ts +21 -2
  22. package/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  23. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  24. package/dist/sse/types/gemini-types.d.ts +20 -1
  25. package/dist/sse/types/responses-types.js +1 -1
  26. package/package.json +1 -1
@@ -183,16 +183,20 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
183
183
  const textParts = [];
184
184
  const reasoningParts = [];
185
185
  const toolCalls = [];
186
+ const toolResultTexts = [];
187
+ const toolOutputs = [];
186
188
  for (const part of parts) {
187
189
  if (!part || typeof part !== 'object')
188
190
  continue;
189
191
  const pObj = part;
192
+ // 1. Text part
190
193
  if (typeof pObj.text === 'string') {
191
194
  const t = pObj.text;
192
195
  if (t && t.trim().length)
193
196
  textParts.push(t);
194
197
  continue;
195
198
  }
199
+ // 2. Content array (nested structure)
196
200
  if (Array.isArray(pObj.content)) {
197
201
  for (const inner of pObj.content) {
198
202
  if (typeof inner === 'string') {
@@ -204,10 +208,20 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
204
208
  }
205
209
  continue;
206
210
  }
211
+ // 3. Reasoning part (channel mode)
207
212
  if (typeof pObj.reasoning === 'string') {
208
213
  reasoningParts.push(pObj.reasoning);
209
214
  continue;
210
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)
211
225
  if (pObj.functionCall && typeof pObj.functionCall === 'object') {
212
226
  const fc = pObj.functionCall;
213
227
  const name = typeof fc.name === 'string' ? String(fc.name) : undefined;
@@ -239,8 +253,68 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
239
253
  toolCalls.push(toolCall);
240
254
  continue;
241
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
+ }
309
+ continue;
310
+ }
242
311
  }
312
+ const hasToolCalls = toolCalls.length > 0;
243
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';
244
318
  const fr = String(primary?.finishReason || '').toUpperCase();
245
319
  if (fr === 'MAX_TOKENS')
246
320
  return 'length';
@@ -265,9 +339,14 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
265
339
  usage.total_tokens = totalTokens;
266
340
  const combinedText = textParts.join('\n');
267
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;
268
347
  const chatMsg = {
269
348
  role,
270
- content: normalized.contentText ?? combinedText ?? ''
349
+ content: finalContent
271
350
  };
272
351
  if (typeof normalized.reasoningText === 'string' && normalized.reasoningText.trim().length) {
273
352
  reasoningParts.push(normalized.reasoningText.trim());
@@ -314,6 +393,9 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
314
393
  if (Object.keys(usage).length > 0) {
315
394
  chatResp.usage = usage;
316
395
  }
396
+ if (toolOutputs.length > 0) {
397
+ chatResp.tool_outputs = toolOutputs;
398
+ }
317
399
  const preservedReasoning = consumeResponsesReasoning(chatResp.id);
318
400
  if (preservedReasoning && preservedReasoning.length) {
319
401
  chatResp.__responses_reasoning = preservedReasoning;
@@ -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';
@@ -94,10 +94,18 @@ export class HubPipeline {
94
94
  });
95
95
  let processedRequest;
96
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;
97
105
  const processResult = await runReqProcessStage1ToolGovernance({
98
106
  request: standardizedRequest,
99
107
  rawPayload: rawRequest,
100
- metadata: normalized.metadata,
108
+ metadata: processMetadata,
101
109
  entryEndpoint: normalized.entryEndpoint,
102
110
  requestId: normalized.id,
103
111
  stageRecorder: inboundRecorder
@@ -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>;