@jsonstudio/llms 0.6.203 → 0.6.230
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversion/codecs/gemini-openai-codec.js +128 -4
- package/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-web-search.js +66 -0
- package/dist/conversion/compat/profiles/chat-glm.json +4 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +11 -3
- package/dist/conversion/hub/process/chat-process.js +131 -1
- package/dist/conversion/hub/response/provider-response.d.ts +22 -0
- package/dist/conversion/hub/response/provider-response.js +12 -1
- package/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
- package/dist/conversion/hub/response/server-side-tools.js +326 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +118 -11
- package/dist/conversion/hub/types/standardized.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.js +49 -3
- package/dist/conversion/shared/snapshot-utils.js +17 -47
- package/dist/conversion/shared/tool-mapping.js +25 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +273 -40
- package/dist/router/virtual-router/context-advisor.d.ts +0 -2
- package/dist/router/virtual-router/context-advisor.js +0 -12
- package/dist/router/virtual-router/engine.d.ts +8 -2
- package/dist/router/virtual-router/engine.js +176 -81
- package/dist/router/virtual-router/types.d.ts +21 -2
- package/dist/sse/json-to-sse/event-generators/responses.js +15 -3
- package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
- package/dist/sse/types/gemini-types.d.ts +20 -1
- package/dist/sse/types/responses-types.js +1 -1
- package/dist/telemetry/stats-center.d.ts +73 -0
- package/dist/telemetry/stats-center.js +280 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { RoutingClassifier } from './classifier.js';
|
|
|
5
5
|
import { buildRoutingFeatures } from './features.js';
|
|
6
6
|
import { ContextAdvisor } from './context-advisor.js';
|
|
7
7
|
import { DEFAULT_MODEL_CONTEXT_TOKENS, DEFAULT_ROUTE, ROUTE_PRIORITY, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
8
|
+
import { getStatsCenter } from '../../telemetry/stats-center.js';
|
|
8
9
|
export class VirtualRouterEngine {
|
|
9
10
|
routing = {};
|
|
10
11
|
providerRegistry = new ProviderRegistry();
|
|
@@ -16,6 +17,7 @@ export class VirtualRouterEngine {
|
|
|
16
17
|
routeStats = new Map();
|
|
17
18
|
debug = console; // thin hook; host may monkey-patch for colored logging
|
|
18
19
|
healthConfig = null;
|
|
20
|
+
statsCenter = getStatsCenter();
|
|
19
21
|
initialize(config) {
|
|
20
22
|
this.validateConfig(config);
|
|
21
23
|
this.routing = config.routing;
|
|
@@ -40,21 +42,36 @@ export class VirtualRouterEngine {
|
|
|
40
42
|
const target = this.providerRegistry.buildTarget(selection.providerKey);
|
|
41
43
|
this.healthManager.recordSuccess(selection.providerKey);
|
|
42
44
|
this.incrementRouteStat(selection.routeUsed, selection.providerKey);
|
|
45
|
+
try {
|
|
46
|
+
this.statsCenter.recordVirtualRouterHit({
|
|
47
|
+
requestId: metadata.requestId,
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
entryEndpoint: metadata.entryEndpoint || '/v1/chat/completions',
|
|
50
|
+
routeName: selection.routeUsed,
|
|
51
|
+
pool: selection.poolId || selection.routeUsed,
|
|
52
|
+
providerKey: selection.providerKey,
|
|
53
|
+
modelId: target.modelId || undefined
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// stats must never break routing
|
|
58
|
+
}
|
|
43
59
|
const hitReason = this.buildHitReason(selection.routeUsed, selection.providerKey, classification, features);
|
|
44
|
-
const formatted = this.formatVirtualRouterHit(selection.routeUsed, selection.providerKey, target.modelId || '', hitReason);
|
|
60
|
+
const formatted = this.formatVirtualRouterHit(selection.routeUsed, selection.poolId, selection.providerKey, target.modelId || '', hitReason);
|
|
45
61
|
if (formatted) {
|
|
46
62
|
this.debug?.log?.(formatted);
|
|
47
63
|
}
|
|
48
64
|
else {
|
|
49
65
|
this.debug?.log?.('[virtual-router-hit]', selection.routeUsed, selection.providerKey, target.modelId || '', hitReason ? `reason=${hitReason}` : '');
|
|
50
66
|
}
|
|
51
|
-
const didFallback = selection.routeUsed !== routeName
|
|
67
|
+
const didFallback = selection.routeUsed !== routeName;
|
|
52
68
|
return {
|
|
53
69
|
target,
|
|
54
70
|
decision: {
|
|
55
71
|
routeName: selection.routeUsed,
|
|
56
72
|
providerKey: selection.providerKey,
|
|
57
73
|
pool: selection.pool,
|
|
74
|
+
poolId: selection.poolId,
|
|
58
75
|
confidence: classification.confidence,
|
|
59
76
|
reasoning: classification.reasoning,
|
|
60
77
|
fallback: didFallback
|
|
@@ -63,6 +80,7 @@ export class VirtualRouterEngine {
|
|
|
63
80
|
routeName: selection.routeUsed,
|
|
64
81
|
providerKey: selection.providerKey,
|
|
65
82
|
pool: selection.pool,
|
|
83
|
+
poolId: selection.poolId,
|
|
66
84
|
reasoning: classification.reasoning,
|
|
67
85
|
fallback: didFallback,
|
|
68
86
|
confidence: classification.confidence
|
|
@@ -92,10 +110,10 @@ export class VirtualRouterEngine {
|
|
|
92
110
|
}
|
|
93
111
|
getStatus() {
|
|
94
112
|
const routes = {};
|
|
95
|
-
for (const [route,
|
|
113
|
+
for (const [route, pools] of Object.entries(this.routing)) {
|
|
96
114
|
const stats = this.routeStats.get(route) ?? { hits: 0 };
|
|
97
115
|
routes[route] = {
|
|
98
|
-
providers:
|
|
116
|
+
providers: this.flattenPoolTargets(pools),
|
|
99
117
|
hits: stats.hits,
|
|
100
118
|
lastUsedProvider: stats.lastProvider
|
|
101
119
|
};
|
|
@@ -112,21 +130,29 @@ export class VirtualRouterEngine {
|
|
|
112
130
|
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
113
131
|
throw new VirtualRouterError('providers configuration is required', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
114
132
|
}
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
133
|
+
const defaultPools = config.routing[DEFAULT_ROUTE];
|
|
134
|
+
if (!this.routeHasTargets(defaultPools)) {
|
|
117
135
|
throw new VirtualRouterError('default route must be configured with at least one provider', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
118
136
|
}
|
|
137
|
+
if (!this.hasPrimaryPool(defaultPools)) {
|
|
138
|
+
throw new VirtualRouterError('default route must define at least one non-backup pool', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
139
|
+
}
|
|
119
140
|
const providerKeys = new Set(Object.keys(config.providers));
|
|
120
|
-
for (const [routeName,
|
|
121
|
-
if (!
|
|
141
|
+
for (const [routeName, pools] of Object.entries(config.routing)) {
|
|
142
|
+
if (!this.routeHasTargets(pools)) {
|
|
122
143
|
if (routeName === DEFAULT_ROUTE) {
|
|
123
144
|
throw new VirtualRouterError('default route cannot be empty', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
124
145
|
}
|
|
125
146
|
continue;
|
|
126
147
|
}
|
|
127
|
-
for (const
|
|
128
|
-
if (!
|
|
129
|
-
|
|
148
|
+
for (const pool of pools) {
|
|
149
|
+
if (!Array.isArray(pool.targets) || !pool.targets.length) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const providerKey of pool.targets) {
|
|
153
|
+
if (!providerKeys.has(providerKey)) {
|
|
154
|
+
throw new VirtualRouterError(`Route ${routeName} references unknown provider ${providerKey}`, VirtualRouterErrorCode.CONFIG_ERROR);
|
|
155
|
+
}
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
}
|
|
@@ -136,8 +162,7 @@ export class VirtualRouterEngine {
|
|
|
136
162
|
const stickyKey = this.resolveStickyKey(metadata);
|
|
137
163
|
const attempted = [];
|
|
138
164
|
const visitedRoutes = new Set();
|
|
139
|
-
const
|
|
140
|
-
const routeQueue = this.initializeRouteQueue(candidates, fallbackRoute);
|
|
165
|
+
const routeQueue = this.initializeRouteQueue(candidates);
|
|
141
166
|
const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
|
|
142
167
|
? Math.max(0, features.estimatedTokens)
|
|
143
168
|
: 0;
|
|
@@ -146,33 +171,51 @@ export class VirtualRouterEngine {
|
|
|
146
171
|
if (visitedRoutes.has(routeName)) {
|
|
147
172
|
continue;
|
|
148
173
|
}
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
174
|
+
const routePools = this.routing[routeName];
|
|
175
|
+
if (!this.routeHasTargets(routePools)) {
|
|
151
176
|
visitedRoutes.add(routeName);
|
|
152
|
-
attempted.push(routeName);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const contextResult = this.contextAdvisor.classify(pool, estimatedTokens, (key) => this.providerRegistry.get(key));
|
|
156
|
-
if (this.maybeDeferToFallback(routeName, contextResult, routeQueue, visitedRoutes, fallbackRoute)) {
|
|
177
|
+
attempted.push(`${routeName}:empty`);
|
|
157
178
|
continue;
|
|
158
179
|
}
|
|
159
180
|
visitedRoutes.add(routeName);
|
|
160
|
-
const
|
|
161
|
-
for (const
|
|
162
|
-
const providerKey = this.
|
|
163
|
-
routeName,
|
|
164
|
-
candidates: candidatePool,
|
|
165
|
-
stickyKey,
|
|
166
|
-
availabilityCheck: (key) => this.healthManager.isAvailable(key)
|
|
167
|
-
});
|
|
181
|
+
const orderedPools = this.sortRoutePools(routePools);
|
|
182
|
+
for (const poolTier of orderedPools) {
|
|
183
|
+
const { providerKey, poolTargets, tierId, failureHint } = this.trySelectFromTier(routeName, poolTier, stickyKey, estimatedTokens);
|
|
168
184
|
if (providerKey) {
|
|
169
|
-
return { providerKey, routeUsed: routeName, pool };
|
|
185
|
+
return { providerKey, routeUsed: routeName, pool: poolTargets, poolId: tierId };
|
|
186
|
+
}
|
|
187
|
+
if (failureHint) {
|
|
188
|
+
attempted.push(failureHint);
|
|
170
189
|
}
|
|
171
190
|
}
|
|
172
|
-
attempted.push(this.describeAttempt(routeName, contextResult));
|
|
173
191
|
}
|
|
174
192
|
throw new VirtualRouterError(`All providers unavailable for route ${requestedRoute}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { routeName: requestedRoute, attempted });
|
|
175
193
|
}
|
|
194
|
+
trySelectFromTier(routeName, tier, stickyKey, estimatedTokens) {
|
|
195
|
+
const targets = Array.isArray(tier.targets) ? tier.targets : [];
|
|
196
|
+
if (!targets.length) {
|
|
197
|
+
return { providerKey: null, poolTargets: [], tierId: tier.id, failureHint: `${routeName}:${tier.id}:empty` };
|
|
198
|
+
}
|
|
199
|
+
const contextResult = this.contextAdvisor.classify(targets, estimatedTokens, (key) => this.providerRegistry.get(key));
|
|
200
|
+
const prioritizedPools = this.buildContextCandidatePools(contextResult);
|
|
201
|
+
for (const candidatePool of prioritizedPools) {
|
|
202
|
+
const providerKey = this.loadBalancer.select({
|
|
203
|
+
routeName: `${routeName}:${tier.id}`,
|
|
204
|
+
candidates: candidatePool,
|
|
205
|
+
stickyKey,
|
|
206
|
+
availabilityCheck: (key) => this.healthManager.isAvailable(key)
|
|
207
|
+
});
|
|
208
|
+
if (providerKey) {
|
|
209
|
+
return { providerKey, poolTargets: tier.targets, tierId: tier.id };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
providerKey: null,
|
|
214
|
+
poolTargets: tier.targets,
|
|
215
|
+
tierId: tier.id,
|
|
216
|
+
failureHint: this.describeAttempt(routeName, tier.id, contextResult)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
176
219
|
incrementRouteStat(routeName, providerKey) {
|
|
177
220
|
if (!this.routeStats.has(routeName)) {
|
|
178
221
|
this.routeStats.set(routeName, { hits: 0, lastProvider: providerKey });
|
|
@@ -185,63 +228,34 @@ export class VirtualRouterEngine {
|
|
|
185
228
|
providerHealthConfig() {
|
|
186
229
|
return this.healthManager.getConfig();
|
|
187
230
|
}
|
|
188
|
-
initializeRouteQueue(candidates
|
|
189
|
-
|
|
190
|
-
if (fallbackRoute && !queue.includes(fallbackRoute)) {
|
|
191
|
-
queue.push(fallbackRoute);
|
|
192
|
-
}
|
|
193
|
-
return queue;
|
|
194
|
-
}
|
|
195
|
-
resolveFallbackRoute() {
|
|
196
|
-
const candidate = this.contextRouting?.fallbackRoute;
|
|
197
|
-
if (!candidate) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
const pool = this.routing[candidate];
|
|
201
|
-
if (!Array.isArray(pool) || pool.length === 0) {
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
return candidate;
|
|
205
|
-
}
|
|
206
|
-
maybeDeferToFallback(routeName, contextResult, queue, visited, fallbackRoute) {
|
|
207
|
-
if (!fallbackRoute || fallbackRoute === routeName || visited.has(fallbackRoute)) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
if (!this.contextAdvisor.prefersFallback(contextResult)) {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
const fallbackPool = this.routing[fallbackRoute];
|
|
214
|
-
if (!Array.isArray(fallbackPool) || fallbackPool.length === 0) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
queue.unshift(routeName);
|
|
218
|
-
queue.unshift(fallbackRoute);
|
|
219
|
-
return true;
|
|
231
|
+
initializeRouteQueue(candidates) {
|
|
232
|
+
return Array.from(new Set(candidates));
|
|
220
233
|
}
|
|
221
234
|
buildContextCandidatePools(result) {
|
|
222
235
|
const ordered = [];
|
|
223
236
|
if (result.safe.length) {
|
|
224
237
|
ordered.push(result.safe);
|
|
238
|
+
// 如果存在安全候选,直接放弃当前处于警戒阈值的模型
|
|
239
|
+
return ordered;
|
|
225
240
|
}
|
|
226
241
|
if (result.risky.length) {
|
|
227
242
|
ordered.push(result.risky);
|
|
228
243
|
}
|
|
229
|
-
|
|
230
|
-
ordered.push(result.overflow);
|
|
231
|
-
}
|
|
244
|
+
// ratio >= 1 视为上下文溢出,直接标记为不可用
|
|
232
245
|
return ordered;
|
|
233
246
|
}
|
|
234
|
-
describeAttempt(routeName, result) {
|
|
247
|
+
describeAttempt(routeName, poolId, result) {
|
|
248
|
+
const prefix = poolId ? `${routeName}:${poolId}` : routeName;
|
|
235
249
|
if (result.safe.length > 0) {
|
|
236
|
-
return `${
|
|
250
|
+
return `${prefix}:health`;
|
|
237
251
|
}
|
|
238
252
|
if (result.risky.length > 0) {
|
|
239
|
-
return `${
|
|
253
|
+
return `${prefix}:context_risky`;
|
|
240
254
|
}
|
|
241
255
|
if (result.overflow.length > 0) {
|
|
242
|
-
return `${
|
|
256
|
+
return `${prefix}:max_context_window`;
|
|
243
257
|
}
|
|
244
|
-
return
|
|
258
|
+
return prefix;
|
|
245
259
|
}
|
|
246
260
|
resolveStickyKey(metadata) {
|
|
247
261
|
const resume = metadata.responsesResume;
|
|
@@ -347,13 +361,8 @@ export class VirtualRouterEngine {
|
|
|
347
361
|
if (!deduped.includes(DEFAULT_ROUTE)) {
|
|
348
362
|
deduped.push(DEFAULT_ROUTE);
|
|
349
363
|
}
|
|
350
|
-
const filtered = deduped.filter((routeName) =>
|
|
351
|
-
|
|
352
|
-
return Array.isArray(pool) && pool.length > 0;
|
|
353
|
-
});
|
|
354
|
-
if (!filtered.includes(DEFAULT_ROUTE) &&
|
|
355
|
-
Array.isArray(this.routing[DEFAULT_ROUTE]) &&
|
|
356
|
-
this.routing[DEFAULT_ROUTE].length > 0) {
|
|
364
|
+
const filtered = deduped.filter((routeName) => this.routeHasTargets(this.routing[routeName]));
|
|
365
|
+
if (!filtered.includes(DEFAULT_ROUTE) && this.routeHasTargets(this.routing[DEFAULT_ROUTE])) {
|
|
357
366
|
filtered.push(DEFAULT_ROUTE);
|
|
358
367
|
}
|
|
359
368
|
return filtered.length ? filtered : [DEFAULT_ROUTE];
|
|
@@ -365,6 +374,52 @@ export class VirtualRouterEngine {
|
|
|
365
374
|
const idx = ROUTE_PRIORITY.indexOf(routeName);
|
|
366
375
|
return idx >= 0 ? idx : ROUTE_PRIORITY.length;
|
|
367
376
|
}
|
|
377
|
+
routeHasTargets(pools) {
|
|
378
|
+
if (!Array.isArray(pools)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
return pools.some((pool) => Array.isArray(pool.targets) && pool.targets.length > 0);
|
|
382
|
+
}
|
|
383
|
+
hasPrimaryPool(pools) {
|
|
384
|
+
if (!Array.isArray(pools)) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return pools.some((pool) => !pool.backup && Array.isArray(pool.targets) && pool.targets.length > 0);
|
|
388
|
+
}
|
|
389
|
+
sortRoutePools(pools) {
|
|
390
|
+
if (!Array.isArray(pools)) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
return pools
|
|
394
|
+
.filter((pool) => Array.isArray(pool.targets) && pool.targets.length > 0)
|
|
395
|
+
.sort((a, b) => {
|
|
396
|
+
if (a.backup && !b.backup)
|
|
397
|
+
return 1;
|
|
398
|
+
if (!a.backup && b.backup)
|
|
399
|
+
return -1;
|
|
400
|
+
if (a.priority !== b.priority) {
|
|
401
|
+
return b.priority - a.priority;
|
|
402
|
+
}
|
|
403
|
+
return a.id.localeCompare(b.id);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
flattenPoolTargets(pools) {
|
|
407
|
+
const flattened = [];
|
|
408
|
+
if (!Array.isArray(pools)) {
|
|
409
|
+
return flattened;
|
|
410
|
+
}
|
|
411
|
+
for (const pool of pools) {
|
|
412
|
+
if (!Array.isArray(pool.targets)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
for (const target of pool.targets) {
|
|
416
|
+
if (typeof target === 'string' && target && !flattened.includes(target)) {
|
|
417
|
+
flattened.push(target);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return flattened;
|
|
422
|
+
}
|
|
368
423
|
buildHitReason(routeUsed, providerKey, classification, features) {
|
|
369
424
|
const reasoning = classification.reasoning || '';
|
|
370
425
|
const primary = reasoning.split('|')[0] || '';
|
|
@@ -406,18 +461,31 @@ export class VirtualRouterEngine {
|
|
|
406
461
|
}
|
|
407
462
|
return `${baseLabel}(${normalizedDetail})`;
|
|
408
463
|
}
|
|
409
|
-
formatVirtualRouterHit(routeName, providerKey, modelId, hitReason) {
|
|
464
|
+
formatVirtualRouterHit(routeName, poolId, providerKey, modelId, hitReason) {
|
|
410
465
|
try {
|
|
466
|
+
// 生成本地时间戳
|
|
467
|
+
const now = new Date();
|
|
468
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
469
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
470
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
471
|
+
const timestamp = `${hours}:${minutes}:${seconds}`;
|
|
411
472
|
const prefixColor = '\x1b[38;5;208m';
|
|
412
473
|
const reset = '\x1b[0m';
|
|
474
|
+
const timeColor = '\x1b[90m'; // 灰色
|
|
413
475
|
const routeColor = this.resolveRouteColor(routeName);
|
|
414
476
|
const prefix = `${prefixColor}[virtual-router-hit]${reset}`;
|
|
415
|
-
const
|
|
477
|
+
const timeLabel = `${timeColor}${timestamp}${reset}`;
|
|
478
|
+
const { providerLabel, resolvedModel } = this.describeTargetProvider(providerKey, modelId);
|
|
479
|
+
const routeLabel = poolId ? `${routeName}/${poolId}` : routeName;
|
|
480
|
+
const targetLabel = `${routeLabel} -> ${providerLabel}${resolvedModel ? '.' + resolvedModel : ''}`;
|
|
416
481
|
const reasonLabel = hitReason ? ` reason=${hitReason}` : '';
|
|
417
|
-
return `${prefix} ${routeColor}${targetLabel}${reasonLabel}${reset}`;
|
|
482
|
+
return `${prefix} ${timeLabel} ${routeColor}${targetLabel}${reasonLabel}${reset}`;
|
|
418
483
|
}
|
|
419
484
|
catch {
|
|
420
|
-
|
|
485
|
+
const now = new Date();
|
|
486
|
+
const timestamp = now.toLocaleTimeString('zh-CN', { hour12: false });
|
|
487
|
+
const routeLabel = poolId ? `${routeName}/${poolId}` : routeName;
|
|
488
|
+
return `[virtual-router-hit] ${timestamp} ${routeLabel} -> ${providerKey}${modelId ? '.' + modelId : ''}${hitReason ? ` reason=${hitReason}` : ''}`;
|
|
421
489
|
}
|
|
422
490
|
}
|
|
423
491
|
resolveRouteColor(routeName) {
|
|
@@ -457,4 +525,31 @@ export class VirtualRouterEngine {
|
|
|
457
525
|
}
|
|
458
526
|
return `${ratio.toFixed(2)}/${Math.round(limit)}`;
|
|
459
527
|
}
|
|
528
|
+
describeTargetProvider(providerKey, fallbackModelId) {
|
|
529
|
+
const parsed = this.parseProviderKey(providerKey);
|
|
530
|
+
if (!parsed) {
|
|
531
|
+
return { providerLabel: providerKey, resolvedModel: fallbackModelId };
|
|
532
|
+
}
|
|
533
|
+
const aliasLabel = parsed.keyAlias ? `${parsed.providerId}[${parsed.keyAlias}]` : parsed.providerId;
|
|
534
|
+
const resolvedModel = parsed.modelId || fallbackModelId;
|
|
535
|
+
return { providerLabel: aliasLabel, resolvedModel };
|
|
536
|
+
}
|
|
537
|
+
parseProviderKey(providerKey) {
|
|
538
|
+
const trimmed = typeof providerKey === 'string' ? providerKey.trim() : '';
|
|
539
|
+
if (!trimmed) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
const parts = trimmed.split('.');
|
|
543
|
+
if (parts.length < 2) {
|
|
544
|
+
return { providerId: trimmed };
|
|
545
|
+
}
|
|
546
|
+
if (parts.length === 2) {
|
|
547
|
+
return { providerId: parts[0], modelId: parts[1] };
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
providerId: parts[0],
|
|
551
|
+
keyAlias: parts[1],
|
|
552
|
+
modelId: parts.slice(2).join('.')
|
|
553
|
+
};
|
|
554
|
+
}
|
|
460
555
|
}
|
|
@@ -5,7 +5,13 @@ import type { StandardizedRequest } from '../../conversion/hub/types/standardize
|
|
|
5
5
|
export declare const DEFAULT_MODEL_CONTEXT_TOKENS = 200000;
|
|
6
6
|
export declare const DEFAULT_ROUTE = "default";
|
|
7
7
|
export declare const ROUTE_PRIORITY: string[];
|
|
8
|
-
export
|
|
8
|
+
export interface RoutePoolTier {
|
|
9
|
+
id: string;
|
|
10
|
+
targets: string[];
|
|
11
|
+
priority: number;
|
|
12
|
+
backup?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export type RoutingPools = Record<string, RoutePoolTier[]>;
|
|
9
15
|
export type StreamingPreference = 'auto' | 'always' | 'never';
|
|
10
16
|
export interface ProviderAuthConfig {
|
|
11
17
|
type: 'apiKey' | 'oauth';
|
|
@@ -72,6 +78,16 @@ export interface ProviderHealthConfig {
|
|
|
72
78
|
cooldownMs: number;
|
|
73
79
|
fatalCooldownMs?: number;
|
|
74
80
|
}
|
|
81
|
+
export interface VirtualRouterWebSearchEngineConfig {
|
|
82
|
+
id: string;
|
|
83
|
+
providerKey: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
default?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface VirtualRouterWebSearchConfig {
|
|
88
|
+
engines: VirtualRouterWebSearchEngineConfig[];
|
|
89
|
+
injectPolicy?: 'always' | 'selective';
|
|
90
|
+
}
|
|
75
91
|
export interface VirtualRouterConfig {
|
|
76
92
|
routing: RoutingPools;
|
|
77
93
|
providers: Record<string, ProviderProfile>;
|
|
@@ -79,11 +95,11 @@ export interface VirtualRouterConfig {
|
|
|
79
95
|
loadBalancing?: LoadBalancingPolicy;
|
|
80
96
|
health?: ProviderHealthConfig;
|
|
81
97
|
contextRouting?: VirtualRouterContextRoutingConfig;
|
|
98
|
+
webSearch?: VirtualRouterWebSearchConfig;
|
|
82
99
|
}
|
|
83
100
|
export interface VirtualRouterContextRoutingConfig {
|
|
84
101
|
warnRatio: number;
|
|
85
102
|
hardLimit?: boolean;
|
|
86
|
-
fallbackRoute?: string;
|
|
87
103
|
}
|
|
88
104
|
export type VirtualRouterProviderDefinition = Record<string, unknown>;
|
|
89
105
|
export interface VirtualRouterBootstrapInput extends Record<string, unknown> {
|
|
@@ -94,6 +110,7 @@ export interface VirtualRouterBootstrapInput extends Record<string, unknown> {
|
|
|
94
110
|
loadBalancing?: LoadBalancingPolicy;
|
|
95
111
|
health?: ProviderHealthConfig;
|
|
96
112
|
contextRouting?: VirtualRouterContextRoutingConfig;
|
|
113
|
+
webSearch?: VirtualRouterWebSearchConfig | Record<string, unknown>;
|
|
97
114
|
}
|
|
98
115
|
export type ProviderRuntimeMap = Record<string, ProviderRuntimeProfile>;
|
|
99
116
|
export interface VirtualRouterBootstrapResult {
|
|
@@ -152,6 +169,7 @@ export interface RoutingDecision {
|
|
|
152
169
|
reasoning: string;
|
|
153
170
|
fallback: boolean;
|
|
154
171
|
pool: string[];
|
|
172
|
+
poolId?: string;
|
|
155
173
|
}
|
|
156
174
|
export interface TargetMetadata {
|
|
157
175
|
providerKey: string;
|
|
@@ -185,6 +203,7 @@ export interface RoutingDiagnostics {
|
|
|
185
203
|
reasoning: string;
|
|
186
204
|
fallback: boolean;
|
|
187
205
|
pool: string[];
|
|
206
|
+
poolId?: string;
|
|
188
207
|
confidence: number;
|
|
189
208
|
}
|
|
190
209
|
export interface RoutingStatusSnapshot {
|
|
@@ -8,11 +8,22 @@ function cloneRegex(source) {
|
|
|
8
8
|
return new RegExp(source.source, source.flags);
|
|
9
9
|
}
|
|
10
10
|
function getChunkSize(config) {
|
|
11
|
-
|
|
11
|
+
const size = typeof config.chunkSize === 'number'
|
|
12
|
+
? config.chunkSize
|
|
13
|
+
: DEFAULT_RESPONSES_EVENT_GENERATOR_CONFIG.chunkSize;
|
|
14
|
+
if (size !== undefined && size <= 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return Math.max(1, size || 1);
|
|
12
18
|
}
|
|
13
19
|
function chunkText(text, config) {
|
|
20
|
+
const size = getChunkSize(config);
|
|
21
|
+
if (size === null) {
|
|
22
|
+
// chunking explicitly disabled
|
|
23
|
+
return [text];
|
|
24
|
+
}
|
|
14
25
|
try {
|
|
15
|
-
return StringUtils.chunkString(text,
|
|
26
|
+
return StringUtils.chunkString(text, size, cloneRegex(TEXT_CHUNK_BOUNDARY));
|
|
16
27
|
}
|
|
17
28
|
catch {
|
|
18
29
|
return [text];
|
|
@@ -84,7 +95,8 @@ function normalizeUsage(usage) {
|
|
|
84
95
|
}
|
|
85
96
|
// 默认配置
|
|
86
97
|
export const DEFAULT_RESPONSES_EVENT_GENERATOR_CONFIG = {
|
|
87
|
-
|
|
98
|
+
// 默认关闭文本切片,让上游模型的分块行为原样透传
|
|
99
|
+
chunkSize: 0,
|
|
88
100
|
chunkDelayMs: 8,
|
|
89
101
|
enableIdGeneration: true,
|
|
90
102
|
enableTimestampGeneration: true,
|
|
@@ -70,9 +70,35 @@ export class GeminiSseToJsonConverter {
|
|
|
70
70
|
if (!accumulator.has(candidateIndex)) {
|
|
71
71
|
accumulator.set(candidateIndex, { role, parts: [] });
|
|
72
72
|
}
|
|
73
|
+
const candidate = accumulator.get(candidateIndex);
|
|
73
74
|
const normalizedParts = this.normalizeReasoningPart(part, context);
|
|
74
75
|
for (const normalizedPart of normalizedParts) {
|
|
75
|
-
|
|
76
|
+
// For text parts, accumulate into the last text part instead of creating new ones
|
|
77
|
+
if ('text' in normalizedPart && typeof normalizedPart.text === 'string') {
|
|
78
|
+
const lastPart = candidate.parts[candidate.parts.length - 1];
|
|
79
|
+
if (lastPart && 'text' in lastPart && typeof lastPart.text === 'string') {
|
|
80
|
+
// Append to existing text part
|
|
81
|
+
lastPart.text += normalizedPart.text;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Create new text part
|
|
85
|
+
candidate.parts.push(normalizedPart);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if ('reasoning' in normalizedPart && typeof normalizedPart.reasoning === 'string') {
|
|
89
|
+
// For reasoning parts, also accumulate
|
|
90
|
+
const lastPart = candidate.parts[candidate.parts.length - 1];
|
|
91
|
+
if (lastPart && 'reasoning' in lastPart && typeof lastPart.reasoning === 'string') {
|
|
92
|
+
lastPart.reasoning += normalizedPart.reasoning;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
candidate.parts.push(normalizedPart);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// For other part types (functionCall, functionResponse, etc.), add as separate parts
|
|
100
|
+
candidate.parts.push(normalizedPart);
|
|
101
|
+
}
|
|
76
102
|
}
|
|
77
103
|
}
|
|
78
104
|
buildResponse(accumulator, donePayload) {
|
|
@@ -7,12 +7,14 @@ export interface GeminiContentFunctionCallPart {
|
|
|
7
7
|
functionCall: {
|
|
8
8
|
name: string;
|
|
9
9
|
args?: Record<string, unknown>;
|
|
10
|
+
id?: string;
|
|
10
11
|
[key: string]: unknown;
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
export interface GeminiContentFunctionResponsePart {
|
|
14
15
|
functionResponse: {
|
|
15
16
|
name?: string;
|
|
17
|
+
id?: string;
|
|
16
18
|
response?: unknown;
|
|
17
19
|
[key: string]: unknown;
|
|
18
20
|
};
|
|
@@ -23,7 +25,24 @@ export interface GeminiContentInlineDataPart {
|
|
|
23
25
|
data: string;
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
|
-
export
|
|
28
|
+
export interface GeminiContentThoughtPart {
|
|
29
|
+
thought: string;
|
|
30
|
+
}
|
|
31
|
+
export interface GeminiContentExecutableCodePart {
|
|
32
|
+
executableCode: {
|
|
33
|
+
language?: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface GeminiContentCodeExecutionResultPart {
|
|
39
|
+
codeExecutionResult: {
|
|
40
|
+
outcome?: string;
|
|
41
|
+
output?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export type GeminiContentPart = GeminiContentTextPart | GeminiContentFunctionCallPart | GeminiContentFunctionResponsePart | GeminiContentInlineDataPart | GeminiContentThoughtPart | GeminiContentExecutableCodePart | GeminiContentCodeExecutionResultPart | Record<string, unknown>;
|
|
27
46
|
export interface GeminiCandidate {
|
|
28
47
|
content?: {
|
|
29
48
|
role?: string;
|
|
@@ -23,7 +23,7 @@ export const DEFAULT_RESPONSES_CONVERSION_CONFIG = {
|
|
|
23
23
|
defaultChunkSize: 12,
|
|
24
24
|
defaultDelayMs: 8,
|
|
25
25
|
reasoningChunkSize: 24,
|
|
26
|
-
textChunkSize:
|
|
26
|
+
textChunkSize: 128,
|
|
27
27
|
functionCallChunkSize: 24,
|
|
28
28
|
enableEventValidation: true,
|
|
29
29
|
enableSequenceValidation: true,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface VirtualRouterHitEvent {
|
|
2
|
+
requestId: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
entryEndpoint: string;
|
|
5
|
+
routeName: string;
|
|
6
|
+
pool: string;
|
|
7
|
+
providerKey: string;
|
|
8
|
+
runtimeKey?: string;
|
|
9
|
+
providerType?: string;
|
|
10
|
+
modelId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProviderUsageEvent {
|
|
13
|
+
requestId: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
providerKey: string;
|
|
16
|
+
runtimeKey?: string;
|
|
17
|
+
providerType: string;
|
|
18
|
+
modelId?: string;
|
|
19
|
+
routeName?: string;
|
|
20
|
+
entryEndpoint?: string;
|
|
21
|
+
success: boolean;
|
|
22
|
+
latencyMs: number;
|
|
23
|
+
promptTokens?: number;
|
|
24
|
+
completionTokens?: number;
|
|
25
|
+
totalTokens?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RouterStatsBucket {
|
|
28
|
+
requestCount: number;
|
|
29
|
+
poolHitCount: Record<string, number>;
|
|
30
|
+
routeHitCount: Record<string, number>;
|
|
31
|
+
providerHitCount: Record<string, number>;
|
|
32
|
+
}
|
|
33
|
+
export interface RouterStatsSnapshot {
|
|
34
|
+
global: RouterStatsBucket;
|
|
35
|
+
byEntryEndpoint: Record<string, RouterStatsBucket>;
|
|
36
|
+
}
|
|
37
|
+
export interface ProviderStatsBucket {
|
|
38
|
+
requestCount: number;
|
|
39
|
+
successCount: number;
|
|
40
|
+
errorCount: number;
|
|
41
|
+
latencySumMs: number;
|
|
42
|
+
minLatencyMs: number;
|
|
43
|
+
maxLatencyMs: number;
|
|
44
|
+
usage: {
|
|
45
|
+
promptTokens: number;
|
|
46
|
+
completionTokens: number;
|
|
47
|
+
totalTokens: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface ProviderStatsSnapshot {
|
|
51
|
+
global: ProviderStatsBucket;
|
|
52
|
+
byProviderKey: Record<string, ProviderStatsBucket>;
|
|
53
|
+
byRoute: Record<string, ProviderStatsBucket>;
|
|
54
|
+
byEntryEndpoint: Record<string, ProviderStatsBucket>;
|
|
55
|
+
}
|
|
56
|
+
export interface StatsSnapshot {
|
|
57
|
+
router: RouterStatsSnapshot;
|
|
58
|
+
providers: ProviderStatsSnapshot;
|
|
59
|
+
}
|
|
60
|
+
export interface StatsCenterOptions {
|
|
61
|
+
enable?: boolean;
|
|
62
|
+
autoPrintOnExit?: boolean;
|
|
63
|
+
persistPath?: string | null;
|
|
64
|
+
}
|
|
65
|
+
export interface StatsCenter {
|
|
66
|
+
recordVirtualRouterHit(ev: VirtualRouterHitEvent): void;
|
|
67
|
+
recordProviderUsage(ev: ProviderUsageEvent): void;
|
|
68
|
+
getSnapshot(): StatsSnapshot;
|
|
69
|
+
flushToDisk(): Promise<void>;
|
|
70
|
+
reset(): void;
|
|
71
|
+
}
|
|
72
|
+
export declare function initStatsCenter(options?: StatsCenterOptions): StatsCenter;
|
|
73
|
+
export declare function getStatsCenter(): StatsCenter;
|