@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.
Files changed (26) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +83 -1
  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 +9 -1
  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/types/standardized.d.ts +1 -0
  14. package/dist/conversion/responses/responses-openai-bridge.js +49 -3
  15. package/dist/conversion/shared/tool-mapping.js +25 -2
  16. package/dist/router/virtual-router/bootstrap.js +273 -40
  17. package/dist/router/virtual-router/context-advisor.d.ts +0 -2
  18. package/dist/router/virtual-router/context-advisor.js +0 -12
  19. package/dist/router/virtual-router/engine.d.ts +7 -2
  20. package/dist/router/virtual-router/engine.js +161 -82
  21. package/dist/router/virtual-router/types.d.ts +21 -2
  22. package/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  23. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  24. package/dist/sse/types/gemini-types.d.ts +20 -1
  25. package/dist/sse/types/responses-types.js +1 -1
  26. 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 || classification.fallback;
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, pool] of Object.entries(this.routing)) {
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: [...pool],
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 defaultPool = config.routing[DEFAULT_ROUTE];
132
- if (!Array.isArray(defaultPool) || defaultPool.length === 0) {
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, pool] of Object.entries(config.routing)) {
137
- if (!Array.isArray(pool) || !pool.length) {
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 providerKey of pool) {
144
- if (!providerKeys.has(providerKey)) {
145
- 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
+ }
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 fallbackRoute = this.resolveFallbackRoute();
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 pool = this.routing[routeName];
166
- if (!Array.isArray(pool) || pool.length === 0) {
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 prioritizedPools = this.buildContextCandidatePools(contextResult);
177
- for (const candidatePool of prioritizedPools) {
178
- const providerKey = this.loadBalancer.select({
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, fallbackRoute) {
205
- const queue = Array.from(new Set(candidates));
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
- if (result.overflow.length && this.contextAdvisor.allowsOverflow()) {
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 `${routeName}:health`;
250
+ return `${prefix}:health`;
253
251
  }
254
252
  if (result.risky.length > 0) {
255
- return `${routeName}:context_risky`;
253
+ return `${prefix}:context_risky`;
256
254
  }
257
255
  if (result.overflow.length > 0) {
258
- return `${routeName}:context_overflow`;
256
+ return `${prefix}:max_context_window`;
259
257
  }
260
- return routeName;
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
- const pool = this.routing[routeName];
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 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 : ''}`;
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
- 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}` : ''}`;
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 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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.215",
3
+ "version": "0.6.230",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",