@jsonstudio/llms 0.6.6 → 0.6.34
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 +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +31 -7
- package/dist/router/virtual-router/classifier.js +56 -27
- package/dist/router/virtual-router/default-thinking-keywords.d.ts +1 -0
- package/dist/router/virtual-router/default-thinking-keywords.js +13 -0
- package/dist/router/virtual-router/features.js +110 -8
- package/dist/router/virtual-router/token-counter.d.ts +2 -0
- package/dist/router/virtual-router/token-counter.js +105 -0
- package/dist/router/virtual-router/types.d.ts +4 -0
- package/dist/router/virtual-router/types.js +2 -2
- package/package.json +1 -1
|
@@ -262,6 +262,7 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
262
262
|
catch {
|
|
263
263
|
// best-effort policy execution
|
|
264
264
|
}
|
|
265
|
+
const normalizedFinishReason = toolCalls.length ? 'tool_calls' : finish_reason;
|
|
265
266
|
const chatResp = {
|
|
266
267
|
id: payload?.id || `chatcmpl_${Date.now()}`,
|
|
267
268
|
object: 'chat.completion',
|
|
@@ -269,7 +270,7 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
269
270
|
choices: [
|
|
270
271
|
{
|
|
271
272
|
index: 0,
|
|
272
|
-
finish_reason,
|
|
273
|
+
finish_reason: normalizedFinishReason,
|
|
273
274
|
message: chatMsg
|
|
274
275
|
}
|
|
275
276
|
]
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
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';
|
|
10
11
|
export declare const VERSION = "0.4.0";
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
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';
|
|
10
11
|
export const VERSION = '0.4.0';
|
|
@@ -1,10 +1,13 @@
|
|
|
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'];
|
|
2
4
|
const DEFAULT_CLASSIFIER = {
|
|
3
|
-
longContextThresholdTokens:
|
|
4
|
-
thinkingKeywords:
|
|
5
|
+
longContextThresholdTokens: 180000,
|
|
6
|
+
thinkingKeywords: DEFAULT_THINKING_KEYWORDS,
|
|
5
7
|
codingKeywords: ['apply_patch', 'write_file', 'create_file', 'shell', '修改文件', '写入文件'],
|
|
6
8
|
backgroundKeywords: ['background', 'context dump', '上下文'],
|
|
7
|
-
visionKeywords: ['vision', 'image', 'picture', 'photo']
|
|
9
|
+
visionKeywords: ['vision', 'image', 'picture', 'photo'],
|
|
10
|
+
keywordInjections: {}
|
|
8
11
|
};
|
|
9
12
|
const DEFAULT_LOAD_BALANCING = { strategy: 'round-robin' };
|
|
10
13
|
const DEFAULT_HEALTH = { failureThreshold: 3, cooldownMs: 30_000, fatalCooldownMs: 300_000 };
|
|
@@ -186,7 +189,8 @@ function normalizeClassifier(input) {
|
|
|
186
189
|
thinkingKeywords: normalizeStringArray(normalized.thinkingKeywords, DEFAULT_CLASSIFIER.thinkingKeywords),
|
|
187
190
|
codingKeywords: normalizeStringArray(normalized.codingKeywords, DEFAULT_CLASSIFIER.codingKeywords),
|
|
188
191
|
backgroundKeywords: normalizeStringArray(normalized.backgroundKeywords, DEFAULT_CLASSIFIER.backgroundKeywords),
|
|
189
|
-
visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords)
|
|
192
|
+
visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords),
|
|
193
|
+
keywordInjections: normalizeKeywordInjectionMap(normalized.keywordInjections)
|
|
190
194
|
};
|
|
191
195
|
return result;
|
|
192
196
|
}
|
|
@@ -197,6 +201,26 @@ function normalizeStringArray(value, fallback) {
|
|
|
197
201
|
const normalized = value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
|
|
198
202
|
return normalized.length ? normalized : [...fallback];
|
|
199
203
|
}
|
|
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
|
+
}
|
|
200
224
|
function normalizeProvider(providerId, raw) {
|
|
201
225
|
const provider = asRecord(raw);
|
|
202
226
|
const providerType = detectProviderType(provider);
|
|
@@ -234,9 +258,9 @@ function normalizeResponsesConfig(provider) {
|
|
|
234
258
|
return undefined;
|
|
235
259
|
}
|
|
236
260
|
function resolveCompatibilityProfile(provider) {
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
return
|
|
261
|
+
const profile = readOptionalString(provider.compatibilityProfile);
|
|
262
|
+
if (profile) {
|
|
263
|
+
return profile;
|
|
240
264
|
}
|
|
241
265
|
return 'default';
|
|
242
266
|
}
|
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY } from './types.js';
|
|
2
|
-
|
|
2
|
+
import { DEFAULT_THINKING_KEYWORDS } from './default-thinking-keywords.js';
|
|
3
|
+
const DEFAULT_LONG_CONTEXT_THRESHOLD = 180000;
|
|
3
4
|
export class RoutingClassifier {
|
|
4
5
|
config;
|
|
5
6
|
constructor(config) {
|
|
7
|
+
const keywordConfig = normalizeKeywordConfig(config);
|
|
6
8
|
this.config = {
|
|
7
9
|
longContextThresholdTokens: config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD,
|
|
8
|
-
thinkingKeywords:
|
|
9
|
-
backgroundKeywords:
|
|
10
|
+
thinkingKeywords: keywordConfig.thinking,
|
|
11
|
+
backgroundKeywords: keywordConfig.background,
|
|
12
|
+
visionKeywords: keywordConfig.vision,
|
|
13
|
+
codingKeywords: keywordConfig.coding
|
|
10
14
|
};
|
|
11
15
|
}
|
|
12
16
|
classify(features) {
|
|
13
17
|
const evaluations = [
|
|
18
|
+
{
|
|
19
|
+
route: 'longcontext',
|
|
20
|
+
triggered: features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD),
|
|
21
|
+
reason: 'longcontext:token-threshold'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
route: 'thinking',
|
|
25
|
+
triggered: features.hasThinkingKeyword ||
|
|
26
|
+
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []) ||
|
|
27
|
+
features.previousToolCategory === 'context_read' ||
|
|
28
|
+
features.previousToolCategory === 'plan',
|
|
29
|
+
reason: 'thinking:keywords'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
route: 'background',
|
|
33
|
+
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
34
|
+
reason: 'background:keywords'
|
|
35
|
+
},
|
|
14
36
|
{
|
|
15
37
|
route: 'vision',
|
|
16
|
-
triggered: features.hasVisionTool && features.hasImageAttachment
|
|
38
|
+
triggered: (features.hasVisionTool && features.hasImageAttachment) ||
|
|
39
|
+
features.previousToolCategory === 'vision',
|
|
17
40
|
reason: 'vision:requires-tool+image'
|
|
18
41
|
},
|
|
19
42
|
{
|
|
@@ -23,29 +46,15 @@ export class RoutingClassifier {
|
|
|
23
46
|
},
|
|
24
47
|
{
|
|
25
48
|
route: 'coding',
|
|
26
|
-
triggered: features.hasCodingTool
|
|
49
|
+
triggered: features.hasCodingTool ||
|
|
50
|
+
containsKeywords(features.userTextSample, this.config.codingKeywords ?? []) ||
|
|
51
|
+
features.previousToolCategory === 'coding',
|
|
27
52
|
reason: 'coding:coding-tools-detected'
|
|
28
53
|
},
|
|
29
54
|
{
|
|
30
55
|
route: 'tools',
|
|
31
56
|
triggered: features.hasTools || features.hasToolCallResponses,
|
|
32
57
|
reason: 'tools:tool-request-detected'
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
route: 'longcontext',
|
|
36
|
-
triggered: features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD),
|
|
37
|
-
reason: 'longcontext:token-threshold'
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
route: 'thinking',
|
|
41
|
-
triggered: features.hasThinkingKeyword ||
|
|
42
|
-
containsKeywords(features.userTextSample, this.config.thinkingKeywords ?? []),
|
|
43
|
-
reason: 'thinking:keywords'
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
route: 'background',
|
|
47
|
-
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
48
|
-
reason: 'background:keywords'
|
|
49
58
|
}
|
|
50
59
|
];
|
|
51
60
|
const triggeredEvaluations = evaluations.filter((evaluation) => evaluation.triggered);
|
|
@@ -84,12 +93,6 @@ export class RoutingClassifier {
|
|
|
84
93
|
return index >= 0 ? index : ROUTE_PRIORITY.length;
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
|
-
function normalizeList(source, fallback) {
|
|
88
|
-
if (!source || source.length === 0) {
|
|
89
|
-
return fallback;
|
|
90
|
-
}
|
|
91
|
-
return source.map((item) => item.toLowerCase());
|
|
92
|
-
}
|
|
93
96
|
function containsKeywords(text, keywords) {
|
|
94
97
|
if (!text || !keywords.length) {
|
|
95
98
|
return false;
|
|
@@ -97,3 +100,29 @@ function containsKeywords(text, keywords) {
|
|
|
97
100
|
const normalized = text.toLowerCase();
|
|
98
101
|
return keywords.some((keyword) => normalized.includes(keyword));
|
|
99
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_THINKING_KEYWORDS: string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countRequestTokens } from './token-counter.js';
|
|
1
2
|
const THINKING_KEYWORDS = ['let me think', 'chain of thought', 'cot', 'reason step', 'deliberate'];
|
|
2
3
|
const WEB_TOOL_KEYWORDS = ['websearch', 'web_search', 'web-search', 'webfetch', 'web_fetch', 'web_request', 'search_web', 'internet_search'];
|
|
3
4
|
export function buildRoutingFeatures(request, metadata) {
|
|
@@ -7,10 +8,11 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
7
8
|
const normalizedUserText = latestUserText.toLowerCase();
|
|
8
9
|
const hasTools = Array.isArray(request.tools) && request.tools.length > 0;
|
|
9
10
|
const hasToolCallResponses = assistantMessages.some((msg) => Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0);
|
|
10
|
-
const estimatedTokens =
|
|
11
|
+
const estimatedTokens = countRequestTokens(request);
|
|
11
12
|
const hasThinking = detectKeyword(normalizedUserText, THINKING_KEYWORDS);
|
|
12
|
-
const
|
|
13
|
-
const
|
|
13
|
+
const previousToolCategory = detectPreviousToolCategory(request.messages);
|
|
14
|
+
const hasVisionTool = detectVisionTool(request) || previousToolCategory === 'vision';
|
|
15
|
+
const hasImageAttachment = previousToolCategory === 'vision' || (hasVisionTool && detectImageAttachment(latestUserMessage));
|
|
14
16
|
const hasCodingTool = detectCodingTool(request);
|
|
15
17
|
const hasWebTool = detectWebTool(request);
|
|
16
18
|
const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
|
|
@@ -28,6 +30,7 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
28
30
|
hasCodingTool,
|
|
29
31
|
hasThinkingKeyword,
|
|
30
32
|
estimatedTokens,
|
|
33
|
+
previousToolCategory,
|
|
31
34
|
metadata: {
|
|
32
35
|
...metadata
|
|
33
36
|
}
|
|
@@ -125,12 +128,111 @@ function detectExtendedThinkingKeyword(text) {
|
|
|
125
128
|
const keywords = ['仔细分析', '思考', '超级思考', '深度思考', 'careful analysis', 'deep thinking', 'deliberate'];
|
|
126
129
|
return keywords.some((keyword) => text.includes(keyword));
|
|
127
130
|
}
|
|
128
|
-
function
|
|
129
|
-
if (!
|
|
130
|
-
return
|
|
131
|
+
function detectPreviousToolCategory(messages) {
|
|
132
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
let lastUserIndex = messages.length;
|
|
136
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
137
|
+
if (messages[idx]?.role === 'user') {
|
|
138
|
+
lastUserIndex = idx;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (let idx = lastUserIndex - 1; idx >= 0; idx -= 1) {
|
|
143
|
+
const candidate = messages[idx];
|
|
144
|
+
if (!candidate || candidate.role !== 'assistant') {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const messageRecord = candidate;
|
|
148
|
+
const rawCalls = Array.isArray(messageRecord.tool_calls)
|
|
149
|
+
? messageRecord.tool_calls
|
|
150
|
+
: [];
|
|
151
|
+
if (!rawCalls.length) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const call of rawCalls) {
|
|
155
|
+
const categorized = categorizeToolCall(call);
|
|
156
|
+
if (categorized) {
|
|
157
|
+
return categorized;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function categorizeToolCall(call) {
|
|
164
|
+
if (!call) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const fn = (call.function ?? call);
|
|
168
|
+
const name = typeof (fn?.name ?? call.name) === 'string' ? String(fn?.name ?? call.name) : '';
|
|
169
|
+
if (!name) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const args = parseFunctionArguments(typeof fn?.arguments === 'string' ? fn.arguments : undefined);
|
|
173
|
+
if (name === 'update_plan') {
|
|
174
|
+
return 'plan';
|
|
175
|
+
}
|
|
176
|
+
if (name === 'view_image') {
|
|
177
|
+
return hasImageLink(args) ? 'vision' : null;
|
|
178
|
+
}
|
|
179
|
+
if (name === 'apply_patch') {
|
|
180
|
+
return 'coding';
|
|
131
181
|
}
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
if (name === 'shell_command') {
|
|
183
|
+
const command = typeof args?.command === 'string' ? args.command : '';
|
|
184
|
+
return classifyShellCommand(command);
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function parseFunctionArguments(raw) {
|
|
189
|
+
if (!raw) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(raw);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const CONTEXT_READ_PATTERNS = [
|
|
200
|
+
/\brg\b/i,
|
|
201
|
+
/\bsed\b/i,
|
|
202
|
+
/\bcat\b/i,
|
|
203
|
+
/\btail\b/i,
|
|
204
|
+
/\bhead\b/i,
|
|
205
|
+
/\bls\b/i,
|
|
206
|
+
/\bwc\b/i,
|
|
207
|
+
/\bgrep\b/i,
|
|
208
|
+
/node\s+-\s*<<['"]/i,
|
|
209
|
+
/python\s+-\s*<<['"]/i
|
|
210
|
+
];
|
|
211
|
+
function classifyShellCommand(command) {
|
|
212
|
+
if (!command) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const normalized = command.trim();
|
|
216
|
+
if (!normalized) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (CONTEXT_READ_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
220
|
+
return 'context_read';
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const IMAGE_LINK_KEYS = ['imagePath', 'path', 'filepath', 'file', 'url', 'image', 'src'];
|
|
225
|
+
function hasImageLink(args) {
|
|
226
|
+
if (!args || typeof args !== 'object') {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
for (const key of IMAGE_LINK_KEYS) {
|
|
230
|
+
const value = args[key];
|
|
231
|
+
if (typeof value === 'string' && value.trim()) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
134
236
|
}
|
|
135
237
|
function extractToolName(tool) {
|
|
136
238
|
if (!tool || typeof tool !== 'object') {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { encoding_for_model, get_encoding } from 'tiktoken';
|
|
2
|
+
const DEFAULT_ENCODING = 'cl100k_base';
|
|
3
|
+
const encoderCache = new Map();
|
|
4
|
+
let defaultEncoder = null;
|
|
5
|
+
function getEncoder(model) {
|
|
6
|
+
if (model) {
|
|
7
|
+
const normalized = model.trim();
|
|
8
|
+
if (encoderCache.has(normalized)) {
|
|
9
|
+
return encoderCache.get(normalized);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const encoder = encoding_for_model(normalized);
|
|
13
|
+
encoderCache.set(normalized, encoder);
|
|
14
|
+
return encoder;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// fall back to default encoder
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!defaultEncoder) {
|
|
21
|
+
defaultEncoder = get_encoding(DEFAULT_ENCODING);
|
|
22
|
+
}
|
|
23
|
+
return defaultEncoder;
|
|
24
|
+
}
|
|
25
|
+
export function countRequestTokens(request) {
|
|
26
|
+
const encoder = getEncoder(request.model);
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const message of request.messages || []) {
|
|
29
|
+
total += countMessageTokens(message, encoder);
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(request.tools)) {
|
|
32
|
+
for (const tool of request.tools) {
|
|
33
|
+
total += encodeText(JSON.stringify(tool ?? {}), encoder);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (request.parameters) {
|
|
37
|
+
total += encodeText(JSON.stringify(request.parameters), encoder);
|
|
38
|
+
}
|
|
39
|
+
if (request.metadata) {
|
|
40
|
+
total += encodeText(JSON.stringify(request.metadata), encoder);
|
|
41
|
+
}
|
|
42
|
+
return total;
|
|
43
|
+
}
|
|
44
|
+
function countMessageTokens(message, encoder) {
|
|
45
|
+
let total = 0;
|
|
46
|
+
total += encodeText(message.role, encoder);
|
|
47
|
+
total += encodeContent(message.content, encoder);
|
|
48
|
+
if (Array.isArray(message.tool_calls)) {
|
|
49
|
+
for (const call of message.tool_calls) {
|
|
50
|
+
total += encodeText(JSON.stringify(call ?? {}), encoder);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(message.metadata?.toolRuns)) {
|
|
54
|
+
total += encodeText(JSON.stringify(message.metadata?.toolRuns), encoder);
|
|
55
|
+
}
|
|
56
|
+
if (message.name) {
|
|
57
|
+
total += encodeText(message.name, encoder);
|
|
58
|
+
}
|
|
59
|
+
if (message.metadata) {
|
|
60
|
+
total += encodeText(JSON.stringify(message.metadata), encoder);
|
|
61
|
+
}
|
|
62
|
+
if (message.tool_call_id) {
|
|
63
|
+
total += encodeText(message.tool_call_id, encoder);
|
|
64
|
+
}
|
|
65
|
+
return total;
|
|
66
|
+
}
|
|
67
|
+
function encodeContent(content, encoder) {
|
|
68
|
+
if (content === null || content === undefined) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (typeof content === 'string') {
|
|
72
|
+
return encodeText(content, encoder);
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(content)) {
|
|
75
|
+
let total = 0;
|
|
76
|
+
for (const part of content) {
|
|
77
|
+
if (typeof part === 'string') {
|
|
78
|
+
total += encodeText(part, encoder);
|
|
79
|
+
}
|
|
80
|
+
else if (part && typeof part === 'object') {
|
|
81
|
+
if (typeof part.text === 'string') {
|
|
82
|
+
total += encodeText(part.text, encoder);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
total += encodeText(JSON.stringify(part), encoder);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
if (typeof content === 'object') {
|
|
92
|
+
return encodeText(JSON.stringify(content), encoder);
|
|
93
|
+
}
|
|
94
|
+
return encodeText(String(content), encoder);
|
|
95
|
+
}
|
|
96
|
+
function encodeText(value, encoder) {
|
|
97
|
+
if (value === null || value === undefined) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const text = typeof value === 'string' ? value : String(value);
|
|
101
|
+
if (!text.trim()) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
return encoder.encode(text).length;
|
|
105
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { StandardizedRequest } from '../../conversion/hub/types/standardize
|
|
|
5
5
|
export declare const DEFAULT_ROUTE = "default";
|
|
6
6
|
export declare const ROUTE_PRIORITY: string[];
|
|
7
7
|
export type RoutingPools = Record<string, string[]>;
|
|
8
|
+
export type PreviousToolCategory = 'context_read' | 'plan' | 'vision' | 'coding';
|
|
8
9
|
export interface ProviderAuthConfig {
|
|
9
10
|
type: 'apiKey' | 'oauth';
|
|
10
11
|
secretRef?: string;
|
|
@@ -47,12 +48,14 @@ export interface ProviderRuntimeProfile {
|
|
|
47
48
|
processMode?: 'chat' | 'passthrough';
|
|
48
49
|
responsesConfig?: ResponsesProviderConfig;
|
|
49
50
|
}
|
|
51
|
+
export type RouteKeywordCategory = 'thinking' | 'background' | 'vision' | 'coding';
|
|
50
52
|
export interface VirtualRouterClassifierConfig {
|
|
51
53
|
longContextThresholdTokens?: number;
|
|
52
54
|
thinkingKeywords?: string[];
|
|
53
55
|
codingKeywords?: string[];
|
|
54
56
|
backgroundKeywords?: string[];
|
|
55
57
|
visionKeywords?: string[];
|
|
58
|
+
keywordInjections?: Partial<Record<RouteKeywordCategory, string[]>>;
|
|
56
59
|
}
|
|
57
60
|
export interface LoadBalancingPolicy {
|
|
58
61
|
strategy: 'round-robin' | 'weighted' | 'sticky';
|
|
@@ -111,6 +114,7 @@ export interface RoutingFeatures {
|
|
|
111
114
|
hasCodingTool: boolean;
|
|
112
115
|
hasThinkingKeyword: boolean;
|
|
113
116
|
estimatedTokens: number;
|
|
117
|
+
previousToolCategory?: PreviousToolCategory | null;
|
|
114
118
|
metadata: RouterMetadataInput;
|
|
115
119
|
}
|
|
116
120
|
export interface ClassificationResult {
|