@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.
- package/README.md +2 -0
- package/dist/conversion/codecs/gemini-openai-codec.js +24 -2
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
- package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
- package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-image-content.js +83 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
- package/dist/conversion/compat/actions/glm-web-search.js +25 -28
- package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
- package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
- package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
- package/dist/conversion/compat/profiles/chat-glm.json +194 -184
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -195
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/config/sample-config.json +1 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +32 -1
- package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
- package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
- package/dist/conversion/hub/pipeline/target-utils.js +6 -0
- package/dist/conversion/hub/process/chat-process.js +186 -40
- package/dist/conversion/hub/response/provider-response.d.ts +13 -1
- package/dist/conversion/hub/response/provider-response.js +84 -35
- package/dist/conversion/hub/response/server-side-tools.js +61 -4
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
- package/dist/conversion/hub/standardized-bridge.js +14 -0
- package/dist/conversion/responses/responses-openai-bridge.js +110 -6
- package/dist/conversion/shared/anthropic-message-utils.js +133 -9
- package/dist/conversion/shared/bridge-message-utils.js +137 -10
- package/dist/conversion/shared/errors.d.ts +20 -0
- package/dist/conversion/shared/errors.js +28 -0
- package/dist/conversion/shared/responses-conversation-store.js +30 -3
- package/dist/conversion/shared/responses-output-builder.js +111 -8
- package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
- package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
- package/dist/filters/special/request-toolcalls-stringify.js +103 -3
- package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
- package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
- package/dist/router/virtual-router/bootstrap.js +44 -12
- package/dist/router/virtual-router/classifier.js +13 -17
- package/dist/router/virtual-router/engine.d.ts +39 -0
- package/dist/router/virtual-router/engine.js +755 -55
- package/dist/router/virtual-router/features.js +1 -1
- package/dist/router/virtual-router/message-utils.js +36 -24
- package/dist/router/virtual-router/provider-registry.d.ts +15 -0
- package/dist/router/virtual-router/provider-registry.js +42 -1
- package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
- package/dist/router/virtual-router/routing-instructions.js +383 -0
- package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
- package/dist/router/virtual-router/sticky-session-store.js +110 -0
- package/dist/router/virtual-router/token-counter.js +14 -3
- package/dist/router/virtual-router/tool-signals.js +0 -22
- package/dist/router/virtual-router/types.d.ts +80 -0
- package/dist/router/virtual-router/types.js +2 -1
- package/dist/servertool/engine.d.ts +27 -0
- package/dist/servertool/engine.js +101 -0
- package/dist/servertool/flow-types.d.ts +40 -0
- package/dist/servertool/flow-types.js +1 -0
- package/dist/servertool/handlers/vision.d.ts +1 -0
- package/dist/servertool/handlers/vision.js +194 -0
- package/dist/servertool/handlers/web-search.d.ts +1 -0
- package/dist/servertool/handlers/web-search.js +791 -0
- package/dist/servertool/orchestration-types.d.ts +33 -0
- package/dist/servertool/orchestration-types.js +1 -0
- package/dist/servertool/registry.d.ts +18 -0
- package/dist/servertool/registry.js +27 -0
- package/dist/servertool/server-side-tools.d.ts +8 -0
- package/dist/servertool/server-side-tools.js +208 -0
- package/dist/servertool/types.d.ts +94 -0
- package/dist/servertool/types.js +1 -0
- package/dist/servertool/vision-tool.d.ts +2 -0
- package/dist/servertool/vision-tool.js +185 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
- package/package.json +1 -1
|
@@ -111,6 +111,7 @@ function applyLocalToolGovernance(chatRequest, rawPayload) {
|
|
|
111
111
|
}
|
|
112
112
|
const hasImageHint = detectImageHint(messages, rawPayload);
|
|
113
113
|
if (hasImageHint) {
|
|
114
|
+
// 有图片线索时不干预工具列表,保持由上游(Codex 等)决定 view_image 的暴露与使用。
|
|
114
115
|
return chatRequest;
|
|
115
116
|
}
|
|
116
117
|
const filteredTools = tools.filter((tool) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Ensure assistant.tool_calls[].function.arguments is a JSON string containing valid JSON.
|
|
4
|
+
* - If arguments is not a string, JSON.stringify it.
|
|
5
|
+
* - If arguments is a string but not parseable as JSON, wrap it into a JSON object so the
|
|
6
|
+
* provider always receives syntactically valid JSON (e.g. {"input": "<raw>"}).
|
|
7
|
+
* Also set assistant.content=null when tool_calls exist (request-side invariant).
|
|
8
|
+
*/
|
|
9
|
+
export declare class RequestToolCallsStringifyFilter implements Filter<JsonObject> {
|
|
10
|
+
readonly name = "request_toolcalls_stringify";
|
|
11
|
+
readonly stage: FilterContext['stage'];
|
|
12
|
+
apply(input: JsonObject): FilterResult<JsonObject>;
|
|
13
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ensure assistant.tool_calls[].function.arguments is a JSON string.
|
|
2
|
+
* Ensure assistant.tool_calls[].function.arguments is a JSON string containing valid JSON.
|
|
3
|
+
* - If arguments is not a string, JSON.stringify it.
|
|
4
|
+
* - If arguments is a string but not parseable as JSON, wrap it into a JSON object so the
|
|
5
|
+
* provider always receives syntactically valid JSON (e.g. {"input": "<raw>"}).
|
|
3
6
|
* Also set assistant.content=null when tool_calls exist (request-side invariant).
|
|
4
7
|
*/
|
|
5
8
|
export class RequestToolCallsStringifyFilter {
|
|
@@ -18,14 +21,111 @@ export class RequestToolCallsStringifyFilter {
|
|
|
18
21
|
if (!tc || typeof tc !== 'object')
|
|
19
22
|
continue;
|
|
20
23
|
const fn = tc.function || {};
|
|
21
|
-
if (fn
|
|
24
|
+
if (!fn || typeof fn !== 'object')
|
|
25
|
+
continue;
|
|
26
|
+
const currentArgs = fn.arguments;
|
|
27
|
+
const fnName = typeof fn.name === 'string' ? fn.name.trim() : '';
|
|
28
|
+
// Case 1: non-string arguments → stringify directly
|
|
29
|
+
if (currentArgs !== undefined && typeof currentArgs !== 'string') {
|
|
22
30
|
try {
|
|
23
|
-
fn.arguments = JSON.stringify(
|
|
31
|
+
fn.arguments = JSON.stringify(currentArgs ?? {});
|
|
24
32
|
}
|
|
25
33
|
catch {
|
|
26
34
|
fn.arguments = '{}';
|
|
27
35
|
}
|
|
28
36
|
tc.function = fn;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Case 2: string arguments → ensure it is valid JSON
|
|
40
|
+
if (typeof currentArgs === 'string') {
|
|
41
|
+
const trimmed = currentArgs.trim();
|
|
42
|
+
if (trimmed.length === 0) {
|
|
43
|
+
fn.arguments = '{}';
|
|
44
|
+
tc.function = fn;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let parsedOk = false;
|
|
48
|
+
let parsedValue = undefined;
|
|
49
|
+
try {
|
|
50
|
+
parsedValue = JSON.parse(trimmed);
|
|
51
|
+
parsedOk = true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
parsedOk = false;
|
|
55
|
+
}
|
|
56
|
+
if (!parsedOk) {
|
|
57
|
+
// Wrap raw string into a JSON object to keep payload syntactically valid.
|
|
58
|
+
// For shell, align with GLM/统一工具治理约定,优先映射到 { command },
|
|
59
|
+
// 其余模型仍使用 { input } 形式。
|
|
60
|
+
try {
|
|
61
|
+
if (fnName === 'shell') {
|
|
62
|
+
fn.arguments = JSON.stringify({ command: currentArgs });
|
|
63
|
+
}
|
|
64
|
+
else if (fnName === 'apply_patch') {
|
|
65
|
+
fn.arguments = JSON.stringify({ patch: currentArgs });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
fn.arguments = JSON.stringify({ input: currentArgs });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
fn.arguments = '{}';
|
|
73
|
+
}
|
|
74
|
+
tc.function = fn;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// 已经是合法 JSON 的场景下,仅对特定工具做形状修复。
|
|
78
|
+
if (parsedOk && fnName === 'apply_patch') {
|
|
79
|
+
try {
|
|
80
|
+
let obj = parsedValue;
|
|
81
|
+
// 1) 若整体是字符串,则视为补丁文本
|
|
82
|
+
if (typeof obj === 'string') {
|
|
83
|
+
obj = { patch: obj };
|
|
84
|
+
}
|
|
85
|
+
if (obj && typeof obj === 'object') {
|
|
86
|
+
const container = obj;
|
|
87
|
+
const rawPatch = container.patch;
|
|
88
|
+
const rawInput = container.input;
|
|
89
|
+
// 2) 若 patch 是形如 JSON 的字符串,尝试解包 {"input": "..."} 或 {"patch": "..."}
|
|
90
|
+
if (typeof rawPatch === 'string') {
|
|
91
|
+
const ptrim = rawPatch.trim();
|
|
92
|
+
if (ptrim.startsWith('{') && ptrim.endsWith('}')) {
|
|
93
|
+
try {
|
|
94
|
+
const inner = JSON.parse(ptrim);
|
|
95
|
+
if (typeof inner.patch === 'string') {
|
|
96
|
+
container.patch = inner.patch;
|
|
97
|
+
}
|
|
98
|
+
else if (typeof inner.input === 'string') {
|
|
99
|
+
container.patch = inner.input;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
container.patch = ptrim;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
container.patch = rawPatch;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (rawPatch === undefined && typeof rawInput === 'string') {
|
|
111
|
+
// 3) 若只有 input 字段,则复制一份到 patch,避免双层包装
|
|
112
|
+
container.patch = rawInput;
|
|
113
|
+
}
|
|
114
|
+
fn.arguments = JSON.stringify(container);
|
|
115
|
+
tc.function = fn;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// 回退到原始字符串形状
|
|
121
|
+
fn.arguments = trimmed;
|
|
122
|
+
tc.function = fn;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 其它合法 JSON 场景保持原样
|
|
127
|
+
fn.arguments = trimmed;
|
|
128
|
+
tc.function = fn;
|
|
29
129
|
}
|
|
30
130
|
}
|
|
31
131
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonicalize assistant textual tool markup into tool_calls (Chat path).
|
|
4
|
+
*
|
|
5
|
+
* 行为分两步:
|
|
6
|
+
* 1. 使用 text-markup-normalizer 将纯文本中的工具标记(含 apply_patch)提升为 tool_calls;
|
|
7
|
+
* 2. 再通过 canonicalizeChatResponseTools 统一 tool_calls 形态(content=null、arguments 为字符串等)。
|
|
8
|
+
*
|
|
9
|
+
* 这样可以保证:无论上游 provider 是哪种协议,只要在文本中输出符合规范的 apply_patch / shell 等片段,
|
|
10
|
+
* 在响应侧都会被折叠为标准的 tool_calls,供后续工具治理与客户端透明消费。
|
|
11
|
+
*/
|
|
12
|
+
export declare class ResponseToolTextCanonicalizeFilter implements Filter<JsonObject> {
|
|
13
|
+
readonly name = "response_tool_text_canonicalize";
|
|
14
|
+
readonly stage: FilterContext['stage'];
|
|
15
|
+
apply(input: JsonObject): FilterResult<JsonObject>;
|
|
16
|
+
}
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canonicalize assistant textual tool markup into tool_calls (Chat path).
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 行为分两步:
|
|
5
|
+
* 1. 使用 text-markup-normalizer 将纯文本中的工具标记(含 apply_patch)提升为 tool_calls;
|
|
6
|
+
* 2. 再通过 canonicalizeChatResponseTools 统一 tool_calls 形态(content=null、arguments 为字符串等)。
|
|
7
|
+
*
|
|
8
|
+
* 这样可以保证:无论上游 provider 是哪种协议,只要在文本中输出符合规范的 apply_patch / shell 等片段,
|
|
9
|
+
* 在响应侧都会被折叠为标准的 tool_calls,供后续工具治理与客户端透明消费。
|
|
4
10
|
*/
|
|
5
11
|
export class ResponseToolTextCanonicalizeFilter {
|
|
6
12
|
name = 'response_tool_text_canonicalize';
|
|
7
13
|
stage = 'response_pre';
|
|
8
14
|
apply(input) {
|
|
9
15
|
try {
|
|
10
|
-
// Defer to existing canonicalizer for behavior parity
|
|
11
16
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
17
|
const { canonicalizeChatResponseTools } = require('../../conversion/shared/tool-canonicalizer.js');
|
|
13
|
-
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
19
|
+
const { normalizeAssistantTextToToolCalls } = require('../../conversion/shared/text-markup-normalizer.js');
|
|
20
|
+
// 先在文本层面抽取工具调用(apply_patch / shell / MCP 等)
|
|
21
|
+
let working = input && typeof input === 'object' ? JSON.parse(JSON.stringify(input)) : input;
|
|
22
|
+
try {
|
|
23
|
+
const choices = Array.isArray(working?.choices) ? working.choices : [];
|
|
24
|
+
for (const ch of choices) {
|
|
25
|
+
if (!ch || typeof ch !== 'object')
|
|
26
|
+
continue;
|
|
27
|
+
const msg = ch.message;
|
|
28
|
+
if (msg && typeof msg === 'object') {
|
|
29
|
+
ch.message = normalizeAssistantTextToToolCalls(msg);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
working.choices = choices;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// best-effort:文本解析失败时保留原始 payload
|
|
36
|
+
}
|
|
37
|
+
const out = canonicalizeChatResponseTools(working);
|
|
14
38
|
return { ok: true, data: out };
|
|
15
39
|
}
|
|
16
40
|
catch {
|
|
@@ -27,7 +27,7 @@ export function bootstrapVirtualRouterConfig(input) {
|
|
|
27
27
|
if (!Object.keys(routingSource).length) {
|
|
28
28
|
throw new VirtualRouterError('Virtual Router routing table cannot be empty', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
29
29
|
}
|
|
30
|
-
const webSearch = normalizeWebSearch(section.webSearch);
|
|
30
|
+
const webSearch = normalizeWebSearch(section.webSearch, routingSource);
|
|
31
31
|
validateWebSearchRouting(webSearch, routingSource);
|
|
32
32
|
const { runtimeEntries, aliasIndex } = buildProviderRuntimeEntries(providersSource);
|
|
33
33
|
const { routing, targetKeys } = expandRoutingTable(routingSource, aliasIndex);
|
|
@@ -121,7 +121,8 @@ function buildProviderRuntimeEntries(providers) {
|
|
|
121
121
|
streaming: normalizedProvider.streaming,
|
|
122
122
|
modelStreaming: normalizedProvider.modelStreaming,
|
|
123
123
|
modelContextTokens: normalizedProvider.modelContextTokens,
|
|
124
|
-
defaultContextTokens: normalizedProvider.defaultContextTokens
|
|
124
|
+
defaultContextTokens: normalizedProvider.defaultContextTokens,
|
|
125
|
+
...(normalizedProvider.serverToolsDisabled ? { serverToolsDisabled: true } : {})
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
}
|
|
@@ -158,7 +159,8 @@ function expandRoutingTable(routingSource, aliasIndex) {
|
|
|
158
159
|
id: pool.id,
|
|
159
160
|
priority: pool.priority,
|
|
160
161
|
backup: pool.backup,
|
|
161
|
-
targets: expandedTargets
|
|
162
|
+
targets: expandedTargets,
|
|
163
|
+
...(pool.force ? { force: true } : {})
|
|
162
164
|
});
|
|
163
165
|
}
|
|
164
166
|
}
|
|
@@ -194,7 +196,8 @@ function buildProviderProfiles(targetKeys, runtimeEntries) {
|
|
|
194
196
|
processMode: runtime.processMode || 'chat',
|
|
195
197
|
responsesConfig: runtime.responsesConfig,
|
|
196
198
|
streaming: streamingPref,
|
|
197
|
-
maxContextTokens: contextTokens
|
|
199
|
+
maxContextTokens: contextTokens,
|
|
200
|
+
...(runtime.serverToolsDisabled ? { serverToolsDisabled: true } : {})
|
|
198
201
|
};
|
|
199
202
|
targetRuntime[targetKey] = {
|
|
200
203
|
...runtime,
|
|
@@ -274,12 +277,15 @@ function normalizeRoutePoolEntry(routeName, entry, index, total) {
|
|
|
274
277
|
(typeof record.type === 'string' && record.type.toLowerCase() === 'backup');
|
|
275
278
|
const priority = normalizePriorityValue(record.priority, total - index);
|
|
276
279
|
const targets = normalizeRouteTargets(record);
|
|
280
|
+
const force = record.force === true ||
|
|
281
|
+
(typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true');
|
|
277
282
|
return targets.length
|
|
278
283
|
? {
|
|
279
284
|
id,
|
|
280
285
|
priority,
|
|
281
286
|
backup,
|
|
282
|
-
targets
|
|
287
|
+
targets,
|
|
288
|
+
...(force ? { force: true } : {})
|
|
283
289
|
}
|
|
284
290
|
: null;
|
|
285
291
|
}
|
|
@@ -386,6 +392,12 @@ function normalizeProvider(providerId, raw) {
|
|
|
386
392
|
const streaming = resolveProviderStreamingPreference(provider, responsesNode);
|
|
387
393
|
const modelStreaming = normalizeModelStreaming(provider);
|
|
388
394
|
const { modelContextTokens, defaultContextTokens } = normalizeModelContextTokens(provider);
|
|
395
|
+
const serverToolsDisabled = provider.serverToolsDisabled === true ||
|
|
396
|
+
(typeof provider.serverToolsDisabled === 'string' &&
|
|
397
|
+
provider.serverToolsDisabled.trim().toLowerCase() === 'true') ||
|
|
398
|
+
(provider.serverTools &&
|
|
399
|
+
typeof provider.serverTools === 'object' &&
|
|
400
|
+
provider.serverTools.enabled === false);
|
|
389
401
|
return {
|
|
390
402
|
providerId,
|
|
391
403
|
providerType,
|
|
@@ -398,7 +410,8 @@ function normalizeProvider(providerId, raw) {
|
|
|
398
410
|
streaming,
|
|
399
411
|
modelStreaming,
|
|
400
412
|
modelContextTokens,
|
|
401
|
-
defaultContextTokens
|
|
413
|
+
defaultContextTokens,
|
|
414
|
+
...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
|
|
402
415
|
};
|
|
403
416
|
}
|
|
404
417
|
function normalizeModelStreaming(provider) {
|
|
@@ -539,9 +552,9 @@ function validateWebSearchRouting(webSearch, routingSource) {
|
|
|
539
552
|
if (!webSearch) {
|
|
540
553
|
return;
|
|
541
554
|
}
|
|
542
|
-
const routePools = routingSource['web_search'];
|
|
555
|
+
const routePools = routingSource['web_search'] ?? routingSource['search'];
|
|
543
556
|
if (!Array.isArray(routePools) || !routePools.length) {
|
|
544
|
-
throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
557
|
+
throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search (or search) route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
545
558
|
}
|
|
546
559
|
const targets = new Set();
|
|
547
560
|
for (const pool of routePools) {
|
|
@@ -556,11 +569,11 @@ function validateWebSearchRouting(webSearch, routingSource) {
|
|
|
556
569
|
}
|
|
557
570
|
for (const engine of webSearch.engines) {
|
|
558
571
|
if (!targets.has(engine.providerKey)) {
|
|
559
|
-
throw new VirtualRouterError(`Virtual Router webSearch engine "${engine.id}" references providerKey "${engine.providerKey}" which is not present in routing.web_search`, VirtualRouterErrorCode.CONFIG_ERROR);
|
|
572
|
+
throw new VirtualRouterError(`Virtual Router webSearch engine "${engine.id}" references providerKey "${engine.providerKey}" which is not present in routing.web_search/search`, VirtualRouterErrorCode.CONFIG_ERROR);
|
|
560
573
|
}
|
|
561
574
|
}
|
|
562
575
|
}
|
|
563
|
-
function normalizeWebSearch(input) {
|
|
576
|
+
function normalizeWebSearch(input, routingSource) {
|
|
564
577
|
if (!input || typeof input !== 'object') {
|
|
565
578
|
return undefined;
|
|
566
579
|
}
|
|
@@ -588,6 +601,12 @@ function normalizeWebSearch(input) {
|
|
|
588
601
|
: undefined;
|
|
589
602
|
const isDefault = node.default === true ||
|
|
590
603
|
(typeof node.default === 'string' && node.default.trim().toLowerCase() === 'true');
|
|
604
|
+
const serverToolsDisabled = node.serverToolsDisabled === true ||
|
|
605
|
+
(typeof node.serverToolsDisabled === 'string' &&
|
|
606
|
+
node.serverToolsDisabled.trim().toLowerCase() === 'true') ||
|
|
607
|
+
(node.serverTools &&
|
|
608
|
+
typeof node.serverTools === 'object' &&
|
|
609
|
+
node.serverTools.enabled === false);
|
|
591
610
|
// Deduplicate by id; first wins, subsequent are ignored.
|
|
592
611
|
if (engines.some((engine) => engine.id === id)) {
|
|
593
612
|
continue;
|
|
@@ -596,13 +615,15 @@ function normalizeWebSearch(input) {
|
|
|
596
615
|
id,
|
|
597
616
|
providerKey,
|
|
598
617
|
description,
|
|
599
|
-
default: isDefault
|
|
618
|
+
default: isDefault,
|
|
619
|
+
...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
|
|
600
620
|
});
|
|
601
621
|
}
|
|
602
622
|
if (!engines.length) {
|
|
603
623
|
return undefined;
|
|
604
624
|
}
|
|
605
625
|
let injectPolicy;
|
|
626
|
+
let force;
|
|
606
627
|
const rawPolicy = record.injectPolicy ?? record?.inject_policy;
|
|
607
628
|
if (typeof rawPolicy === 'string') {
|
|
608
629
|
const normalized = rawPolicy.trim().toLowerCase();
|
|
@@ -610,9 +631,20 @@ function normalizeWebSearch(input) {
|
|
|
610
631
|
injectPolicy = normalized;
|
|
611
632
|
}
|
|
612
633
|
}
|
|
634
|
+
if (record.force === true ||
|
|
635
|
+
(typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true')) {
|
|
636
|
+
force = true;
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
const webSearchPools = routingSource['web_search'] ?? routingSource['search'] ?? [];
|
|
640
|
+
if (Array.isArray(webSearchPools) && webSearchPools.some((pool) => pool.force)) {
|
|
641
|
+
force = true;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
613
644
|
return {
|
|
614
645
|
engines,
|
|
615
|
-
injectPolicy: injectPolicy ?? 'selective'
|
|
646
|
+
injectPolicy: injectPolicy ?? 'selective',
|
|
647
|
+
...(force ? { force } : {})
|
|
616
648
|
};
|
|
617
649
|
}
|
|
618
650
|
function extractProviderAuthEntries(providerId, raw) {
|
|
@@ -17,34 +17,30 @@ export class RoutingClassifier {
|
|
|
17
17
|
const thinkingContinuation = lastToolCategory === 'read';
|
|
18
18
|
const searchContinuation = lastToolCategory === 'search';
|
|
19
19
|
const toolsContinuation = lastToolCategory === 'other';
|
|
20
|
-
if (latestMessageFromUser) {
|
|
21
|
-
const reasoning = 'thinking:user-input';
|
|
22
|
-
const evaluations = {
|
|
23
|
-
thinking: { triggered: true, reason: reasoning }
|
|
24
|
-
};
|
|
25
|
-
const candidates = this.ensureDefaultCandidate(['thinking']);
|
|
26
|
-
return this.buildResult('thinking', reasoning, evaluations, candidates);
|
|
27
|
-
}
|
|
28
20
|
const evaluationMap = {
|
|
29
21
|
vision: {
|
|
30
|
-
triggered: features.
|
|
31
|
-
reason: 'vision:
|
|
22
|
+
triggered: features.hasImageAttachment,
|
|
23
|
+
reason: 'vision:image-detected'
|
|
24
|
+
},
|
|
25
|
+
thinking: {
|
|
26
|
+
triggered: latestMessageFromUser,
|
|
27
|
+
reason: 'thinking:user-input'
|
|
32
28
|
},
|
|
33
29
|
longcontext: {
|
|
34
30
|
triggered: reachedLongContext,
|
|
35
31
|
reason: 'longcontext:token-threshold'
|
|
36
32
|
},
|
|
37
|
-
websearch: {
|
|
38
|
-
triggered: features.hasWebTool || searchContinuation,
|
|
39
|
-
reason: searchContinuation ? 'websearch:last-tool-search' : 'websearch:web-tools-detected'
|
|
40
|
-
},
|
|
41
33
|
coding: {
|
|
42
34
|
triggered: codingContinuation,
|
|
43
35
|
reason: 'coding:last-tool-write'
|
|
44
36
|
},
|
|
45
|
-
|
|
46
|
-
triggered: thinkingContinuation
|
|
47
|
-
reason:
|
|
37
|
+
thinking_continuation: {
|
|
38
|
+
triggered: thinkingContinuation,
|
|
39
|
+
reason: 'thinking:last-tool-read'
|
|
40
|
+
},
|
|
41
|
+
search: {
|
|
42
|
+
triggered: searchContinuation,
|
|
43
|
+
reason: 'search:last-tool-search'
|
|
48
44
|
},
|
|
49
45
|
tools: {
|
|
50
46
|
triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
|
|
@@ -12,6 +12,8 @@ export declare class VirtualRouterEngine {
|
|
|
12
12
|
private readonly debug;
|
|
13
13
|
private healthConfig;
|
|
14
14
|
private readonly statsCenter;
|
|
15
|
+
private webSearchForce;
|
|
16
|
+
private routingInstructionState;
|
|
15
17
|
initialize(config: VirtualRouterConfig): void;
|
|
16
18
|
route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
|
|
17
19
|
target: TargetMetadata;
|
|
@@ -28,6 +30,11 @@ export declare class VirtualRouterEngine {
|
|
|
28
30
|
}>;
|
|
29
31
|
health: import("./types.js").ProviderHealthState[];
|
|
30
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* 将分类器产生的逻辑路由名直接归一化为配置中的路由键。
|
|
35
|
+
* 不再维护 "websearch" 之类的别名,调用方应显式使用 "web_search" 或 "search" 等实际路由名。
|
|
36
|
+
*/
|
|
37
|
+
private normalizeRouteAlias;
|
|
31
38
|
private validateConfig;
|
|
32
39
|
private selectProvider;
|
|
33
40
|
private trySelectFromTier;
|
|
@@ -37,16 +44,48 @@ export declare class VirtualRouterEngine {
|
|
|
37
44
|
private buildContextCandidatePools;
|
|
38
45
|
private describeAttempt;
|
|
39
46
|
private resolveStickyKey;
|
|
47
|
+
private resolveSessionScope;
|
|
48
|
+
private getRoutingInstructionState;
|
|
49
|
+
private buildMetadataInstructions;
|
|
50
|
+
private parseMetadataDisableDescriptor;
|
|
51
|
+
private resolveRoutingMode;
|
|
52
|
+
private resolveInstructionTarget;
|
|
53
|
+
private filterCandidatesByRoutingState;
|
|
54
|
+
private selectFromCandidates;
|
|
55
|
+
private extractProviderId;
|
|
56
|
+
/**
|
|
57
|
+
* 在已有候选路由集合上,筛选出真正挂载了 sticky 池内 providerKey 的路由,
|
|
58
|
+
* 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
|
|
59
|
+
* sticky 模式,就不会再命中独立的 tools 池(例如 glm/qwen 工具模型)。
|
|
60
|
+
* 若候选集合中完全没有挂载 sticky key 的路由,则尝试在 default 路由上兜底。
|
|
61
|
+
*/
|
|
62
|
+
private buildStickyRouteCandidatesFromFiltered;
|
|
63
|
+
/**
|
|
64
|
+
* 在 sticky 模式下,仅在 sticky 池内选择 Provider:
|
|
65
|
+
* - stickyKeySet 表示已经解析并通过健康检查的 providerKey 集合;
|
|
66
|
+
* - 不再依赖 routing[*].targets 中是否挂载这些 key,避免「未初始化路由池」导致 sticky 池为空;
|
|
67
|
+
* - 仍然尊重 allowed/disabledProviders、disabledKeys、disabledModels 以及上下文长度。
|
|
68
|
+
*/
|
|
69
|
+
private selectFromStickyPool;
|
|
70
|
+
private extractKeyAlias;
|
|
71
|
+
private normalizeAliasDescriptor;
|
|
72
|
+
private extractKeyIndex;
|
|
73
|
+
private getProviderModelId;
|
|
40
74
|
private mapProviderError;
|
|
41
75
|
private deriveReason;
|
|
42
76
|
private buildRouteCandidates;
|
|
77
|
+
private reorderForInlineVision;
|
|
78
|
+
private routeSupportsInlineVision;
|
|
43
79
|
private sortByPriority;
|
|
44
80
|
private routeWeight;
|
|
81
|
+
private routeHasForceFlag;
|
|
45
82
|
private routeHasTargets;
|
|
46
83
|
private hasPrimaryPool;
|
|
47
84
|
private sortRoutePools;
|
|
48
85
|
private flattenPoolTargets;
|
|
49
86
|
private buildHitReason;
|
|
87
|
+
private isRoutingStateEmpty;
|
|
88
|
+
private persistRoutingInstructionState;
|
|
50
89
|
private decorateWithDetail;
|
|
51
90
|
private formatVirtualRouterHit;
|
|
52
91
|
private resolveRouteColor;
|