@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.
Files changed (32) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +128 -4
  2. package/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
  3. package/dist/conversion/compat/actions/glm-web-search.js +66 -0
  4. package/dist/conversion/compat/profiles/chat-glm.json +4 -1
  5. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  6. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +11 -3
  8. package/dist/conversion/hub/process/chat-process.js +131 -1
  9. package/dist/conversion/hub/response/provider-response.d.ts +22 -0
  10. package/dist/conversion/hub/response/provider-response.js +12 -1
  11. package/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
  12. package/dist/conversion/hub/response/server-side-tools.js +326 -0
  13. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +118 -11
  14. package/dist/conversion/hub/types/standardized.d.ts +1 -0
  15. package/dist/conversion/responses/responses-openai-bridge.js +49 -3
  16. package/dist/conversion/shared/snapshot-utils.js +17 -47
  17. package/dist/conversion/shared/tool-mapping.js +25 -2
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/router/virtual-router/bootstrap.js +273 -40
  21. package/dist/router/virtual-router/context-advisor.d.ts +0 -2
  22. package/dist/router/virtual-router/context-advisor.js +0 -12
  23. package/dist/router/virtual-router/engine.d.ts +8 -2
  24. package/dist/router/virtual-router/engine.js +176 -81
  25. package/dist/router/virtual-router/types.d.ts +21 -2
  26. package/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  27. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  28. package/dist/sse/types/gemini-types.d.ts +20 -1
  29. package/dist/sse/types/responses-types.js +1 -1
  30. package/dist/telemetry/stats-center.d.ts +73 -0
  31. package/dist/telemetry/stats-center.js +280 -0
  32. 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 || classification.fallback;
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, pool] of Object.entries(this.routing)) {
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: [...pool],
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 defaultPool = config.routing[DEFAULT_ROUTE];
116
- if (!Array.isArray(defaultPool) || defaultPool.length === 0) {
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, pool] of Object.entries(config.routing)) {
121
- if (!Array.isArray(pool) || !pool.length) {
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 providerKey of pool) {
128
- if (!providerKeys.has(providerKey)) {
129
- throw new VirtualRouterError(`Route ${routeName} references unknown provider ${providerKey}`, VirtualRouterErrorCode.CONFIG_ERROR);
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 fallbackRoute = this.resolveFallbackRoute();
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 pool = this.routing[routeName];
150
- if (!Array.isArray(pool) || pool.length === 0) {
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 prioritizedPools = this.buildContextCandidatePools(contextResult);
161
- for (const candidatePool of prioritizedPools) {
162
- const providerKey = this.loadBalancer.select({
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, fallbackRoute) {
189
- const queue = Array.from(new Set(candidates));
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
- if (result.overflow.length && this.contextAdvisor.allowsOverflow()) {
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 `${routeName}:health`;
250
+ return `${prefix}:health`;
237
251
  }
238
252
  if (result.risky.length > 0) {
239
- return `${routeName}:context_risky`;
253
+ return `${prefix}:context_risky`;
240
254
  }
241
255
  if (result.overflow.length > 0) {
242
- return `${routeName}:context_overflow`;
256
+ return `${prefix}:max_context_window`;
243
257
  }
244
- return routeName;
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
- const pool = this.routing[routeName];
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 targetLabel = `${routeName} -> ${providerKey}${modelId ? '.' + modelId : ''}`;
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
- return `[virtual-router-hit] ${routeName} -> ${providerKey}${modelId ? '.' + modelId : ''}${hitReason ? ` reason=${hitReason}` : ''}`;
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 type RoutingPools = Record<string, string[]>;
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
- return Math.max(1, config.chunkSize || DEFAULT_RESPONSES_EVENT_GENERATOR_CONFIG.chunkSize);
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, getChunkSize(config), cloneRegex(TEXT_CHUNK_BOUNDARY));
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
- chunkSize: 12,
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
- accumulator.get(candidateIndex).parts.push(normalizedPart);
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 type GeminiContentPart = GeminiContentTextPart | GeminiContentFunctionCallPart | GeminiContentFunctionResponsePart | GeminiContentInlineDataPart | Record<string, unknown>;
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: 12,
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;