@jsonstudio/llms 0.6.34 → 0.6.54
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 +1 -2
- package/dist/conversion/compat/profiles/chat-glm.json +17 -0
- package/dist/conversion/compat/profiles/chat-iflow.json +36 -0
- package/dist/conversion/compat/profiles/chat-lmstudio.json +37 -0
- package/dist/conversion/compat/profiles/chat-qwen.json +18 -0
- package/dist/conversion/compat/profiles/responses-c4m.json +45 -0
- package/dist/conversion/config/compat-profiles.json +38 -0
- package/dist/conversion/config/sample-config.json +314 -0
- package/dist/conversion/config/version-switch.json +150 -0
- package/dist/conversion/hub/pipeline/compat/compat-engine.d.ts +4 -0
- package/dist/conversion/hub/pipeline/compat/compat-engine.js +667 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.d.ts +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +76 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +62 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.js +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +76 -28
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +10 -12
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.d.ts +14 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +23 -0
- package/dist/conversion/hub/response/provider-response.js +18 -0
- package/dist/conversion/hub/response/response-mappers.d.ts +1 -1
- package/dist/conversion/hub/response/response-mappers.js +2 -12
- package/dist/conversion/shared/responses-output-builder.js +22 -43
- package/dist/conversion/shared/responses-response-utils.js +1 -47
- package/dist/conversion/shared/text-markup-normalizer.js +2 -2
- package/dist/conversion/shared/tool-canonicalizer.js +16 -118
- package/dist/conversion/shared/tool-mapping.js +0 -30
- package/dist/filters/config/openai-openai.fieldmap.json +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/router/virtual-router/bootstrap.js +18 -33
- package/dist/router/virtual-router/classifier.js +51 -77
- package/dist/router/virtual-router/features.js +338 -111
- package/dist/router/virtual-router/types.d.ts +2 -4
- package/dist/router/virtual-router/types.js +2 -2
- package/dist/sse/sse-to-json/builders/response-builder.js +1 -0
- package/package.json +3 -3
|
@@ -12,133 +12,31 @@ function repairArgumentsToString(args) {
|
|
|
12
12
|
return String(args);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
function extractStringContent(value) {
|
|
16
|
-
if (typeof value === 'string') {
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
if (Array.isArray(value)) {
|
|
20
|
-
const parts = [];
|
|
21
|
-
for (const entry of value) {
|
|
22
|
-
if (typeof entry === 'string') {
|
|
23
|
-
parts.push(entry);
|
|
24
|
-
}
|
|
25
|
-
else if (entry && typeof entry === 'object') {
|
|
26
|
-
const text = entry.text;
|
|
27
|
-
if (typeof text === 'string') {
|
|
28
|
-
parts.push(text);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
if (parts.length) {
|
|
33
|
-
return parts.join('\n');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
function readApplyPatchArgument(args) {
|
|
39
|
-
if (!args || typeof args !== 'object')
|
|
40
|
-
return undefined;
|
|
41
|
-
const input = args.input;
|
|
42
|
-
if (typeof input === 'string' && input.trim().length > 0) {
|
|
43
|
-
return input;
|
|
44
|
-
}
|
|
45
|
-
const legacy = args.patch;
|
|
46
|
-
if (typeof legacy === 'string' && legacy.trim().length > 0) {
|
|
47
|
-
return legacy;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
function hasApplyPatchInput(argText) {
|
|
52
|
-
if (!argText) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
const parsed = JSON.parse(argText);
|
|
57
|
-
const content = readApplyPatchArgument(parsed);
|
|
58
|
-
return typeof content === 'string' && content.trim().length > 0;
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function extractUnifiedDiff(content) {
|
|
65
|
-
if (!content)
|
|
66
|
-
return undefined;
|
|
67
|
-
const begin = content.indexOf('*** Begin Patch');
|
|
68
|
-
const end = content.indexOf('*** End Patch');
|
|
69
|
-
if (begin >= 0 && end > begin) {
|
|
70
|
-
return content.slice(begin, end + '*** End Patch'.length).trim();
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
function createToolCallId() {
|
|
75
|
-
return `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
-
}
|
|
77
15
|
export function canonicalizeChatResponseTools(payload) {
|
|
78
16
|
try {
|
|
79
17
|
const out = isObject(payload) ? JSON.parse(JSON.stringify(payload)) : payload;
|
|
80
18
|
const choices = Array.isArray(out?.choices) ? out.choices : [];
|
|
81
19
|
for (const ch of choices) {
|
|
82
20
|
const msg = ch && ch.message ? ch.message : undefined;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const originalContentText = extractStringContent(msg?.content);
|
|
86
|
-
const harvestedPatch = typeof originalContentText === 'string' ? extractUnifiedDiff(originalContentText) : undefined;
|
|
87
|
-
if (hasToolCalls) {
|
|
88
|
-
// ensure arguments is string and content is null when tool_calls present
|
|
89
|
-
try {
|
|
90
|
-
if (!ch.finish_reason)
|
|
91
|
-
ch.finish_reason = 'tool_calls';
|
|
92
|
-
}
|
|
93
|
-
catch { /* ignore */ }
|
|
94
|
-
try {
|
|
95
|
-
if (msg && typeof msg === 'object')
|
|
96
|
-
msg.content = null;
|
|
97
|
-
}
|
|
98
|
-
catch { /* ignore */ }
|
|
99
|
-
for (const tc of toolCalls) {
|
|
100
|
-
try {
|
|
101
|
-
const fn = tc && tc.function ? tc.function : undefined;
|
|
102
|
-
if (fn) {
|
|
103
|
-
const repaired = repairArgumentsToString(fn.arguments);
|
|
104
|
-
let nextArgs = repaired;
|
|
105
|
-
try {
|
|
106
|
-
const parsed = JSON.parse(repaired);
|
|
107
|
-
const diffText = readApplyPatchArgument(parsed);
|
|
108
|
-
if (typeof diffText === 'string' && diffText.trim().length > 0) {
|
|
109
|
-
nextArgs = JSON.stringify({ input: diffText });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// fallback to repaired string
|
|
114
|
-
}
|
|
115
|
-
fn.arguments = nextArgs;
|
|
116
|
-
if (typeof fn.name === 'string' &&
|
|
117
|
-
fn.name === 'apply_patch' &&
|
|
118
|
-
!hasApplyPatchInput(fn.arguments) &&
|
|
119
|
-
harvestedPatch) {
|
|
120
|
-
fn.arguments = JSON.stringify({ input: harvestedPatch });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch { /* ignore */ }
|
|
125
|
-
}
|
|
21
|
+
const tcs = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
|
|
22
|
+
if (!tcs || !tcs.length)
|
|
126
23
|
continue;
|
|
24
|
+
// ensure arguments is string and content is null when tool_calls present
|
|
25
|
+
try {
|
|
26
|
+
if (!ch.finish_reason)
|
|
27
|
+
ch.finish_reason = 'tool_calls';
|
|
127
28
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
id,
|
|
132
|
-
type: 'function',
|
|
133
|
-
function: {
|
|
134
|
-
name: 'apply_patch',
|
|
135
|
-
arguments: JSON.stringify({ input: harvestedPatch })
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
try {
|
|
139
|
-
msg.tool_calls = [toolCall];
|
|
29
|
+
catch { /* ignore */ }
|
|
30
|
+
try {
|
|
31
|
+
if (msg && typeof msg === 'object')
|
|
140
32
|
msg.content = null;
|
|
141
|
-
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
for (const tc of tcs) {
|
|
36
|
+
try {
|
|
37
|
+
const fn = tc && tc.function ? tc.function : undefined;
|
|
38
|
+
if (fn)
|
|
39
|
+
fn.arguments = repairArgumentsToString(fn.arguments);
|
|
142
40
|
}
|
|
143
41
|
catch { /* ignore */ }
|
|
144
42
|
}
|
|
@@ -15,28 +15,6 @@ const DEFAULT_SANITIZER = (value) => {
|
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
};
|
|
18
|
-
const APPLY_PATCH_TOOL_NAME = 'apply_patch';
|
|
19
|
-
const APPLY_PATCH_ARG_KEY = 'input';
|
|
20
|
-
function isApplyPatchTool(name) {
|
|
21
|
-
return typeof name === 'string' && name.trim() === APPLY_PATCH_TOOL_NAME;
|
|
22
|
-
}
|
|
23
|
-
function buildApplyPatchParameters() {
|
|
24
|
-
return {
|
|
25
|
-
type: 'object',
|
|
26
|
-
properties: {
|
|
27
|
-
[APPLY_PATCH_ARG_KEY]: {
|
|
28
|
-
type: 'string',
|
|
29
|
-
description: 'Unified diff patch content (*** Begin Patch ... *** End Patch)'
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
required: [APPLY_PATCH_ARG_KEY],
|
|
33
|
-
additionalProperties: false
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function applyPatchSchemaToFunction(fnNode) {
|
|
37
|
-
fnNode.parameters = buildApplyPatchParameters();
|
|
38
|
-
fnNode.strict = true;
|
|
39
|
-
}
|
|
40
18
|
function resolveToolName(candidate, options) {
|
|
41
19
|
const sanitized = options?.sanitizeName?.(candidate);
|
|
42
20
|
if (typeof sanitized === 'string' && sanitized.trim().length) {
|
|
@@ -102,9 +80,6 @@ export function bridgeToolToChatDefinition(rawTool, options) {
|
|
|
102
80
|
if (strict !== undefined) {
|
|
103
81
|
fnOut.strict = strict;
|
|
104
82
|
}
|
|
105
|
-
if (isApplyPatchTool(name)) {
|
|
106
|
-
applyPatchSchemaToFunction(fnOut);
|
|
107
|
-
}
|
|
108
83
|
return {
|
|
109
84
|
type: normalizedType,
|
|
110
85
|
function: fnOut
|
|
@@ -156,11 +131,6 @@ export function chatToolToBridgeDefinition(rawTool, options) {
|
|
|
156
131
|
if (strict !== undefined) {
|
|
157
132
|
fnOut.strict = strict;
|
|
158
133
|
}
|
|
159
|
-
if (isApplyPatchTool(name)) {
|
|
160
|
-
applyPatchSchemaToFunction(fnOut);
|
|
161
|
-
responseShape.parameters = buildApplyPatchParameters();
|
|
162
|
-
responseShape.strict = true;
|
|
163
|
-
}
|
|
164
134
|
responseShape.function = fnOut;
|
|
165
135
|
return responseShape;
|
|
166
136
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"request": [
|
|
3
|
+
{
|
|
4
|
+
"sourcePath": "messages[*].tool_calls[*].function.arguments",
|
|
5
|
+
"targetPath": "messages[*].tool_calls[*].function.arguments",
|
|
6
|
+
"type": "string",
|
|
7
|
+
"transform": "stringifyJson"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"response": [
|
|
11
|
+
{
|
|
12
|
+
"sourcePath": "choices[*].message.tool_calls[*].function.arguments",
|
|
13
|
+
"targetPath": "choices[*].message.tool_calls[*].function.arguments",
|
|
14
|
+
"type": "string",
|
|
15
|
+
"transform": "stringifyJson"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,4 @@
|
|
|
7
7
|
export * from './conversion/index.js';
|
|
8
8
|
export * from './router/virtual-router/bootstrap.js';
|
|
9
9
|
export * from './router/virtual-router/types.js';
|
|
10
|
-
export { DEFAULT_THINKING_KEYWORDS } from './router/virtual-router/default-thinking-keywords.js';
|
|
11
10
|
export declare const VERSION = "0.4.0";
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,4 @@
|
|
|
7
7
|
export * from './conversion/index.js';
|
|
8
8
|
export * from './router/virtual-router/bootstrap.js';
|
|
9
9
|
export * from './router/virtual-router/types.js';
|
|
10
|
-
export { DEFAULT_THINKING_KEYWORDS } from './router/virtual-router/default-thinking-keywords.js';
|
|
11
10
|
export const VERSION = '0.4.0';
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
2
|
-
import { DEFAULT_THINKING_KEYWORDS } from './default-thinking-keywords.js';
|
|
3
|
-
const KEYWORD_INJECTION_KEYS = ['thinking', 'background', 'vision', 'coding'];
|
|
4
2
|
const DEFAULT_CLASSIFIER = {
|
|
5
3
|
longContextThresholdTokens: 180000,
|
|
6
|
-
thinkingKeywords:
|
|
4
|
+
thinkingKeywords: ['think step', 'analysis', 'reasoning', '仔细分析', '深度思考'],
|
|
7
5
|
codingKeywords: ['apply_patch', 'write_file', 'create_file', 'shell', '修改文件', '写入文件'],
|
|
8
6
|
backgroundKeywords: ['background', 'context dump', '上下文'],
|
|
9
|
-
visionKeywords: ['vision', 'image', 'picture', 'photo']
|
|
10
|
-
keywordInjections: {}
|
|
7
|
+
visionKeywords: ['vision', 'image', 'picture', 'photo']
|
|
11
8
|
};
|
|
12
9
|
const DEFAULT_LOAD_BALANCING = { strategy: 'round-robin' };
|
|
13
10
|
const DEFAULT_HEALTH = { failureThreshold: 3, cooldownMs: 30_000, fatalCooldownMs: 300_000 };
|
|
@@ -189,8 +186,7 @@ function normalizeClassifier(input) {
|
|
|
189
186
|
thinkingKeywords: normalizeStringArray(normalized.thinkingKeywords, DEFAULT_CLASSIFIER.thinkingKeywords),
|
|
190
187
|
codingKeywords: normalizeStringArray(normalized.codingKeywords, DEFAULT_CLASSIFIER.codingKeywords),
|
|
191
188
|
backgroundKeywords: normalizeStringArray(normalized.backgroundKeywords, DEFAULT_CLASSIFIER.backgroundKeywords),
|
|
192
|
-
visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords)
|
|
193
|
-
keywordInjections: normalizeKeywordInjectionMap(normalized.keywordInjections)
|
|
189
|
+
visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords)
|
|
194
190
|
};
|
|
195
191
|
return result;
|
|
196
192
|
}
|
|
@@ -201,26 +197,6 @@ function normalizeStringArray(value, fallback) {
|
|
|
201
197
|
const normalized = value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
|
|
202
198
|
return normalized.length ? normalized : [...fallback];
|
|
203
199
|
}
|
|
204
|
-
function normalizeKeywordInjectionMap(value) {
|
|
205
|
-
const map = {};
|
|
206
|
-
const record = asRecord(value);
|
|
207
|
-
if (!record) {
|
|
208
|
-
return map;
|
|
209
|
-
}
|
|
210
|
-
for (const key of KEYWORD_INJECTION_KEYS) {
|
|
211
|
-
const rawList = record[key];
|
|
212
|
-
if (!Array.isArray(rawList)) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const normalized = rawList
|
|
216
|
-
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
217
|
-
.filter(Boolean);
|
|
218
|
-
if (normalized.length) {
|
|
219
|
-
map[key] = normalized;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return map;
|
|
223
|
-
}
|
|
224
200
|
function normalizeProvider(providerId, raw) {
|
|
225
201
|
const provider = asRecord(raw);
|
|
226
202
|
const providerType = detectProviderType(provider);
|
|
@@ -232,7 +208,7 @@ function normalizeProvider(providerId, raw) {
|
|
|
232
208
|
? provider.baseUrl.trim()
|
|
233
209
|
: '';
|
|
234
210
|
const headers = normalizeHeaders(provider.headers);
|
|
235
|
-
const compatibilityProfile = resolveCompatibilityProfile(provider);
|
|
211
|
+
const compatibilityProfile = resolveCompatibilityProfile(providerId, provider);
|
|
236
212
|
const responsesConfig = normalizeResponsesConfig(provider);
|
|
237
213
|
const processMode = normalizeProcessMode(provider.process);
|
|
238
214
|
return {
|
|
@@ -257,12 +233,21 @@ function normalizeResponsesConfig(provider) {
|
|
|
257
233
|
}
|
|
258
234
|
return undefined;
|
|
259
235
|
}
|
|
260
|
-
function resolveCompatibilityProfile(provider) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
236
|
+
function resolveCompatibilityProfile(providerId, provider) {
|
|
237
|
+
if (typeof provider.compatibilityProfile === 'string' && provider.compatibilityProfile.trim()) {
|
|
238
|
+
return provider.compatibilityProfile.trim();
|
|
239
|
+
}
|
|
240
|
+
const legacyFields = [];
|
|
241
|
+
if (typeof provider.compat === 'string') {
|
|
242
|
+
legacyFields.push('compat');
|
|
243
|
+
}
|
|
244
|
+
if (typeof provider.compatibility_profile === 'string') {
|
|
245
|
+
legacyFields.push('compatibility_profile');
|
|
246
|
+
}
|
|
247
|
+
if (legacyFields.length > 0) {
|
|
248
|
+
throw new VirtualRouterError(`Provider "${providerId}" uses legacy compatibility field(s): ${legacyFields.join(', ')}. Rename to "compatibilityProfile".`, VirtualRouterErrorCode.CONFIG_ERROR);
|
|
264
249
|
}
|
|
265
|
-
return '
|
|
250
|
+
return 'compat:passthrough';
|
|
266
251
|
}
|
|
267
252
|
function normalizeProcessMode(value) {
|
|
268
253
|
if (typeof value !== 'string') {
|
|
@@ -1,73 +1,67 @@
|
|
|
1
1
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY } from './types.js';
|
|
2
|
-
import { DEFAULT_THINKING_KEYWORDS } from './default-thinking-keywords.js';
|
|
3
2
|
const DEFAULT_LONG_CONTEXT_THRESHOLD = 180000;
|
|
4
3
|
export class RoutingClassifier {
|
|
5
4
|
config;
|
|
6
5
|
constructor(config) {
|
|
7
|
-
const keywordConfig = normalizeKeywordConfig(config);
|
|
8
6
|
this.config = {
|
|
9
7
|
longContextThresholdTokens: config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD,
|
|
10
|
-
thinkingKeywords:
|
|
11
|
-
backgroundKeywords:
|
|
12
|
-
visionKeywords: keywordConfig.vision,
|
|
13
|
-
codingKeywords: keywordConfig.coding
|
|
8
|
+
thinkingKeywords: normalizeList(config.thinkingKeywords, ['think step', 'analysis', 'reasoning']),
|
|
9
|
+
backgroundKeywords: normalizeList(config.backgroundKeywords, ['background', 'context dump'])
|
|
14
10
|
};
|
|
15
11
|
}
|
|
16
12
|
classify(features) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const lastToolCategory = features.lastAssistantToolCategory;
|
|
14
|
+
const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
|
|
15
|
+
const thinkingKeywordHit = features.hasThinkingKeyword ||
|
|
16
|
+
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []);
|
|
17
|
+
const codingContinuation = lastToolCategory === 'write';
|
|
18
|
+
const thinkingContinuation = lastToolCategory === 'read';
|
|
19
|
+
const searchContinuation = lastToolCategory === 'search';
|
|
20
|
+
const toolsContinuation = lastToolCategory === 'other';
|
|
21
|
+
const evaluationMap = {
|
|
22
|
+
vision: {
|
|
23
|
+
triggered: features.hasVisionTool && features.hasImageAttachment,
|
|
24
|
+
reason: 'vision:requires-tool+image'
|
|
22
25
|
},
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []) ||
|
|
27
|
-
features.previousToolCategory === 'context_read' ||
|
|
28
|
-
features.previousToolCategory === 'plan',
|
|
29
|
-
reason: 'thinking:keywords'
|
|
26
|
+
longcontext: {
|
|
27
|
+
triggered: reachedLongContext,
|
|
28
|
+
reason: 'longcontext:token-threshold'
|
|
30
29
|
},
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
reason: 'background:keywords'
|
|
30
|
+
websearch: {
|
|
31
|
+
triggered: features.hasWebTool || searchContinuation,
|
|
32
|
+
reason: searchContinuation ? 'websearch:last-tool-search' : 'websearch:web-tools-detected'
|
|
35
33
|
},
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
features.previousToolCategory === 'vision',
|
|
40
|
-
reason: 'vision:requires-tool+image'
|
|
34
|
+
coding: {
|
|
35
|
+
triggered: codingContinuation,
|
|
36
|
+
reason: 'coding:last-tool-write'
|
|
41
37
|
},
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reason: 'websearch:web-tools-detected'
|
|
38
|
+
thinking: {
|
|
39
|
+
triggered: thinkingContinuation || thinkingKeywordHit,
|
|
40
|
+
reason: thinkingContinuation ? 'thinking:last-tool-read' : 'thinking:keywords'
|
|
46
41
|
},
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
containsKeywords(features.userTextSample, this.config.codingKeywords ?? []) ||
|
|
51
|
-
features.previousToolCategory === 'coding',
|
|
52
|
-
reason: 'coding:coding-tools-detected'
|
|
42
|
+
tools: {
|
|
43
|
+
triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
|
|
44
|
+
reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
|
|
53
45
|
},
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
background: {
|
|
47
|
+
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
48
|
+
reason: 'background:keywords'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
for (const routeName of ROUTE_PRIORITY) {
|
|
52
|
+
const evaluation = evaluationMap[routeName];
|
|
53
|
+
if (evaluation && evaluation.triggered) {
|
|
54
|
+
const candidates = this.ensureDefaultCandidate([routeName]);
|
|
55
|
+
return this.buildResult(routeName, evaluation.reason, evaluationMap, candidates);
|
|
58
56
|
}
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const chosenRoute = orderedRoutes.length ? orderedRoutes[0] : DEFAULT_ROUTE;
|
|
63
|
-
const chosenReason = triggeredEvaluations.find((entry) => entry.route === chosenRoute)?.reason || 'fallback:default';
|
|
64
|
-
const candidates = this.ensureDefaultCandidate(orderedRoutes);
|
|
65
|
-
return this.buildResult(chosenRoute, chosenReason, evaluations, candidates);
|
|
57
|
+
}
|
|
58
|
+
const candidates = this.ensureDefaultCandidate([DEFAULT_ROUTE]);
|
|
59
|
+
return this.buildResult(DEFAULT_ROUTE, 'fallback:default', evaluationMap, candidates);
|
|
66
60
|
}
|
|
67
61
|
buildResult(routeName, chosenReason, evaluations, candidates) {
|
|
68
|
-
const diagnostics = evaluations
|
|
69
|
-
.filter((evaluation) => evaluation.triggered)
|
|
70
|
-
.map((evaluation) => evaluation.reason);
|
|
62
|
+
const diagnostics = Object.entries(evaluations)
|
|
63
|
+
.filter(([_, evaluation]) => evaluation.triggered)
|
|
64
|
+
.map(([_, evaluation]) => evaluation.reason);
|
|
71
65
|
const reasoningParts = [chosenReason, ...diagnostics.filter((reason) => reason !== chosenReason)];
|
|
72
66
|
return {
|
|
73
67
|
routeName,
|
|
@@ -93,6 +87,12 @@ export class RoutingClassifier {
|
|
|
93
87
|
return index >= 0 ? index : ROUTE_PRIORITY.length;
|
|
94
88
|
}
|
|
95
89
|
}
|
|
90
|
+
function normalizeList(source, fallback) {
|
|
91
|
+
if (!source || source.length === 0) {
|
|
92
|
+
return fallback;
|
|
93
|
+
}
|
|
94
|
+
return source.map((item) => item.toLowerCase());
|
|
95
|
+
}
|
|
96
96
|
function containsKeywords(text, keywords) {
|
|
97
97
|
if (!text || !keywords.length) {
|
|
98
98
|
return false;
|
|
@@ -100,29 +100,3 @@ function containsKeywords(text, keywords) {
|
|
|
100
100
|
const normalized = text.toLowerCase();
|
|
101
101
|
return keywords.some((keyword) => normalized.includes(keyword));
|
|
102
102
|
}
|
|
103
|
-
function normalizeKeywordConfig(config) {
|
|
104
|
-
const injections = config.keywordInjections ?? {};
|
|
105
|
-
return {
|
|
106
|
-
thinking: mergeKeywordLists(DEFAULT_THINKING_KEYWORDS, config.thinkingKeywords, injections.thinking),
|
|
107
|
-
background: mergeKeywordLists(['background', 'context dump', '上下文'], config.backgroundKeywords, injections.background),
|
|
108
|
-
vision: mergeKeywordLists(['vision', 'image', 'picture', 'photo'], config.visionKeywords, injections.vision),
|
|
109
|
-
coding: mergeKeywordLists(config.codingKeywords, injections.coding)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function mergeKeywordLists(...lists) {
|
|
113
|
-
const set = new Set();
|
|
114
|
-
for (const list of lists) {
|
|
115
|
-
if (!Array.isArray(list)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
for (const item of list) {
|
|
119
|
-
if (!item)
|
|
120
|
-
continue;
|
|
121
|
-
const normalized = String(item).toLowerCase();
|
|
122
|
-
if (normalized) {
|
|
123
|
-
set.add(normalized);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return Array.from(set);
|
|
128
|
-
}
|