@jsonstudio/llms 0.6.215 → 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 +83 -1
- 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 +9 -1
- 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/types/standardized.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.js +49 -3
- package/dist/conversion/shared/tool-mapping.js +25 -2
- 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 +7 -2
- package/dist/router/virtual-router/engine.js +161 -82
- 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/package.json +1 -1
|
@@ -48,7 +48,7 @@ export class VirtualRouterEngine {
|
|
|
48
48
|
timestamp: Date.now(),
|
|
49
49
|
entryEndpoint: metadata.entryEndpoint || '/v1/chat/completions',
|
|
50
50
|
routeName: selection.routeUsed,
|
|
51
|
-
pool: selection.routeUsed,
|
|
51
|
+
pool: selection.poolId || selection.routeUsed,
|
|
52
52
|
providerKey: selection.providerKey,
|
|
53
53
|
modelId: target.modelId || undefined
|
|
54
54
|
});
|
|
@@ -57,20 +57,21 @@ export class VirtualRouterEngine {
|
|
|
57
57
|
// stats must never break routing
|
|
58
58
|
}
|
|
59
59
|
const hitReason = this.buildHitReason(selection.routeUsed, selection.providerKey, classification, features);
|
|
60
|
-
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);
|
|
61
61
|
if (formatted) {
|
|
62
62
|
this.debug?.log?.(formatted);
|
|
63
63
|
}
|
|
64
64
|
else {
|
|
65
65
|
this.debug?.log?.('[virtual-router-hit]', selection.routeUsed, selection.providerKey, target.modelId || '', hitReason ? `reason=${hitReason}` : '');
|
|
66
66
|
}
|
|
67
|
-
const didFallback = selection.routeUsed !== routeName
|
|
67
|
+
const didFallback = selection.routeUsed !== routeName;
|
|
68
68
|
return {
|
|
69
69
|
target,
|
|
70
70
|
decision: {
|
|
71
71
|
routeName: selection.routeUsed,
|
|
72
72
|
providerKey: selection.providerKey,
|
|
73
73
|
pool: selection.pool,
|
|
74
|
+
poolId: selection.poolId,
|
|
74
75
|
confidence: classification.confidence,
|
|
75
76
|
reasoning: classification.reasoning,
|
|
76
77
|
fallback: didFallback
|
|
@@ -79,6 +80,7 @@ export class VirtualRouterEngine {
|
|
|
79
80
|
routeName: selection.routeUsed,
|
|
80
81
|
providerKey: selection.providerKey,
|
|
81
82
|
pool: selection.pool,
|
|
83
|
+
poolId: selection.poolId,
|
|
82
84
|
reasoning: classification.reasoning,
|
|
83
85
|
fallback: didFallback,
|
|
84
86
|
confidence: classification.confidence
|
|
@@ -108,10 +110,10 @@ export class VirtualRouterEngine {
|
|
|
108
110
|
}
|
|
109
111
|
getStatus() {
|
|
110
112
|
const routes = {};
|
|
111
|
-
for (const [route,
|
|
113
|
+
for (const [route, pools] of Object.entries(this.routing)) {
|
|
112
114
|
const stats = this.routeStats.get(route) ?? { hits: 0 };
|
|
113
115
|
routes[route] = {
|
|
114
|
-
providers:
|
|
116
|
+
providers: this.flattenPoolTargets(pools),
|
|
115
117
|
hits: stats.hits,
|
|
116
118
|
lastUsedProvider: stats.lastProvider
|
|
117
119
|
};
|
|
@@ -128,21 +130,29 @@ export class VirtualRouterEngine {
|
|
|
128
130
|
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
129
131
|
throw new VirtualRouterError('providers configuration is required', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
130
132
|
}
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
133
|
+
const defaultPools = config.routing[DEFAULT_ROUTE];
|
|
134
|
+
if (!this.routeHasTargets(defaultPools)) {
|
|
133
135
|
throw new VirtualRouterError('default route must be configured with at least one provider', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
134
136
|
}
|
|
137
|
+
if (!this.hasPrimaryPool(defaultPools)) {
|
|
138
|
+
throw new VirtualRouterError('default route must define at least one non-backup pool', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
139
|
+
}
|
|
135
140
|
const providerKeys = new Set(Object.keys(config.providers));
|
|
136
|
-
for (const [routeName,
|
|
137
|
-
if (!
|
|
141
|
+
for (const [routeName, pools] of Object.entries(config.routing)) {
|
|
142
|
+
if (!this.routeHasTargets(pools)) {
|
|
138
143
|
if (routeName === DEFAULT_ROUTE) {
|
|
139
144
|
throw new VirtualRouterError('default route cannot be empty', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
140
145
|
}
|
|
141
146
|
continue;
|
|
142
147
|
}
|
|
143
|
-
for (const
|
|
144
|
-
if (!
|
|
145
|
-
|
|
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
|
+
}
|
|
146
156
|
}
|
|
147
157
|
}
|
|
148
158
|
}
|
|
@@ -152,8 +162,7 @@ export class VirtualRouterEngine {
|
|
|
152
162
|
const stickyKey = this.resolveStickyKey(metadata);
|
|
153
163
|
const attempted = [];
|
|
154
164
|
const visitedRoutes = new Set();
|
|
155
|
-
const
|
|
156
|
-
const routeQueue = this.initializeRouteQueue(candidates, fallbackRoute);
|
|
165
|
+
const routeQueue = this.initializeRouteQueue(candidates);
|
|
157
166
|
const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
|
|
158
167
|
? Math.max(0, features.estimatedTokens)
|
|
159
168
|
: 0;
|
|
@@ -162,33 +171,51 @@ export class VirtualRouterEngine {
|
|
|
162
171
|
if (visitedRoutes.has(routeName)) {
|
|
163
172
|
continue;
|
|
164
173
|
}
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
174
|
+
const routePools = this.routing[routeName];
|
|
175
|
+
if (!this.routeHasTargets(routePools)) {
|
|
167
176
|
visitedRoutes.add(routeName);
|
|
168
|
-
attempted.push(routeName);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const contextResult = this.contextAdvisor.classify(pool, estimatedTokens, (key) => this.providerRegistry.get(key));
|
|
172
|
-
if (this.maybeDeferToFallback(routeName, contextResult, routeQueue, visitedRoutes, fallbackRoute)) {
|
|
177
|
+
attempted.push(`${routeName}:empty`);
|
|
173
178
|
continue;
|
|
174
179
|
}
|
|
175
180
|
visitedRoutes.add(routeName);
|
|
176
|
-
const
|
|
177
|
-
for (const
|
|
178
|
-
const providerKey = this.
|
|
179
|
-
routeName,
|
|
180
|
-
candidates: candidatePool,
|
|
181
|
-
stickyKey,
|
|
182
|
-
availabilityCheck: (key) => this.healthManager.isAvailable(key)
|
|
183
|
-
});
|
|
181
|
+
const orderedPools = this.sortRoutePools(routePools);
|
|
182
|
+
for (const poolTier of orderedPools) {
|
|
183
|
+
const { providerKey, poolTargets, tierId, failureHint } = this.trySelectFromTier(routeName, poolTier, stickyKey, estimatedTokens);
|
|
184
184
|
if (providerKey) {
|
|
185
|
-
return { providerKey, routeUsed: routeName, pool };
|
|
185
|
+
return { providerKey, routeUsed: routeName, pool: poolTargets, poolId: tierId };
|
|
186
|
+
}
|
|
187
|
+
if (failureHint) {
|
|
188
|
+
attempted.push(failureHint);
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
|
-
attempted.push(this.describeAttempt(routeName, contextResult));
|
|
189
191
|
}
|
|
190
192
|
throw new VirtualRouterError(`All providers unavailable for route ${requestedRoute}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { routeName: requestedRoute, attempted });
|
|
191
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
|
+
}
|
|
192
219
|
incrementRouteStat(routeName, providerKey) {
|
|
193
220
|
if (!this.routeStats.has(routeName)) {
|
|
194
221
|
this.routeStats.set(routeName, { hits: 0, lastProvider: providerKey });
|
|
@@ -201,63 +228,34 @@ export class VirtualRouterEngine {
|
|
|
201
228
|
providerHealthConfig() {
|
|
202
229
|
return this.healthManager.getConfig();
|
|
203
230
|
}
|
|
204
|
-
initializeRouteQueue(candidates
|
|
205
|
-
|
|
206
|
-
if (fallbackRoute && !queue.includes(fallbackRoute)) {
|
|
207
|
-
queue.push(fallbackRoute);
|
|
208
|
-
}
|
|
209
|
-
return queue;
|
|
210
|
-
}
|
|
211
|
-
resolveFallbackRoute() {
|
|
212
|
-
const candidate = this.contextRouting?.fallbackRoute;
|
|
213
|
-
if (!candidate) {
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
const pool = this.routing[candidate];
|
|
217
|
-
if (!Array.isArray(pool) || pool.length === 0) {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
return candidate;
|
|
221
|
-
}
|
|
222
|
-
maybeDeferToFallback(routeName, contextResult, queue, visited, fallbackRoute) {
|
|
223
|
-
if (!fallbackRoute || fallbackRoute === routeName || visited.has(fallbackRoute)) {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
if (!this.contextAdvisor.prefersFallback(contextResult)) {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
const fallbackPool = this.routing[fallbackRoute];
|
|
230
|
-
if (!Array.isArray(fallbackPool) || fallbackPool.length === 0) {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
queue.unshift(routeName);
|
|
234
|
-
queue.unshift(fallbackRoute);
|
|
235
|
-
return true;
|
|
231
|
+
initializeRouteQueue(candidates) {
|
|
232
|
+
return Array.from(new Set(candidates));
|
|
236
233
|
}
|
|
237
234
|
buildContextCandidatePools(result) {
|
|
238
235
|
const ordered = [];
|
|
239
236
|
if (result.safe.length) {
|
|
240
237
|
ordered.push(result.safe);
|
|
238
|
+
// 如果存在安全候选,直接放弃当前处于警戒阈值的模型
|
|
239
|
+
return ordered;
|
|
241
240
|
}
|
|
242
241
|
if (result.risky.length) {
|
|
243
242
|
ordered.push(result.risky);
|
|
244
243
|
}
|
|
245
|
-
|
|
246
|
-
ordered.push(result.overflow);
|
|
247
|
-
}
|
|
244
|
+
// ratio >= 1 视为上下文溢出,直接标记为不可用
|
|
248
245
|
return ordered;
|
|
249
246
|
}
|
|
250
|
-
describeAttempt(routeName, result) {
|
|
247
|
+
describeAttempt(routeName, poolId, result) {
|
|
248
|
+
const prefix = poolId ? `${routeName}:${poolId}` : routeName;
|
|
251
249
|
if (result.safe.length > 0) {
|
|
252
|
-
return `${
|
|
250
|
+
return `${prefix}:health`;
|
|
253
251
|
}
|
|
254
252
|
if (result.risky.length > 0) {
|
|
255
|
-
return `${
|
|
253
|
+
return `${prefix}:context_risky`;
|
|
256
254
|
}
|
|
257
255
|
if (result.overflow.length > 0) {
|
|
258
|
-
return `${
|
|
256
|
+
return `${prefix}:max_context_window`;
|
|
259
257
|
}
|
|
260
|
-
return
|
|
258
|
+
return prefix;
|
|
261
259
|
}
|
|
262
260
|
resolveStickyKey(metadata) {
|
|
263
261
|
const resume = metadata.responsesResume;
|
|
@@ -363,13 +361,8 @@ export class VirtualRouterEngine {
|
|
|
363
361
|
if (!deduped.includes(DEFAULT_ROUTE)) {
|
|
364
362
|
deduped.push(DEFAULT_ROUTE);
|
|
365
363
|
}
|
|
366
|
-
const filtered = deduped.filter((routeName) =>
|
|
367
|
-
|
|
368
|
-
return Array.isArray(pool) && pool.length > 0;
|
|
369
|
-
});
|
|
370
|
-
if (!filtered.includes(DEFAULT_ROUTE) &&
|
|
371
|
-
Array.isArray(this.routing[DEFAULT_ROUTE]) &&
|
|
372
|
-
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])) {
|
|
373
366
|
filtered.push(DEFAULT_ROUTE);
|
|
374
367
|
}
|
|
375
368
|
return filtered.length ? filtered : [DEFAULT_ROUTE];
|
|
@@ -381,6 +374,52 @@ export class VirtualRouterEngine {
|
|
|
381
374
|
const idx = ROUTE_PRIORITY.indexOf(routeName);
|
|
382
375
|
return idx >= 0 ? idx : ROUTE_PRIORITY.length;
|
|
383
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
|
+
}
|
|
384
423
|
buildHitReason(routeUsed, providerKey, classification, features) {
|
|
385
424
|
const reasoning = classification.reasoning || '';
|
|
386
425
|
const primary = reasoning.split('|')[0] || '';
|
|
@@ -422,18 +461,31 @@ export class VirtualRouterEngine {
|
|
|
422
461
|
}
|
|
423
462
|
return `${baseLabel}(${normalizedDetail})`;
|
|
424
463
|
}
|
|
425
|
-
formatVirtualRouterHit(routeName, providerKey, modelId, hitReason) {
|
|
464
|
+
formatVirtualRouterHit(routeName, poolId, providerKey, modelId, hitReason) {
|
|
426
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}`;
|
|
427
472
|
const prefixColor = '\x1b[38;5;208m';
|
|
428
473
|
const reset = '\x1b[0m';
|
|
474
|
+
const timeColor = '\x1b[90m'; // 灰色
|
|
429
475
|
const routeColor = this.resolveRouteColor(routeName);
|
|
430
476
|
const prefix = `${prefixColor}[virtual-router-hit]${reset}`;
|
|
431
|
-
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 : ''}`;
|
|
432
481
|
const reasonLabel = hitReason ? ` reason=${hitReason}` : '';
|
|
433
|
-
return `${prefix} ${routeColor}${targetLabel}${reasonLabel}${reset}`;
|
|
482
|
+
return `${prefix} ${timeLabel} ${routeColor}${targetLabel}${reasonLabel}${reset}`;
|
|
434
483
|
}
|
|
435
484
|
catch {
|
|
436
|
-
|
|
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}` : ''}`;
|
|
437
489
|
}
|
|
438
490
|
}
|
|
439
491
|
resolveRouteColor(routeName) {
|
|
@@ -473,4 +525,31 @@ export class VirtualRouterEngine {
|
|
|
473
525
|
}
|
|
474
526
|
return `${ratio.toFixed(2)}/${Math.round(limit)}`;
|
|
475
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
|
+
}
|
|
476
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,
|