@jsonstudio/llms 0.6.104 → 0.6.141
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/shared/responses-conversation-store.js +26 -3
- package/dist/router/virtual-router/bootstrap.js +40 -1
- package/dist/router/virtual-router/classifier.js +52 -12
- package/dist/router/virtual-router/engine.d.ts +26 -0
- package/dist/router/virtual-router/engine.js +371 -17
- package/dist/router/virtual-router/features.js +183 -21
- package/dist/router/virtual-router/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -205,16 +205,29 @@ class ResponsesConversationStore {
|
|
|
205
205
|
}
|
|
206
206
|
resumeConversation(responseId, submitPayload, options) {
|
|
207
207
|
if (typeof responseId !== 'string' || !responseId.trim()) {
|
|
208
|
-
|
|
208
|
+
raiseResumeError('Responses conversation requires valid response_id', {
|
|
209
|
+
code: 'RESPONSES_RESUME_MISSING_ID',
|
|
210
|
+
status: 422,
|
|
211
|
+
origin: 'client'
|
|
212
|
+
});
|
|
209
213
|
}
|
|
210
214
|
this.prune();
|
|
211
215
|
const entry = this.responseIndex.get(responseId);
|
|
212
216
|
if (!entry) {
|
|
213
|
-
|
|
217
|
+
raiseResumeError('Responses conversation expired or not found', {
|
|
218
|
+
code: 'RESPONSES_RESUME_NOT_FOUND',
|
|
219
|
+
status: 500,
|
|
220
|
+
origin: 'server',
|
|
221
|
+
details: { responseId }
|
|
222
|
+
});
|
|
214
223
|
}
|
|
215
224
|
const toolOutputs = Array.isArray(submitPayload.tool_outputs) ? submitPayload.tool_outputs : [];
|
|
216
225
|
if (!toolOutputs.length) {
|
|
217
|
-
|
|
226
|
+
raiseResumeError('tool_outputs array is required when submitting Responses tool results', {
|
|
227
|
+
code: 'RESPONSES_RESUME_MISSING_OUTPUTS',
|
|
228
|
+
status: 422,
|
|
229
|
+
origin: 'client'
|
|
230
|
+
});
|
|
218
231
|
}
|
|
219
232
|
const mergedInput = coerceInputArray(entry.input);
|
|
220
233
|
const normalizedOutputs = normalizeSubmittedToolOutputs(toolOutputs);
|
|
@@ -281,6 +294,16 @@ class ResponsesConversationStore {
|
|
|
281
294
|
}
|
|
282
295
|
const store = new ResponsesConversationStore();
|
|
283
296
|
const RESPONSES_DEBUG = (process.env.ROUTECODEX_RESPONSES_DEBUG || '').trim() === '1';
|
|
297
|
+
function raiseResumeError(message, options) {
|
|
298
|
+
const err = new Error(message);
|
|
299
|
+
err.code = options?.code ?? 'RESPONSES_RESUME_ERROR';
|
|
300
|
+
err.status = options?.status;
|
|
301
|
+
err.origin = options?.origin;
|
|
302
|
+
if (options?.details) {
|
|
303
|
+
err.details = options.details;
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
284
307
|
export function captureResponsesRequestContext(args) {
|
|
285
308
|
try {
|
|
286
309
|
if (RESPONSES_DEBUG) {
|
|
@@ -227,9 +227,48 @@ function normalizeResponsesConfig(provider) {
|
|
|
227
227
|
if (!node) {
|
|
228
228
|
return undefined;
|
|
229
229
|
}
|
|
230
|
+
const config = {};
|
|
230
231
|
const rawStyle = typeof node.toolCallIdStyle === 'string' ? node.toolCallIdStyle.trim().toLowerCase() : undefined;
|
|
231
232
|
if (rawStyle === 'fc' || rawStyle === 'preserve') {
|
|
232
|
-
|
|
233
|
+
config.toolCallIdStyle = rawStyle;
|
|
234
|
+
}
|
|
235
|
+
const streaming = normalizeResponsesStreaming(node.streaming);
|
|
236
|
+
if (streaming) {
|
|
237
|
+
config.streaming = streaming;
|
|
238
|
+
}
|
|
239
|
+
const instructionsMode = normalizeResponsesInstructionsMode(node.instructionsMode);
|
|
240
|
+
if (instructionsMode) {
|
|
241
|
+
config.instructionsMode = instructionsMode;
|
|
242
|
+
}
|
|
243
|
+
return Object.keys(config).length ? config : undefined;
|
|
244
|
+
}
|
|
245
|
+
function normalizeResponsesStreaming(value) {
|
|
246
|
+
if (value === true) {
|
|
247
|
+
return 'always';
|
|
248
|
+
}
|
|
249
|
+
if (value === false) {
|
|
250
|
+
return 'never';
|
|
251
|
+
}
|
|
252
|
+
if (typeof value === 'string') {
|
|
253
|
+
const normalized = value.trim().toLowerCase();
|
|
254
|
+
if (normalized === 'always' || normalized === 'true' || normalized === '1' || normalized === 'yes') {
|
|
255
|
+
return 'always';
|
|
256
|
+
}
|
|
257
|
+
if (normalized === 'never' || normalized === 'false' || normalized === '0' || normalized === 'no') {
|
|
258
|
+
return 'never';
|
|
259
|
+
}
|
|
260
|
+
if (normalized === 'auto') {
|
|
261
|
+
return 'auto';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
function normalizeResponsesInstructionsMode(value) {
|
|
267
|
+
if (value === 'inline') {
|
|
268
|
+
return 'inline';
|
|
269
|
+
}
|
|
270
|
+
if (typeof value === 'string' && value.trim().toLowerCase() === 'inline') {
|
|
271
|
+
return 'inline';
|
|
233
272
|
}
|
|
234
273
|
return undefined;
|
|
235
274
|
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY } from './types.js';
|
|
2
2
|
const DEFAULT_LONG_CONTEXT_THRESHOLD = 180000;
|
|
3
|
+
const WEBSEARCH_HINT_KEYWORDS = [
|
|
4
|
+
'web search',
|
|
5
|
+
'search the web',
|
|
6
|
+
'search online',
|
|
7
|
+
'internet search',
|
|
8
|
+
'search internet',
|
|
9
|
+
'google it',
|
|
10
|
+
'bing it',
|
|
11
|
+
'网络搜索',
|
|
12
|
+
'上网搜索',
|
|
13
|
+
'查一下网络',
|
|
14
|
+
'搜一下网络'
|
|
15
|
+
];
|
|
3
16
|
export class RoutingClassifier {
|
|
4
17
|
config;
|
|
5
18
|
constructor(config) {
|
|
@@ -11,13 +24,34 @@ export class RoutingClassifier {
|
|
|
11
24
|
}
|
|
12
25
|
classify(features) {
|
|
13
26
|
const lastToolCategory = features.lastAssistantToolCategory;
|
|
27
|
+
const toolCategories = features.assistantToolCategories ?? [];
|
|
28
|
+
const hasSearchToolCall = toolCategories.includes('search');
|
|
29
|
+
const hasWriteToolCall = toolCategories.includes('write');
|
|
30
|
+
const hasReadToolCall = toolCategories.includes('read');
|
|
31
|
+
const hasOtherToolCall = toolCategories.includes('other');
|
|
32
|
+
const hasToolCall = toolCategories.length > 0;
|
|
14
33
|
const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
34
|
+
const routeHint = typeof features.metadata?.routeHint === 'string'
|
|
35
|
+
? features.metadata.routeHint.trim().toLowerCase()
|
|
36
|
+
: undefined;
|
|
37
|
+
const websearchKeywordHit = containsKeywords(features.userTextSample, WEBSEARCH_HINT_KEYWORDS);
|
|
38
|
+
const codingContinuation = hasWriteToolCall || lastToolCategory === 'write';
|
|
39
|
+
const thinkingContinuation = hasReadToolCall || lastToolCategory === 'read';
|
|
40
|
+
const userInputDetected = typeof features.userTextSample === 'string'
|
|
41
|
+
? features.userTextSample.trim().length > 0
|
|
42
|
+
: false;
|
|
43
|
+
const searchContinuation = features.assistantCalledWebSearchTool === true;
|
|
44
|
+
const toolsContinuation = hasOtherToolCall ||
|
|
45
|
+
searchContinuation ||
|
|
46
|
+
(hasToolCall && !hasSearchToolCall && !hasWriteToolCall && !hasReadToolCall);
|
|
47
|
+
const toolContinuationReason = hasOtherToolCall
|
|
48
|
+
? formatToolContinuationReason(features.lastAssistantToolName, features.lastAssistantToolDetail)
|
|
49
|
+
: 'tools:tool-call-detected';
|
|
50
|
+
const thinkingReason = thinkingContinuation
|
|
51
|
+
? 'thinking:last-tool-read'
|
|
52
|
+
: userInputDetected
|
|
53
|
+
? 'thinking:user-input'
|
|
54
|
+
: 'thinking';
|
|
21
55
|
const evaluationMap = {
|
|
22
56
|
vision: {
|
|
23
57
|
triggered: features.hasVisionTool && features.hasImageAttachment,
|
|
@@ -28,20 +62,20 @@ export class RoutingClassifier {
|
|
|
28
62
|
reason: 'longcontext:token-threshold'
|
|
29
63
|
},
|
|
30
64
|
websearch: {
|
|
31
|
-
triggered:
|
|
32
|
-
reason:
|
|
65
|
+
triggered: routeHint === 'websearch' || websearchKeywordHit,
|
|
66
|
+
reason: routeHint === 'websearch' ? 'websearch:route-hint' : 'websearch:keywords'
|
|
33
67
|
},
|
|
34
68
|
coding: {
|
|
35
69
|
triggered: codingContinuation,
|
|
36
70
|
reason: 'coding:last-tool-write'
|
|
37
71
|
},
|
|
38
72
|
thinking: {
|
|
39
|
-
triggered: thinkingContinuation ||
|
|
40
|
-
reason:
|
|
73
|
+
triggered: thinkingContinuation || userInputDetected,
|
|
74
|
+
reason: thinkingReason
|
|
41
75
|
},
|
|
42
76
|
tools: {
|
|
43
|
-
triggered: toolsContinuation
|
|
44
|
-
reason:
|
|
77
|
+
triggered: toolsContinuation,
|
|
78
|
+
reason: toolContinuationReason
|
|
45
79
|
},
|
|
46
80
|
background: {
|
|
47
81
|
triggered: containsKeywords(features.userTextSample, this.config.backgroundKeywords ?? []),
|
|
@@ -100,3 +134,9 @@ function containsKeywords(text, keywords) {
|
|
|
100
134
|
const normalized = text.toLowerCase();
|
|
101
135
|
return keywords.some((keyword) => normalized.includes(keyword));
|
|
102
136
|
}
|
|
137
|
+
function formatToolContinuationReason(toolName, toolDetail) {
|
|
138
|
+
const trimmedName = toolName?.trim() || 'tool';
|
|
139
|
+
const trimmedDetail = toolDetail?.trim();
|
|
140
|
+
const detailText = trimmedDetail ? `${trimmedName}: ${trimmedDetail}` : trimmedName;
|
|
141
|
+
return `tools:last-tool-other(${detailText})`;
|
|
142
|
+
}
|
|
@@ -9,6 +9,9 @@ export declare class VirtualRouterEngine {
|
|
|
9
9
|
private routeStats;
|
|
10
10
|
private readonly debug;
|
|
11
11
|
private healthConfig;
|
|
12
|
+
private stickyPlans;
|
|
13
|
+
private selectionHistory;
|
|
14
|
+
private providerErrorStreaks;
|
|
12
15
|
initialize(config: VirtualRouterConfig): void;
|
|
13
16
|
route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
|
|
14
17
|
target: TargetMetadata;
|
|
@@ -25,6 +28,20 @@ export declare class VirtualRouterEngine {
|
|
|
25
28
|
}>;
|
|
26
29
|
health: import("./types.js").ProviderHealthState[];
|
|
27
30
|
};
|
|
31
|
+
private consumeSticky;
|
|
32
|
+
private selectStickyTarget;
|
|
33
|
+
private buildStickyClassification;
|
|
34
|
+
private recordSelectionSnapshot;
|
|
35
|
+
private buildStickyPlan;
|
|
36
|
+
private storeStickyPlan;
|
|
37
|
+
private resolveStickyDescriptor;
|
|
38
|
+
private maybeForceStickyFromHistory;
|
|
39
|
+
private shouldForceApplyPatchSticky;
|
|
40
|
+
private extractPreviousRequestId;
|
|
41
|
+
private pruneStickyPlans;
|
|
42
|
+
private buildErrorSignature;
|
|
43
|
+
private bumpProviderErrorStreak;
|
|
44
|
+
private resetProviderErrorStreak;
|
|
28
45
|
private validateConfig;
|
|
29
46
|
private selectProvider;
|
|
30
47
|
private incrementRouteStat;
|
|
@@ -33,7 +50,16 @@ export declare class VirtualRouterEngine {
|
|
|
33
50
|
private mapProviderError;
|
|
34
51
|
private deriveReason;
|
|
35
52
|
private buildRouteCandidates;
|
|
53
|
+
private ensureConfiguredClassification;
|
|
54
|
+
private normalizeCandidateList;
|
|
55
|
+
private normalizeRouteName;
|
|
56
|
+
private isRouteConfigured;
|
|
36
57
|
private sortByPriority;
|
|
37
58
|
private routeWeight;
|
|
38
59
|
private buildHitReason;
|
|
60
|
+
private formatToolIdentifier;
|
|
61
|
+
private decorateReason;
|
|
62
|
+
private buildVirtualRouterHitLog;
|
|
63
|
+
private colorizeVirtualRouterLog;
|
|
64
|
+
private shouldColorVirtualRouterLogs;
|
|
39
65
|
}
|
|
@@ -4,6 +4,11 @@ import { RouteLoadBalancer } from './load-balancer.js';
|
|
|
4
4
|
import { RoutingClassifier } from './classifier.js';
|
|
5
5
|
import { buildRoutingFeatures } from './features.js';
|
|
6
6
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
7
|
+
const VIRTUAL_ROUTER_HIT_COLOR = '\x1b[38;5;208m';
|
|
8
|
+
const ANSI_RESET = '\x1b[0m';
|
|
9
|
+
const STICKY_PLAN_TTL_MS = 30 * 60 * 1000;
|
|
10
|
+
const ERROR_STREAK_TTL_MS = 10 * 60 * 1000;
|
|
11
|
+
const ERROR_STREAK_THRESHOLD = 4;
|
|
7
12
|
export class VirtualRouterEngine {
|
|
8
13
|
routing = {};
|
|
9
14
|
providerRegistry = new ProviderRegistry();
|
|
@@ -13,6 +18,9 @@ export class VirtualRouterEngine {
|
|
|
13
18
|
routeStats = new Map();
|
|
14
19
|
debug = console; // thin hook; host may monkey-patch for colored logging
|
|
15
20
|
healthConfig = null;
|
|
21
|
+
stickyPlans = new Map();
|
|
22
|
+
selectionHistory = new Map();
|
|
23
|
+
providerErrorStreaks = new Map();
|
|
16
24
|
initialize(config) {
|
|
17
25
|
this.validateConfig(config);
|
|
18
26
|
this.routing = config.routing;
|
|
@@ -29,15 +37,42 @@ export class VirtualRouterEngine {
|
|
|
29
37
|
}
|
|
30
38
|
route(request, metadata) {
|
|
31
39
|
const features = buildRoutingFeatures(request, metadata);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
let stickyActivation = this.consumeSticky(metadata, features);
|
|
41
|
+
let classification = null;
|
|
42
|
+
let selection = null;
|
|
43
|
+
if (stickyActivation) {
|
|
44
|
+
selection = this.selectStickyTarget(stickyActivation, metadata);
|
|
45
|
+
if (selection) {
|
|
46
|
+
classification = this.buildStickyClassification(stickyActivation);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
stickyActivation = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!selection || !classification) {
|
|
53
|
+
classification = this.classifier.classify(features);
|
|
54
|
+
classification = this.ensureConfiguredClassification(classification);
|
|
55
|
+
const routeName = classification.routeName || DEFAULT_ROUTE;
|
|
56
|
+
selection = this.selectProvider(routeName, metadata, classification);
|
|
57
|
+
}
|
|
58
|
+
if (!selection || !classification) {
|
|
59
|
+
throw new VirtualRouterError('Virtual router failed to select provider', VirtualRouterErrorCode.ROUTE_NOT_FOUND);
|
|
60
|
+
}
|
|
35
61
|
const target = this.providerRegistry.buildTarget(selection.providerKey);
|
|
36
62
|
this.healthManager.recordSuccess(selection.providerKey);
|
|
63
|
+
this.resetProviderErrorStreak(selection.providerKey);
|
|
37
64
|
this.incrementRouteStat(selection.routeUsed, selection.providerKey);
|
|
38
|
-
const
|
|
39
|
-
this.
|
|
40
|
-
|
|
65
|
+
const targetModel = typeof target.modelId === 'string' ? target.modelId : '';
|
|
66
|
+
this.recordSelectionSnapshot(metadata.requestId, selection.routeUsed, selection.providerKey, targetModel);
|
|
67
|
+
if (!stickyActivation || stickyActivation.mode === 'forced') {
|
|
68
|
+
const nextPlan = this.buildStickyPlan(features, selection, target);
|
|
69
|
+
this.storeStickyPlan(metadata.requestId, nextPlan);
|
|
70
|
+
}
|
|
71
|
+
const phase = stickyActivation ? 'execution' : 'hit';
|
|
72
|
+
const hitReason = this.buildHitReason(selection.routeUsed, classification, features, stickyActivation || undefined, phase);
|
|
73
|
+
const hitLog = this.buildVirtualRouterHitLog(selection.routeUsed, selection.providerKey, targetModel, hitReason, stickyActivation || undefined, phase);
|
|
74
|
+
this.debug?.log?.(hitLog);
|
|
75
|
+
const didFallback = selection.routeUsed !== (classification.routeName || DEFAULT_ROUTE) || classification.fallback;
|
|
41
76
|
return {
|
|
42
77
|
target,
|
|
43
78
|
decision: {
|
|
@@ -94,6 +129,199 @@ export class VirtualRouterEngine {
|
|
|
94
129
|
health: this.healthManager.getSnapshot()
|
|
95
130
|
};
|
|
96
131
|
}
|
|
132
|
+
consumeSticky(metadata, features) {
|
|
133
|
+
const prevId = this.extractPreviousRequestId(metadata);
|
|
134
|
+
if (!prevId) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
this.pruneStickyPlans();
|
|
138
|
+
const planned = this.stickyPlans.get(prevId);
|
|
139
|
+
if (planned) {
|
|
140
|
+
this.stickyPlans.delete(prevId);
|
|
141
|
+
const activation = {
|
|
142
|
+
...planned,
|
|
143
|
+
sourceRequestId: prevId,
|
|
144
|
+
mode: 'planned'
|
|
145
|
+
};
|
|
146
|
+
if (planned.remainingRounds > 1 && metadata.requestId) {
|
|
147
|
+
this.stickyPlans.set(metadata.requestId, {
|
|
148
|
+
...planned,
|
|
149
|
+
remainingRounds: planned.remainingRounds - 1,
|
|
150
|
+
createdAt: Date.now()
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return activation;
|
|
154
|
+
}
|
|
155
|
+
return this.maybeForceStickyFromHistory(prevId, features);
|
|
156
|
+
}
|
|
157
|
+
selectStickyTarget(sticky, metadata) {
|
|
158
|
+
if (sticky.strategy === 'target' && sticky.providerKey) {
|
|
159
|
+
if (!this.healthManager.isAvailable(sticky.providerKey)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const pool = this.routing[sticky.routeName] ?? [];
|
|
163
|
+
return { providerKey: sticky.providerKey, routeUsed: sticky.routeName, pool };
|
|
164
|
+
}
|
|
165
|
+
const pool = this.routing[sticky.routeName];
|
|
166
|
+
if (!Array.isArray(pool) || pool.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const stub = {
|
|
170
|
+
routeName: sticky.routeName,
|
|
171
|
+
confidence: 1,
|
|
172
|
+
reasoning: `sticky:${sticky.reason}`,
|
|
173
|
+
fallback: false,
|
|
174
|
+
candidates: [sticky.routeName]
|
|
175
|
+
};
|
|
176
|
+
return this.selectProvider(sticky.routeName, metadata, stub);
|
|
177
|
+
}
|
|
178
|
+
buildStickyClassification(sticky) {
|
|
179
|
+
return {
|
|
180
|
+
routeName: sticky.routeName,
|
|
181
|
+
confidence: 1,
|
|
182
|
+
reasoning: `sticky:${sticky.reason}`,
|
|
183
|
+
fallback: false,
|
|
184
|
+
candidates: [sticky.routeName]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
recordSelectionSnapshot(requestId, routeName, providerKey, modelId) {
|
|
188
|
+
if (!requestId || !providerKey) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.selectionHistory.set(requestId, {
|
|
192
|
+
routeName,
|
|
193
|
+
providerKey,
|
|
194
|
+
modelId,
|
|
195
|
+
createdAt: Date.now()
|
|
196
|
+
});
|
|
197
|
+
this.pruneStickyPlans();
|
|
198
|
+
}
|
|
199
|
+
buildStickyPlan(features, selection, target) {
|
|
200
|
+
const descriptor = this.resolveStickyDescriptor(selection.routeUsed, features);
|
|
201
|
+
if (!descriptor || descriptor.rounds <= 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const plan = {
|
|
205
|
+
routeName: selection.routeUsed,
|
|
206
|
+
strategy: descriptor.strategy,
|
|
207
|
+
providerKey: descriptor.strategy === 'target' ? selection.providerKey : undefined,
|
|
208
|
+
modelId: descriptor.strategy === 'target' ? target.modelId : undefined,
|
|
209
|
+
remainingRounds: descriptor.rounds,
|
|
210
|
+
totalRounds: descriptor.rounds,
|
|
211
|
+
reason: descriptor.reason,
|
|
212
|
+
createdAt: Date.now()
|
|
213
|
+
};
|
|
214
|
+
return plan;
|
|
215
|
+
}
|
|
216
|
+
storeStickyPlan(requestId, plan) {
|
|
217
|
+
if (!requestId) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.pruneStickyPlans();
|
|
221
|
+
if (plan && plan.remainingRounds > 0) {
|
|
222
|
+
this.stickyPlans.set(requestId, plan);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
this.stickyPlans.delete(requestId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
resolveStickyDescriptor(routeName, features) {
|
|
229
|
+
if (this.shouldForceApplyPatchSticky(features)) {
|
|
230
|
+
return { strategy: 'target', rounds: 1, reason: 'apply_patch' };
|
|
231
|
+
}
|
|
232
|
+
if (routeName === 'coding' || routeName === 'thinking') {
|
|
233
|
+
return { strategy: 'pool', rounds: 3, reason: routeName };
|
|
234
|
+
}
|
|
235
|
+
if (routeName === 'tools') {
|
|
236
|
+
return { strategy: 'pool', rounds: 0, reason: routeName };
|
|
237
|
+
}
|
|
238
|
+
if (routeName === DEFAULT_ROUTE || !routeName) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return { strategy: 'pool', rounds: 1, reason: routeName };
|
|
242
|
+
}
|
|
243
|
+
maybeForceStickyFromHistory(prevId, features) {
|
|
244
|
+
if (!this.shouldForceApplyPatchSticky(features)) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const snapshot = this.selectionHistory.get(prevId);
|
|
248
|
+
if (!snapshot) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
if (!this.healthManager.isAvailable(snapshot.providerKey)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
routeName: snapshot.routeName,
|
|
256
|
+
providerKey: snapshot.providerKey,
|
|
257
|
+
modelId: snapshot.modelId,
|
|
258
|
+
strategy: 'target',
|
|
259
|
+
remainingRounds: 0,
|
|
260
|
+
totalRounds: 1,
|
|
261
|
+
reason: 'apply_patch',
|
|
262
|
+
createdAt: Date.now(),
|
|
263
|
+
sourceRequestId: prevId,
|
|
264
|
+
mode: 'forced'
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
shouldForceApplyPatchSticky(features) {
|
|
268
|
+
const name = (features.lastAssistantToolName || '').toLowerCase();
|
|
269
|
+
if (name === 'apply_patch') {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
const detail = (features.lastAssistantToolDetail || '').toLowerCase();
|
|
273
|
+
if (detail.includes('apply_patch')) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
extractPreviousRequestId(metadata) {
|
|
279
|
+
const resume = metadata.responsesResume;
|
|
280
|
+
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
281
|
+
return resume.previousRequestId.trim();
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
pruneStickyPlans() {
|
|
286
|
+
const cutoff = Date.now() - STICKY_PLAN_TTL_MS;
|
|
287
|
+
for (const [key, plan] of this.stickyPlans.entries()) {
|
|
288
|
+
if (!plan || plan.createdAt < cutoff) {
|
|
289
|
+
this.stickyPlans.delete(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const [key, snapshot] of this.selectionHistory.entries()) {
|
|
293
|
+
if (!snapshot || snapshot.createdAt < cutoff) {
|
|
294
|
+
this.selectionHistory.delete(key);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
buildErrorSignature(code, statusCode, message) {
|
|
299
|
+
const normalizedMessage = typeof message === 'string'
|
|
300
|
+
? message.trim().toLowerCase().replace(/\s+/g, ' ').slice(0, 120)
|
|
301
|
+
: '';
|
|
302
|
+
const codeToken = code?.toUpperCase() || 'ERR_UNKNOWN';
|
|
303
|
+
const statusToken = typeof statusCode === 'number' ? String(statusCode) : 'NA';
|
|
304
|
+
return `${statusToken}|${codeToken}|${normalizedMessage}`;
|
|
305
|
+
}
|
|
306
|
+
bumpProviderErrorStreak(providerKey, signature) {
|
|
307
|
+
if (!providerKey || !signature) {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
const entry = this.providerErrorStreaks.get(providerKey);
|
|
312
|
+
if (!entry || entry.signature !== signature || now - entry.lastAt > ERROR_STREAK_TTL_MS) {
|
|
313
|
+
this.providerErrorStreaks.set(providerKey, { signature, count: 1, lastAt: now });
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
const next = { signature, count: entry.count + 1, lastAt: now };
|
|
317
|
+
this.providerErrorStreaks.set(providerKey, next);
|
|
318
|
+
return next.count;
|
|
319
|
+
}
|
|
320
|
+
resetProviderErrorStreak(providerKey) {
|
|
321
|
+
if (providerKey) {
|
|
322
|
+
this.providerErrorStreaks.delete(providerKey);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
97
325
|
validateConfig(config) {
|
|
98
326
|
if (!config.routing || typeof config.routing !== 'object') {
|
|
99
327
|
throw new VirtualRouterError('routing configuration is required', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
@@ -121,7 +349,8 @@ export class VirtualRouterEngine {
|
|
|
121
349
|
}
|
|
122
350
|
}
|
|
123
351
|
selectProvider(requestedRoute, metadata, classification) {
|
|
124
|
-
const
|
|
352
|
+
const normalizedRoute = this.normalizeRouteName(requestedRoute);
|
|
353
|
+
const candidates = this.buildRouteCandidates(normalizedRoute, classification.candidates);
|
|
125
354
|
const stickyKey = this.resolveStickyKey(metadata);
|
|
126
355
|
const attempted = [];
|
|
127
356
|
for (const routeName of candidates) {
|
|
@@ -141,7 +370,8 @@ export class VirtualRouterEngine {
|
|
|
141
370
|
}
|
|
142
371
|
attempted.push(routeName);
|
|
143
372
|
}
|
|
144
|
-
|
|
373
|
+
const failureRoute = attempted.length ? attempted[attempted.length - 1] : requestedRoute;
|
|
374
|
+
throw new VirtualRouterError(`All providers unavailable for route ${failureRoute}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { routeName: failureRoute, attempted });
|
|
145
375
|
}
|
|
146
376
|
incrementRouteStat(routeName, providerKey) {
|
|
147
377
|
if (!this.routeStats.has(routeName)) {
|
|
@@ -207,6 +437,13 @@ export class VirtualRouterEngine {
|
|
|
207
437
|
cooldownOverrideMs = Math.max(10 * 60_000, this.providerHealthConfig().fatalCooldownMs ?? 10 * 60_000);
|
|
208
438
|
reason = 'compatibility';
|
|
209
439
|
}
|
|
440
|
+
const signature = this.buildErrorSignature(code, statusCode, event.message);
|
|
441
|
+
const streak = this.bumpProviderErrorStreak(providerKey, signature);
|
|
442
|
+
if (streak >= ERROR_STREAK_THRESHOLD) {
|
|
443
|
+
fatal = true;
|
|
444
|
+
reason = reason === 'unknown' ? 'repeated_error' : `${reason}|repeated`;
|
|
445
|
+
cooldownOverrideMs = Math.max(this.providerHealthConfig().fatalCooldownMs ?? 5 * 60_000, 5 * 60_000);
|
|
446
|
+
}
|
|
210
447
|
return {
|
|
211
448
|
providerKey,
|
|
212
449
|
routeName,
|
|
@@ -270,6 +507,59 @@ export class VirtualRouterEngine {
|
|
|
270
507
|
}
|
|
271
508
|
return filtered.length ? filtered : [DEFAULT_ROUTE];
|
|
272
509
|
}
|
|
510
|
+
ensureConfiguredClassification(classification) {
|
|
511
|
+
const normalizedRoute = this.normalizeRouteName(classification.routeName);
|
|
512
|
+
const normalizedCandidates = this.normalizeCandidateList(normalizedRoute, classification.candidates);
|
|
513
|
+
const fallback = normalizedRoute === DEFAULT_ROUTE ? true : classification.fallback;
|
|
514
|
+
return {
|
|
515
|
+
...classification,
|
|
516
|
+
routeName: normalizedRoute,
|
|
517
|
+
fallback,
|
|
518
|
+
candidates: normalizedCandidates
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
normalizeCandidateList(primaryRoute, rawCandidates) {
|
|
522
|
+
const base = rawCandidates && rawCandidates.length ? rawCandidates : [primaryRoute];
|
|
523
|
+
const deduped = [];
|
|
524
|
+
for (const routeName of base) {
|
|
525
|
+
if (!routeName) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (!this.isRouteConfigured(routeName)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (!deduped.includes(routeName)) {
|
|
532
|
+
deduped.push(routeName);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!deduped.includes(primaryRoute) && this.isRouteConfigured(primaryRoute)) {
|
|
536
|
+
deduped.push(primaryRoute);
|
|
537
|
+
}
|
|
538
|
+
if (!deduped.includes(DEFAULT_ROUTE) && this.isRouteConfigured(DEFAULT_ROUTE)) {
|
|
539
|
+
deduped.push(DEFAULT_ROUTE);
|
|
540
|
+
}
|
|
541
|
+
if (!deduped.length && this.isRouteConfigured(DEFAULT_ROUTE)) {
|
|
542
|
+
deduped.push(DEFAULT_ROUTE);
|
|
543
|
+
}
|
|
544
|
+
return this.sortByPriority(deduped);
|
|
545
|
+
}
|
|
546
|
+
normalizeRouteName(routeName) {
|
|
547
|
+
if (routeName && this.isRouteConfigured(routeName)) {
|
|
548
|
+
return routeName;
|
|
549
|
+
}
|
|
550
|
+
if (this.isRouteConfigured(DEFAULT_ROUTE)) {
|
|
551
|
+
return DEFAULT_ROUTE;
|
|
552
|
+
}
|
|
553
|
+
const firstConfigured = Object.keys(this.routing).find((key) => this.isRouteConfigured(key));
|
|
554
|
+
return firstConfigured || DEFAULT_ROUTE;
|
|
555
|
+
}
|
|
556
|
+
isRouteConfigured(routeName) {
|
|
557
|
+
if (!routeName) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
const pool = this.routing[routeName];
|
|
561
|
+
return Array.isArray(pool) && pool.length > 0;
|
|
562
|
+
}
|
|
273
563
|
sortByPriority(routeNames) {
|
|
274
564
|
return [...routeNames].sort((a, b) => this.routeWeight(a) - this.routeWeight(b));
|
|
275
565
|
}
|
|
@@ -277,25 +567,89 @@ export class VirtualRouterEngine {
|
|
|
277
567
|
const idx = ROUTE_PRIORITY.indexOf(routeName);
|
|
278
568
|
return idx >= 0 ? idx : ROUTE_PRIORITY.length;
|
|
279
569
|
}
|
|
280
|
-
buildHitReason(routeUsed, classification, features) {
|
|
570
|
+
buildHitReason(routeUsed, classification, features, sticky, phase) {
|
|
281
571
|
const reasoning = classification.reasoning || '';
|
|
282
572
|
const primary = reasoning.split('|')[0] || '';
|
|
283
573
|
const lastToolName = features.lastAssistantToolName;
|
|
574
|
+
const lastToolDetail = features.lastAssistantToolDetail;
|
|
284
575
|
if (routeUsed === 'tools') {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
576
|
+
const decoratedTools = lastToolName ? this.formatToolIdentifier(lastToolName, lastToolDetail) : null;
|
|
577
|
+
const base = primary
|
|
578
|
+
? decoratedTools
|
|
579
|
+
? `${primary}(${decoratedTools})`
|
|
580
|
+
: primary
|
|
581
|
+
: decoratedTools
|
|
582
|
+
? `tools(${decoratedTools})`
|
|
583
|
+
: 'tools';
|
|
584
|
+
return this.decorateReason(base, sticky, phase);
|
|
289
585
|
}
|
|
290
586
|
if (routeUsed === 'thinking') {
|
|
291
|
-
return primary || 'thinking';
|
|
587
|
+
return this.decorateReason(primary || 'thinking', sticky, phase);
|
|
292
588
|
}
|
|
293
589
|
if (routeUsed === DEFAULT_ROUTE && classification.fallback) {
|
|
294
|
-
return primary || 'fallback:default';
|
|
590
|
+
return this.decorateReason(primary || 'fallback:default', sticky, phase);
|
|
295
591
|
}
|
|
296
592
|
if (primary) {
|
|
297
|
-
return primary;
|
|
593
|
+
return this.decorateReason(primary, sticky, phase);
|
|
594
|
+
}
|
|
595
|
+
return this.decorateReason(routeUsed ? `route:${routeUsed}` : 'route:unknown', sticky, phase);
|
|
596
|
+
}
|
|
597
|
+
formatToolIdentifier(name, detail) {
|
|
598
|
+
if (!detail) {
|
|
599
|
+
return name;
|
|
600
|
+
}
|
|
601
|
+
return `${name}:${detail}`;
|
|
602
|
+
}
|
|
603
|
+
decorateReason(base, sticky, phase) {
|
|
604
|
+
let result = base;
|
|
605
|
+
if (sticky) {
|
|
606
|
+
result = `${result}|sticky:${sticky.reason}`;
|
|
607
|
+
}
|
|
608
|
+
if (phase === 'execution') {
|
|
609
|
+
result = `${result}|phase=execution`;
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
buildVirtualRouterHitLog(route, providerKey, modelId, reason, sticky, phase) {
|
|
614
|
+
const parts = ['[virtual-router-hit]', route, providerKey];
|
|
615
|
+
if (modelId) {
|
|
616
|
+
parts.push(modelId);
|
|
617
|
+
}
|
|
618
|
+
if (reason) {
|
|
619
|
+
parts.push(`reason=${reason}`);
|
|
620
|
+
}
|
|
621
|
+
parts.push(`phase=${phase}`);
|
|
622
|
+
if (sticky) {
|
|
623
|
+
const total = Math.max(1, sticky.totalRounds || sticky.remainingRounds || 1);
|
|
624
|
+
const consumed = Math.max(1, Math.min(total, total - sticky.remainingRounds));
|
|
625
|
+
const descriptor = `${sticky.strategy}:${sticky.reason}[${consumed}/${total};${sticky.mode}]`;
|
|
626
|
+
parts.push(`sticky=${descriptor}`);
|
|
627
|
+
}
|
|
628
|
+
const message = parts.filter((segment) => typeof segment === 'string' && segment.length > 0).join(' ');
|
|
629
|
+
return this.colorizeVirtualRouterLog(message);
|
|
630
|
+
}
|
|
631
|
+
colorizeVirtualRouterLog(message) {
|
|
632
|
+
if (!this.shouldColorVirtualRouterLogs()) {
|
|
633
|
+
return message;
|
|
634
|
+
}
|
|
635
|
+
return `${VIRTUAL_ROUTER_HIT_COLOR}${message}${ANSI_RESET}`;
|
|
636
|
+
}
|
|
637
|
+
shouldColorVirtualRouterLogs() {
|
|
638
|
+
if (typeof process === 'undefined') {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
const noColor = String(process.env.NO_COLOR ?? process.env.RCC_NO_COLOR ?? '').toLowerCase();
|
|
642
|
+
if (noColor === '1' || noColor === 'true') {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
const forceColor = String(process.env.FORCE_COLOR ?? '').trim();
|
|
646
|
+
if (forceColor === '0') {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
const stdout = process.stdout;
|
|
650
|
+
if (stdout && stdout.isTTY === false) {
|
|
651
|
+
return false;
|
|
298
652
|
}
|
|
299
|
-
return
|
|
653
|
+
return true;
|
|
300
654
|
}
|
|
301
655
|
}
|
|
@@ -43,6 +43,8 @@ const WRITE_TOOL_KEYWORDS = ['write', 'patch', 'modify', 'edit', 'create', 'upda
|
|
|
43
43
|
const SEARCH_TOOL_KEYWORDS = ['search', 'websearch', 'web_fetch', 'webfetch', 'web-request', 'web_request', 'internet'];
|
|
44
44
|
const SHELL_TOOL_NAMES = new Set(['shell_command', 'shell', 'bash']);
|
|
45
45
|
const SHELL_HEREDOC_PATTERN = /<<\s*['"]?[a-z0-9_-]+/i;
|
|
46
|
+
const COMMAND_DETAIL_MAX_LENGTH = 80;
|
|
47
|
+
const TOOL_CATEGORY_PRIORITY = ['search', 'write', 'read', 'other'];
|
|
46
48
|
const SHELL_WRITE_PATTERNS = [
|
|
47
49
|
'apply_patch',
|
|
48
50
|
'sed -i',
|
|
@@ -134,7 +136,7 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
134
136
|
const hasCodingTool = detectCodingTool(request);
|
|
135
137
|
const hasWebTool = detectWebTool(request);
|
|
136
138
|
const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
|
|
137
|
-
const
|
|
139
|
+
const assistantToolSummary = summarizeAssistantToolUsage(assistantMessages);
|
|
138
140
|
return {
|
|
139
141
|
requestId: metadata.requestId,
|
|
140
142
|
model: request.model,
|
|
@@ -149,8 +151,11 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
149
151
|
hasCodingTool,
|
|
150
152
|
hasThinkingKeyword,
|
|
151
153
|
estimatedTokens,
|
|
152
|
-
lastAssistantToolCategory:
|
|
153
|
-
lastAssistantToolName:
|
|
154
|
+
lastAssistantToolCategory: assistantToolSummary.primary?.category,
|
|
155
|
+
lastAssistantToolName: assistantToolSummary.primary?.name,
|
|
156
|
+
lastAssistantToolDetail: assistantToolSummary.primary?.detail,
|
|
157
|
+
assistantToolCategories: assistantToolSummary.categories,
|
|
158
|
+
assistantCalledWebSearchTool: assistantToolSummary.usedWebSearchTool,
|
|
154
159
|
metadata: {
|
|
155
160
|
...metadata
|
|
156
161
|
}
|
|
@@ -294,30 +299,73 @@ function extractToolDescription(tool) {
|
|
|
294
299
|
}
|
|
295
300
|
return '';
|
|
296
301
|
}
|
|
297
|
-
function
|
|
302
|
+
function summarizeAssistantToolUsage(messages) {
|
|
298
303
|
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
299
304
|
const msg = messages[idx];
|
|
300
305
|
if (!msg || !Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) {
|
|
301
306
|
continue;
|
|
302
307
|
}
|
|
303
|
-
|
|
308
|
+
const classifications = [];
|
|
309
|
+
let usedWebSearchTool = false;
|
|
304
310
|
for (const call of msg.tool_calls) {
|
|
305
311
|
const classification = classifyToolCall(call);
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
if (classification.category !== 'other') {
|
|
313
|
-
return classification;
|
|
312
|
+
if (classification) {
|
|
313
|
+
classifications.push(classification);
|
|
314
|
+
if (classification.category === 'search' && isWebSearchToolInvocation(classification)) {
|
|
315
|
+
usedWebSearchTool = true;
|
|
316
|
+
}
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
319
|
+
if (!classifications.length) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const categorySet = new Set();
|
|
323
|
+
for (const classification of classifications) {
|
|
324
|
+
categorySet.add(classification.category);
|
|
325
|
+
}
|
|
326
|
+
const categories = orderToolCategories(Array.from(categorySet));
|
|
327
|
+
const primary = classifications.find((classification) => classification.category !== 'other') ?? classifications[0];
|
|
328
|
+
return {
|
|
329
|
+
categories,
|
|
330
|
+
primary,
|
|
331
|
+
usedWebSearchTool
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return { categories: [], usedWebSearchTool: false };
|
|
335
|
+
}
|
|
336
|
+
function orderToolCategories(categories) {
|
|
337
|
+
const ordered = [];
|
|
338
|
+
for (const category of TOOL_CATEGORY_PRIORITY) {
|
|
339
|
+
if (categories.includes(category)) {
|
|
340
|
+
ordered.push(category);
|
|
318
341
|
}
|
|
319
342
|
}
|
|
320
|
-
|
|
343
|
+
for (const category of categories) {
|
|
344
|
+
if (!ordered.includes(category)) {
|
|
345
|
+
ordered.push(category);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return ordered;
|
|
349
|
+
}
|
|
350
|
+
function isWebSearchToolName(name) {
|
|
351
|
+
const normalized = name.toLowerCase();
|
|
352
|
+
if (SEARCH_TOOL_EXACT.has(normalized)) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return WEB_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()));
|
|
356
|
+
}
|
|
357
|
+
function isWebSearchToolInvocation(classification) {
|
|
358
|
+
if (!classification) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (isWebSearchToolName(classification.name)) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (classification.detail) {
|
|
365
|
+
const detail = classification.detail.toLowerCase();
|
|
366
|
+
return WEB_TOOL_KEYWORDS.some((keyword) => detail.includes(keyword.toLowerCase()));
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
321
369
|
}
|
|
322
370
|
function classifyToolCall(call) {
|
|
323
371
|
if (!call || typeof call !== 'object') {
|
|
@@ -330,14 +378,19 @@ function classifyToolCall(call) {
|
|
|
330
378
|
return undefined;
|
|
331
379
|
}
|
|
332
380
|
const argsObject = parseToolArguments(call?.function?.arguments);
|
|
333
|
-
const commandText = extractCommandText(argsObject);
|
|
381
|
+
const commandText = extractCommandText(argsObject).trim();
|
|
382
|
+
const commandDetail = summarizeCommandDetail(commandText);
|
|
334
383
|
const nameCategory = categorizeToolName(functionName);
|
|
335
384
|
if (nameCategory === 'write' || nameCategory === 'read' || nameCategory === 'search') {
|
|
336
385
|
return { category: nameCategory, name: functionName };
|
|
337
386
|
}
|
|
338
387
|
if (SHELL_TOOL_NAMES.has(functionName)) {
|
|
339
388
|
const shellCategory = classifyShellCommand(commandText);
|
|
340
|
-
return {
|
|
389
|
+
return {
|
|
390
|
+
category: shellCategory,
|
|
391
|
+
name: functionName,
|
|
392
|
+
detail: commandDetail
|
|
393
|
+
};
|
|
341
394
|
}
|
|
342
395
|
if (commandText) {
|
|
343
396
|
const derivedCategory = classifyShellCommand(commandText);
|
|
@@ -380,7 +433,12 @@ function extractCommandText(args) {
|
|
|
380
433
|
return args;
|
|
381
434
|
}
|
|
382
435
|
if (Array.isArray(args)) {
|
|
383
|
-
|
|
436
|
+
const tokens = collectCommandTokens(args);
|
|
437
|
+
if (tokens.length) {
|
|
438
|
+
return tokens.join(' ');
|
|
439
|
+
}
|
|
440
|
+
const derived = extractFirstStringValue(args);
|
|
441
|
+
return derived ?? '';
|
|
384
442
|
}
|
|
385
443
|
if (typeof args === 'object') {
|
|
386
444
|
const record = args;
|
|
@@ -391,7 +449,20 @@ function extractCommandText(args) {
|
|
|
391
449
|
return command;
|
|
392
450
|
}
|
|
393
451
|
if (Array.isArray(command)) {
|
|
394
|
-
|
|
452
|
+
const tokens = collectCommandTokens(command);
|
|
453
|
+
if (tokens.length) {
|
|
454
|
+
return tokens.join(' ');
|
|
455
|
+
}
|
|
456
|
+
const derivedCommand = extractFirstStringValue(command);
|
|
457
|
+
if (derivedCommand) {
|
|
458
|
+
return derivedCommand;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (command && typeof command === 'object') {
|
|
462
|
+
const derivedCommand = extractFirstStringValue(command);
|
|
463
|
+
if (derivedCommand) {
|
|
464
|
+
return derivedCommand;
|
|
465
|
+
}
|
|
395
466
|
}
|
|
396
467
|
if (typeof input === 'string') {
|
|
397
468
|
return input;
|
|
@@ -400,7 +471,30 @@ function extractCommandText(args) {
|
|
|
400
471
|
return nestedArgs;
|
|
401
472
|
}
|
|
402
473
|
if (Array.isArray(nestedArgs)) {
|
|
403
|
-
|
|
474
|
+
const tokens = collectCommandTokens(nestedArgs);
|
|
475
|
+
if (tokens.length) {
|
|
476
|
+
return tokens.join(' ');
|
|
477
|
+
}
|
|
478
|
+
const derivedArgs = extractFirstStringValue(nestedArgs);
|
|
479
|
+
if (derivedArgs) {
|
|
480
|
+
return derivedArgs;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (nestedArgs && typeof nestedArgs === 'object') {
|
|
484
|
+
const derivedArgs = extractFirstStringValue(nestedArgs);
|
|
485
|
+
if (derivedArgs) {
|
|
486
|
+
return derivedArgs;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const fallback = extractFirstStringValue(record);
|
|
490
|
+
if (fallback) {
|
|
491
|
+
return fallback;
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
return JSON.stringify(record);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return '';
|
|
404
498
|
}
|
|
405
499
|
}
|
|
406
500
|
return '';
|
|
@@ -489,3 +583,71 @@ function trimEnclosingQuotes(value) {
|
|
|
489
583
|
}
|
|
490
584
|
return value;
|
|
491
585
|
}
|
|
586
|
+
function summarizeCommandDetail(command) {
|
|
587
|
+
if (!command) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
const [firstSegment] = splitCommandSegments(command);
|
|
591
|
+
const candidate = firstSegment ?? command;
|
|
592
|
+
const normalized = candidate.replace(/\s+/g, ' ').trim();
|
|
593
|
+
if (!normalized) {
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
if (normalized.length > COMMAND_DETAIL_MAX_LENGTH) {
|
|
597
|
+
return `${normalized.slice(0, COMMAND_DETAIL_MAX_LENGTH - 1)}…`;
|
|
598
|
+
}
|
|
599
|
+
return normalized;
|
|
600
|
+
}
|
|
601
|
+
function collectCommandTokens(values) {
|
|
602
|
+
const tokens = [];
|
|
603
|
+
for (const value of values) {
|
|
604
|
+
if (typeof value === 'string') {
|
|
605
|
+
const trimmed = value.trim();
|
|
606
|
+
if (trimmed) {
|
|
607
|
+
tokens.push(trimmed);
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (Array.isArray(value)) {
|
|
612
|
+
const nested = collectCommandTokens(value);
|
|
613
|
+
if (nested.length) {
|
|
614
|
+
tokens.push(nested.join(' '));
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (value && typeof value === 'object') {
|
|
619
|
+
const extracted = extractFirstStringValue(value);
|
|
620
|
+
if (extracted) {
|
|
621
|
+
tokens.push(extracted.trim());
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return tokens.filter(Boolean).slice(0, 16);
|
|
626
|
+
}
|
|
627
|
+
function extractFirstStringValue(value) {
|
|
628
|
+
if (!value) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
if (typeof value === 'string') {
|
|
632
|
+
const trimmed = value.trim();
|
|
633
|
+
return trimmed || undefined;
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(value)) {
|
|
636
|
+
for (const item of value) {
|
|
637
|
+
const extracted = extractFirstStringValue(item);
|
|
638
|
+
if (extracted) {
|
|
639
|
+
return extracted;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
if (typeof value === 'object') {
|
|
645
|
+
for (const candidate of Object.values(value)) {
|
|
646
|
+
const extracted = extractFirstStringValue(candidate);
|
|
647
|
+
if (extracted) {
|
|
648
|
+
return extracted;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
@@ -118,6 +118,9 @@ export interface RoutingFeatures {
|
|
|
118
118
|
estimatedTokens: number;
|
|
119
119
|
lastAssistantToolCategory?: 'read' | 'write' | 'search' | 'other';
|
|
120
120
|
lastAssistantToolName?: string;
|
|
121
|
+
lastAssistantToolDetail?: string;
|
|
122
|
+
assistantToolCategories?: Array<'read' | 'write' | 'search' | 'other'>;
|
|
123
|
+
assistantCalledWebSearchTool?: boolean;
|
|
121
124
|
metadata: RouterMetadataInput;
|
|
122
125
|
}
|
|
123
126
|
export interface ClassificationResult {
|
|
@@ -145,8 +148,11 @@ export interface TargetMetadata {
|
|
|
145
148
|
processMode?: 'chat' | 'passthrough';
|
|
146
149
|
responsesConfig?: ResponsesProviderConfig;
|
|
147
150
|
}
|
|
151
|
+
export type ResponsesStreamingMode = 'auto' | 'always' | 'never';
|
|
148
152
|
export interface ResponsesProviderConfig {
|
|
149
153
|
toolCallIdStyle?: 'fc' | 'preserve';
|
|
154
|
+
streaming?: ResponsesStreamingMode;
|
|
155
|
+
instructionsMode?: 'default' | 'inline';
|
|
150
156
|
}
|
|
151
157
|
export declare enum VirtualRouterErrorCode {
|
|
152
158
|
NO_STANDARDIZED_REQUEST = "NO_STANDARDIZED_REQUEST",
|