@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.
- package/dist/conversion/codecs/gemini-openai-codec.js +128 -4
- 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 +11 -3
- 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/semantic-mappers/gemini-mapper.js +118 -11
- package/dist/conversion/hub/types/standardized.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.js +49 -3
- package/dist/conversion/shared/snapshot-utils.js +17 -47
- package/dist/conversion/shared/tool-mapping.js +25 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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 +8 -2
- package/dist/router/virtual-router/engine.js +176 -81
- 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/dist/telemetry/stats-center.d.ts +73 -0
- package/dist/telemetry/stats-center.js +280 -0
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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,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
|
}
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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>;
|