@jsonstudio/llms 0.6.473 → 0.6.568

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 (82) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  5. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  6. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  7. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
  9. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  10. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  11. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  12. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  13. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  14. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  16. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
  18. package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
  19. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  20. package/dist/conversion/hub/process/chat-process.js +107 -26
  21. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  22. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
  23. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  24. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  25. package/dist/conversion/hub/tool-session-compat.js +299 -0
  26. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  27. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  28. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  29. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  30. package/dist/conversion/shared/args-mapping.js +11 -3
  31. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  32. package/dist/conversion/shared/responses-output-builder.js +47 -88
  33. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  34. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  35. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  36. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  37. package/dist/conversion/shared/tool-governor.js +75 -4
  38. package/dist/conversion/shared/tool-harvester.js +43 -12
  39. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  40. package/dist/conversion/shared/tool-mapping.js +33 -13
  41. package/dist/filters/index.d.ts +1 -0
  42. package/dist/filters/index.js +1 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  44. package/dist/filters/special/request-tools-normalize.js +14 -23
  45. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  46. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  47. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  48. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  49. package/dist/guidance/index.js +70 -27
  50. package/dist/router/virtual-router/bootstrap.js +10 -5
  51. package/dist/router/virtual-router/classifier.js +9 -4
  52. package/dist/router/virtual-router/engine-health.d.ts +22 -0
  53. package/dist/router/virtual-router/engine-health.js +423 -0
  54. package/dist/router/virtual-router/engine-logging.d.ts +20 -0
  55. package/dist/router/virtual-router/engine-logging.js +197 -0
  56. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  57. package/dist/router/virtual-router/engine-selection.js +649 -0
  58. package/dist/router/virtual-router/engine.d.ts +21 -14
  59. package/dist/router/virtual-router/engine.js +200 -523
  60. package/dist/router/virtual-router/message-utils.js +22 -0
  61. package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
  62. package/dist/router/virtual-router/routing-instructions.js +137 -3
  63. package/dist/router/virtual-router/tool-signals.js +57 -11
  64. package/dist/router/virtual-router/types.d.ts +30 -0
  65. package/dist/router/virtual-router/types.js +1 -1
  66. package/dist/servertool/engine.js +3 -0
  67. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  68. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  69. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  70. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  71. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  72. package/dist/servertool/handlers/stop-message-auto.js +204 -0
  73. package/dist/servertool/handlers/vision.js +105 -7
  74. package/dist/servertool/server-side-tools.d.ts +3 -0
  75. package/dist/servertool/server-side-tools.js +29 -0
  76. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  77. package/dist/tools/apply-patch-structured.d.ts +20 -0
  78. package/dist/tools/apply-patch-structured.js +239 -0
  79. package/dist/tools/tool-description-utils.d.ts +5 -0
  80. package/dist/tools/tool-description-utils.js +50 -0
  81. package/dist/tools/tool-registry.js +14 -5
  82. package/package.json +2 -2
@@ -0,0 +1,423 @@
1
+ const SERIES_COOLDOWN_DETAIL_KEY = 'virtualRouterSeriesCooldown';
2
+ const QUOTA_RECOVERY_DETAIL_KEY = 'virtualRouterQuotaRecovery';
3
+ const QUOTA_DEPLETED_DETAIL_KEY = 'virtualRouterQuotaDepleted';
4
+ function parseDurationToMs(value) {
5
+ if (!value || typeof value !== 'string') {
6
+ return null;
7
+ }
8
+ const pattern = /(\d+(?:\.\d+)?)(ms|s|m|h)/gi;
9
+ let totalMs = 0;
10
+ let matched = false;
11
+ let match;
12
+ while ((match = pattern.exec(value)) !== null) {
13
+ matched = true;
14
+ const amount = Number.parseFloat(match[1]);
15
+ if (!Number.isFinite(amount)) {
16
+ continue;
17
+ }
18
+ const unit = match[2].toLowerCase();
19
+ if (unit === 'ms') {
20
+ totalMs += amount;
21
+ }
22
+ else if (unit === 'h') {
23
+ totalMs += amount * 3_600_000;
24
+ }
25
+ else if (unit === 'm') {
26
+ totalMs += amount * 60_000;
27
+ }
28
+ else if (unit === 's') {
29
+ totalMs += amount * 1_000;
30
+ }
31
+ }
32
+ if (!matched) {
33
+ const seconds = Number.parseFloat(value);
34
+ if (Number.isFinite(seconds)) {
35
+ totalMs = seconds * 1_000;
36
+ matched = true;
37
+ }
38
+ }
39
+ if (!matched || totalMs <= 0) {
40
+ return null;
41
+ }
42
+ return Math.round(totalMs);
43
+ }
44
+ function readEnvSchedule(name, fallback) {
45
+ const raw = (process.env[name] || '').trim();
46
+ if (!raw) {
47
+ return fallback;
48
+ }
49
+ const parts = raw.split(',').map((token) => token.trim()).filter(Boolean);
50
+ const parsed = [];
51
+ for (const part of parts) {
52
+ const ms = parseDurationToMs(part);
53
+ if (ms && ms > 0) {
54
+ parsed.push(ms);
55
+ }
56
+ }
57
+ return parsed.length ? parsed : fallback;
58
+ }
59
+ function readEnvDuration(name, fallbackMs) {
60
+ const raw = (process.env[name] || '').trim();
61
+ if (!raw) {
62
+ return fallbackMs;
63
+ }
64
+ const ms = parseDurationToMs(raw);
65
+ return ms && ms > 0 ? ms : fallbackMs;
66
+ }
67
+ /**
68
+ * 对没有 quotaResetDelay 的 429 错误,在 VirtualRouter 内部维护一个简单的阶梯退避策略:
69
+ * - 默认:第 1 次 5 分钟,第 2 次 1 小时,第 3 次 6 小时,第 4 次及以上 24 小时封顶;
70
+ * - 可通过环境变量 ROUTECODEX_RL_SCHEDULE / RCC_RL_SCHEDULE 调整(例如 "5m,1h,6h,24h")。
71
+ *
72
+ * 这里的“次数”针对 providerKey 计数,并带有简单的时间窗口:若距离上次 429 超过 24 小时,则重置计数。
73
+ * 该状态仅用于路由决策,不反映在 healthConfig 上,使 Host 与 VirtualRouter 对 429 处理职责清晰分层。
74
+ */
75
+ const NO_QUOTA_RATE_LIMIT_SCHEDULE_MS = readEnvSchedule('ROUTECODEX_RL_SCHEDULE', [
76
+ 5 * 60_000,
77
+ 60 * 60_000,
78
+ 6 * 60 * 60_000,
79
+ 24 * 60 * 60_000
80
+ ]);
81
+ const rateLimitBackoffByProvider = new Map();
82
+ const RATE_LIMIT_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_RL_RESET_WINDOW', 24 * 60 * 60_000);
83
+ function computeRateLimitCooldownMsForProvider(providerKey, now) {
84
+ const prev = rateLimitBackoffByProvider.get(providerKey);
85
+ let nextCount = 1;
86
+ if (prev) {
87
+ const elapsed = now - prev.lastAt;
88
+ if (Number.isFinite(elapsed) && elapsed >= 0 && elapsed < RATE_LIMIT_RESET_WINDOW_MS) {
89
+ nextCount = prev.count + 1;
90
+ }
91
+ }
92
+ const idx = Math.min(nextCount - 1, NO_QUOTA_RATE_LIMIT_SCHEDULE_MS.length - 1);
93
+ const ttl = NO_QUOTA_RATE_LIMIT_SCHEDULE_MS[idx];
94
+ rateLimitBackoffByProvider.set(providerKey, { count: nextCount, lastAt: now });
95
+ return ttl;
96
+ }
97
+ export function resetRateLimitBackoffForProvider(providerKey) {
98
+ if (!providerKey) {
99
+ return;
100
+ }
101
+ rateLimitBackoffByProvider.delete(providerKey);
102
+ }
103
+ export function handleProviderFailureImpl(event, healthManager, healthConfig, markProviderCooldown) {
104
+ if (!event || !event.providerKey) {
105
+ return;
106
+ }
107
+ if (event.affectsHealth === false) {
108
+ return;
109
+ }
110
+ if (event.fatal) {
111
+ healthManager.tripProvider(event.providerKey, event.reason, event.cooldownOverrideMs);
112
+ }
113
+ else if (event.reason === 'rate_limit' && event.statusCode === 429) {
114
+ // 对非致命的 429 错误:
115
+ // - 若 ProviderErrorEvent 已携带显式 cooldownOverrideMs(例如来自 quotaResetDelay),则直接使用;
116
+ // - 否则针对该 providerKey 启用阶梯退避策略(5min → 1h → 6h → 24h),
117
+ // 在冷却期内从路由池中移除该 alias,避免持续命中上游。
118
+ const providerKey = event.providerKey;
119
+ let ttl = event.cooldownOverrideMs;
120
+ if (!ttl || !Number.isFinite(ttl) || ttl <= 0) {
121
+ ttl = computeRateLimitCooldownMsForProvider(providerKey, Date.now());
122
+ }
123
+ healthManager.cooldownProvider(providerKey, event.reason, ttl);
124
+ markProviderCooldown(providerKey, ttl);
125
+ }
126
+ else {
127
+ healthManager.recordFailure(event.providerKey, event.reason);
128
+ }
129
+ }
130
+ export function mapProviderErrorImpl(event, healthConfig) {
131
+ if (!event || !event.runtime) {
132
+ return null;
133
+ }
134
+ const runtime = event.runtime;
135
+ const providerKey = runtime.providerKey ||
136
+ (runtime.target && typeof runtime.target === 'object'
137
+ ? runtime.target.providerKey
138
+ : undefined);
139
+ if (!providerKey) {
140
+ return null;
141
+ }
142
+ const routeName = runtime.routeName;
143
+ const statusCode = event.status;
144
+ const code = event.code?.toUpperCase() ?? 'ERR_UNKNOWN';
145
+ const stage = event.stage?.toLowerCase() ?? 'unknown';
146
+ const recoverable = event.recoverable === true;
147
+ const providerFamily = runtime.providerFamily &&
148
+ typeof runtime.providerFamily === 'string'
149
+ ? runtime.providerFamily
150
+ : undefined;
151
+ const providerId = runtime.providerId &&
152
+ typeof runtime.providerId === 'string'
153
+ ? runtime.providerId
154
+ : undefined;
155
+ const providerTag = (providerFamily || providerId || '').toLowerCase();
156
+ const isOAuthAuth406 = statusCode === 406 && (providerTag === 'iflow' || providerTag === 'qwen');
157
+ let fatal = !recoverable;
158
+ let reason = deriveReason(code, stage, statusCode);
159
+ let cooldownOverrideMs;
160
+ if (statusCode === 401 || statusCode === 402 || statusCode === 403 || code.includes('AUTH') || isOAuthAuth406) {
161
+ fatal = true;
162
+ cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
163
+ reason = 'auth';
164
+ }
165
+ else if (statusCode === 429 && !recoverable) {
166
+ fatal = true;
167
+ cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
168
+ reason = 'rate_limit';
169
+ }
170
+ else if (statusCode && statusCode >= 500) {
171
+ fatal = true;
172
+ cooldownOverrideMs = Math.max(5 * 60_000, healthConfig.fatalCooldownMs ?? 5 * 60_000);
173
+ reason = 'upstream_error';
174
+ }
175
+ else if (stage.includes('compat')) {
176
+ fatal = true;
177
+ cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
178
+ reason = 'compatibility';
179
+ }
180
+ return {
181
+ providerKey,
182
+ routeName,
183
+ reason,
184
+ fatal,
185
+ statusCode,
186
+ errorCode: code,
187
+ retryable: recoverable,
188
+ affectsHealth: event.affectsHealth !== false,
189
+ cooldownOverrideMs,
190
+ metadata: {
191
+ ...event.runtime,
192
+ stage,
193
+ eventCode: code,
194
+ originalMessage: event.message,
195
+ statusCode
196
+ }
197
+ };
198
+ }
199
+ export function applySeriesCooldownImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
200
+ const seriesDetail = extractSeriesCooldownDetail(event);
201
+ if (!seriesDetail) {
202
+ return;
203
+ }
204
+ const targetKeys = resolveSeriesCooldownTargets(seriesDetail, event, providerRegistry);
205
+ if (targetKeys.length === 0) {
206
+ debug?.log?.('[virtual-router] series cooldown skipped: no targets', {
207
+ providerId: seriesDetail.providerId,
208
+ providerKey: seriesDetail.providerKey,
209
+ series: seriesDetail.series
210
+ });
211
+ return;
212
+ }
213
+ const affected = [];
214
+ for (const providerKey of targetKeys) {
215
+ try {
216
+ const profile = providerRegistry.get(providerKey);
217
+ const modelSeries = resolveModelSeries(profile.modelId);
218
+ if (modelSeries !== seriesDetail.series) {
219
+ continue;
220
+ }
221
+ healthManager.tripProvider(providerKey, 'rate_limit', seriesDetail.cooldownMs);
222
+ markProviderCooldown(providerKey, seriesDetail.cooldownMs);
223
+ affected.push(providerKey);
224
+ }
225
+ catch {
226
+ // ignore lookup failures; invalid keys may show up if config drifted
227
+ }
228
+ }
229
+ if (affected.length) {
230
+ debug?.log?.('[virtual-router] series cooldown', {
231
+ providerId: seriesDetail.providerId,
232
+ providerKey: seriesDetail.providerKey,
233
+ series: seriesDetail.series,
234
+ cooldownMs: seriesDetail.cooldownMs,
235
+ affected
236
+ });
237
+ }
238
+ }
239
+ function extractQuotaRecoveryDetail(event) {
240
+ if (!event || !event.details || typeof event.details !== 'object') {
241
+ return null;
242
+ }
243
+ const raw = event.details[QUOTA_RECOVERY_DETAIL_KEY];
244
+ if (!raw || typeof raw !== 'object') {
245
+ return null;
246
+ }
247
+ const record = raw;
248
+ const providerKeyRaw = record.providerKey;
249
+ if (typeof providerKeyRaw !== 'string' || !providerKeyRaw.trim()) {
250
+ return null;
251
+ }
252
+ const reason = typeof record.reason === 'string' && record.reason.trim()
253
+ ? record.reason.trim()
254
+ : undefined;
255
+ return {
256
+ providerKey: providerKeyRaw.trim(),
257
+ reason
258
+ };
259
+ }
260
+ /**
261
+ * 处理来自 Host 侧的配额恢复事件:
262
+ * - 清除指定 providerKey 在健康管理器中的熔断/冷却状态;
263
+ * - 清理对应的速率退避计数;
264
+ * - 调用调用方提供的 clearProviderCooldown 回调移除显式 cooldown TTL。
265
+ *
266
+ * 返回值表示是否已处理(true=已处理且后续应跳过常规错误映射逻辑)。
267
+ */
268
+ export function applyQuotaRecoveryImpl(event, healthManager, clearProviderCooldown, debug) {
269
+ const detail = extractQuotaRecoveryDetail(event);
270
+ if (!detail) {
271
+ return false;
272
+ }
273
+ const providerKey = detail.providerKey;
274
+ try {
275
+ healthManager.recordSuccess(providerKey);
276
+ resetRateLimitBackoffForProvider(providerKey);
277
+ clearProviderCooldown(providerKey);
278
+ debug?.log?.('[virtual-router] quota recovery', {
279
+ providerKey,
280
+ reason: detail.reason
281
+ });
282
+ }
283
+ catch {
284
+ // 恢复失败不得影响主路由流程
285
+ }
286
+ return true;
287
+ }
288
+ function extractQuotaDepletedDetail(event) {
289
+ if (!event || !event.details || typeof event.details !== 'object') {
290
+ return null;
291
+ }
292
+ const raw = event.details[QUOTA_DEPLETED_DETAIL_KEY];
293
+ if (!raw || typeof raw !== 'object') {
294
+ return null;
295
+ }
296
+ const record = raw;
297
+ const providerKeyRaw = record.providerKey;
298
+ if (typeof providerKeyRaw !== 'string' || !providerKeyRaw.trim()) {
299
+ return null;
300
+ }
301
+ const cooldownMs = typeof record.cooldownMs === 'number' && Number.isFinite(record.cooldownMs) && record.cooldownMs > 0
302
+ ? record.cooldownMs
303
+ : undefined;
304
+ const reason = typeof record.reason === 'string' && record.reason.trim()
305
+ ? record.reason.trim()
306
+ : undefined;
307
+ return {
308
+ providerKey: providerKeyRaw.trim(),
309
+ cooldownMs,
310
+ reason
311
+ };
312
+ }
313
+ export function applyQuotaDepletedImpl(event, healthManager, markProviderCooldown, debug) {
314
+ const detail = extractQuotaDepletedDetail(event);
315
+ if (!detail) {
316
+ return false;
317
+ }
318
+ const ttl = detail.cooldownMs;
319
+ try {
320
+ healthManager.cooldownProvider(detail.providerKey, 'rate_limit', ttl);
321
+ markProviderCooldown(detail.providerKey, ttl);
322
+ debug?.log?.('[virtual-router] quota depleted', {
323
+ providerKey: detail.providerKey,
324
+ cooldownMs: ttl,
325
+ reason: detail.reason
326
+ });
327
+ }
328
+ catch {
329
+ // ignore failures
330
+ }
331
+ return true;
332
+ }
333
+ function resolveSeriesCooldownTargets(detail, event, providerRegistry) {
334
+ const candidates = new Set();
335
+ const push = (key) => {
336
+ if (typeof key !== 'string') {
337
+ return;
338
+ }
339
+ const trimmed = key.trim();
340
+ if (!trimmed) {
341
+ return;
342
+ }
343
+ if (providerRegistry.has(trimmed)) {
344
+ candidates.add(trimmed);
345
+ }
346
+ };
347
+ push(detail.providerKey);
348
+ const runtimeKey = (event.runtime?.target && typeof event.runtime.target === 'object'
349
+ ? event.runtime.target.providerKey
350
+ : undefined) || event.runtime?.providerKey;
351
+ push(runtimeKey);
352
+ return Array.from(candidates);
353
+ }
354
+ function extractSeriesCooldownDetail(event) {
355
+ if (!event || !event.details || typeof event.details !== 'object') {
356
+ return null;
357
+ }
358
+ const raw = event.details[SERIES_COOLDOWN_DETAIL_KEY];
359
+ if (!raw || typeof raw !== 'object') {
360
+ return null;
361
+ }
362
+ const record = raw;
363
+ const providerIdRaw = record.providerId;
364
+ const seriesRaw = record.series;
365
+ const providerKeyRaw = record.providerKey;
366
+ const cooldownRaw = record.cooldownMs;
367
+ if (typeof providerIdRaw !== 'string' || !providerIdRaw.trim()) {
368
+ return null;
369
+ }
370
+ const normalizedSeries = typeof seriesRaw === 'string' ? seriesRaw.trim().toLowerCase() : '';
371
+ if (normalizedSeries !== 'gemini-pro' && normalizedSeries !== 'gemini-flash' && normalizedSeries !== 'claude') {
372
+ return null;
373
+ }
374
+ const cooldownMs = typeof cooldownRaw === 'number'
375
+ ? cooldownRaw
376
+ : typeof cooldownRaw === 'string'
377
+ ? Number.parseFloat(cooldownRaw)
378
+ : Number.NaN;
379
+ if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
380
+ return null;
381
+ }
382
+ return {
383
+ providerId: providerIdRaw.trim(),
384
+ ...(typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length
385
+ ? { providerKey: providerKeyRaw.trim() }
386
+ : {}),
387
+ series: normalizedSeries,
388
+ cooldownMs: Math.round(cooldownMs)
389
+ };
390
+ }
391
+ export function deriveReason(code, stage, statusCode) {
392
+ if (code.includes('RATE') || code.includes('429'))
393
+ return 'rate_limit';
394
+ if (code.includes('AUTH') || statusCode === 401 || statusCode === 403)
395
+ return 'auth';
396
+ if (stage.includes('compat'))
397
+ return 'compatibility';
398
+ if (code.includes('SSE'))
399
+ return 'sse';
400
+ if (code.includes('TIMEOUT') || statusCode === 408 || statusCode === 504)
401
+ return 'timeout';
402
+ if (statusCode && statusCode >= 500)
403
+ return 'upstream_error';
404
+ if (statusCode && statusCode >= 400)
405
+ return 'client_error';
406
+ return 'unknown';
407
+ }
408
+ function resolveModelSeries(modelId) {
409
+ if (!modelId) {
410
+ return 'default';
411
+ }
412
+ const lower = modelId.toLowerCase();
413
+ if (lower.includes('claude') || lower.includes('opus')) {
414
+ return 'claude';
415
+ }
416
+ if (lower.includes('flash')) {
417
+ return 'gemini-flash';
418
+ }
419
+ if (lower.includes('gemini') || lower.includes('pro')) {
420
+ return 'gemini-pro';
421
+ }
422
+ return 'default';
423
+ }
@@ -0,0 +1,20 @@
1
+ import { type ClassificationResult, type RoutingFeatures, type RoutingInstructionMode, type VirtualRouterContextRoutingConfig } from './types.js';
2
+ import { ProviderRegistry } from './provider-registry.js';
3
+ import type { RoutingInstructionState } from './routing-instructions.js';
4
+ type LoggingDeps = {
5
+ providerRegistry: ProviderRegistry;
6
+ contextRouting: VirtualRouterContextRoutingConfig | undefined;
7
+ };
8
+ export declare function formatStickyScope(scope?: string): string | undefined;
9
+ export declare function parseProviderKey(providerKey: string): {
10
+ providerId: string;
11
+ keyAlias?: string;
12
+ modelId?: string;
13
+ } | null;
14
+ export declare function describeTargetProvider(providerKey: string, fallbackModelId?: string): {
15
+ providerLabel: string;
16
+ resolvedModel?: string;
17
+ };
18
+ export declare function buildHitReason(routeUsed: string, providerKey: string, classification: ClassificationResult, features: RoutingFeatures, mode: RoutingInstructionMode | undefined, deps: LoggingDeps): string;
19
+ export declare function formatVirtualRouterHit(routeName: string, poolId: string | undefined, providerKey: string, modelId?: string, hitReason?: string, stickyScope?: string, routingState?: RoutingInstructionState): string;
20
+ export {};
@@ -0,0 +1,197 @@
1
+ import { DEFAULT_MODEL_CONTEXT_TOKENS, DEFAULT_ROUTE } from './types.js';
2
+ export function formatStickyScope(scope) {
3
+ if (!scope || scope.trim().length === 0) {
4
+ return undefined;
5
+ }
6
+ const normalized = scope.trim();
7
+ const maxLength = 20;
8
+ if (normalized.length <= maxLength) {
9
+ return normalized;
10
+ }
11
+ const delimiterIndex = normalized.indexOf(':');
12
+ const prefix = delimiterIndex > 0 ? normalized.slice(0, delimiterIndex + 1) : '';
13
+ const body = delimiterIndex > 0 ? normalized.slice(delimiterIndex + 1) : normalized;
14
+ if (body.length <= 8) {
15
+ return `${prefix}${body}`;
16
+ }
17
+ return `${prefix}${body.slice(0, 4)}…${body.slice(-4)}`;
18
+ }
19
+ export function parseProviderKey(providerKey) {
20
+ const trimmed = typeof providerKey === 'string' ? providerKey.trim() : '';
21
+ if (!trimmed) {
22
+ return null;
23
+ }
24
+ const parts = trimmed.split('.');
25
+ if (parts.length < 2) {
26
+ return { providerId: trimmed };
27
+ }
28
+ if (parts.length === 2) {
29
+ return { providerId: parts[0], modelId: parts[1] };
30
+ }
31
+ return {
32
+ providerId: parts[0],
33
+ keyAlias: parts[1],
34
+ modelId: parts.slice(2).join('.')
35
+ };
36
+ }
37
+ export function describeTargetProvider(providerKey, fallbackModelId) {
38
+ const parsed = parseProviderKey(providerKey);
39
+ if (!parsed) {
40
+ return { providerLabel: providerKey, resolvedModel: fallbackModelId };
41
+ }
42
+ const aliasLabel = parsed.keyAlias ? `${parsed.providerId}[${parsed.keyAlias}]` : parsed.providerId;
43
+ const resolvedModel = parsed.modelId || fallbackModelId;
44
+ return { providerLabel: aliasLabel, resolvedModel };
45
+ }
46
+ function resolveRouteColor(routeName) {
47
+ const map = {
48
+ tools: '\x1b[38;5;214m',
49
+ thinking: '\x1b[34m',
50
+ coding: '\x1b[35m',
51
+ longcontext: '\x1b[38;5;141m',
52
+ web_search: '\x1b[32m',
53
+ search: '\x1b[38;5;34m',
54
+ vision: '\x1b[38;5;207m',
55
+ background: '\x1b[90m'
56
+ };
57
+ return map[routeName] ?? '\x1b[36m';
58
+ }
59
+ function describeContextUsage(providerKey, estimatedTokens, deps) {
60
+ if (typeof estimatedTokens !== 'number' || !Number.isFinite(estimatedTokens) || estimatedTokens <= 0) {
61
+ return undefined;
62
+ }
63
+ let limit = DEFAULT_MODEL_CONTEXT_TOKENS;
64
+ try {
65
+ const profile = deps.providerRegistry.get(providerKey);
66
+ if (profile?.maxContextTokens && Number.isFinite(profile.maxContextTokens)) {
67
+ limit = profile.maxContextTokens;
68
+ }
69
+ }
70
+ catch {
71
+ limit = DEFAULT_MODEL_CONTEXT_TOKENS;
72
+ }
73
+ if (!limit || limit <= 0) {
74
+ return undefined;
75
+ }
76
+ const ratio = estimatedTokens / limit;
77
+ const threshold = deps.contextRouting?.warnRatio ?? 0.9;
78
+ if (ratio < threshold) {
79
+ return undefined;
80
+ }
81
+ return `${ratio.toFixed(2)}/${Math.round(limit)}`;
82
+ }
83
+ function decorateWithDetail(baseLabel, primaryReason, detail) {
84
+ const normalizedDetail = detail && detail.trim();
85
+ if (!normalizedDetail) {
86
+ return primaryReason || baseLabel;
87
+ }
88
+ if (primaryReason) {
89
+ return `${primaryReason}(${normalizedDetail})`;
90
+ }
91
+ return `${baseLabel}(${normalizedDetail})`;
92
+ }
93
+ export function buildHitReason(routeUsed, providerKey, classification, features, mode, deps) {
94
+ const reasoning = classification.reasoning || '';
95
+ let primary = reasoning.split('|')[0] || '';
96
+ const commandDetail = features.lastAssistantToolLabel;
97
+ const isStickyMode = mode === 'sticky';
98
+ if (isStickyMode &&
99
+ (routeUsed === 'tools' || routeUsed === 'thinking' || routeUsed === 'coding')) {
100
+ primary = '';
101
+ }
102
+ const base = (() => {
103
+ if (routeUsed === 'tools') {
104
+ const label = isStickyMode ? 'sticky' : 'tools';
105
+ return decorateWithDetail(primary || label, primary, commandDetail);
106
+ }
107
+ if (routeUsed === 'thinking') {
108
+ const label = isStickyMode ? 'sticky' : 'thinking';
109
+ return decorateWithDetail(primary || label, primary, commandDetail);
110
+ }
111
+ if (routeUsed === 'coding') {
112
+ const label = isStickyMode ? 'sticky' : 'coding';
113
+ return decorateWithDetail(primary || label, primary, commandDetail);
114
+ }
115
+ if (routeUsed === 'web_search' || routeUsed === 'search') {
116
+ return decorateWithDetail(primary || routeUsed, primary, commandDetail);
117
+ }
118
+ if (routeUsed === DEFAULT_ROUTE && classification.fallback) {
119
+ if (isStickyMode) {
120
+ return primary || 'sticky:default';
121
+ }
122
+ return primary || 'fallback:default';
123
+ }
124
+ if (primary) {
125
+ return primary;
126
+ }
127
+ return routeUsed ? `route:${routeUsed}` : 'route:unknown';
128
+ })();
129
+ const contextDetail = describeContextUsage(providerKey, features.estimatedTokens, deps);
130
+ if (contextDetail) {
131
+ return `${base}|context:${contextDetail}`;
132
+ }
133
+ return base;
134
+ }
135
+ export function formatVirtualRouterHit(routeName, poolId, providerKey, modelId, hitReason, stickyScope, routingState) {
136
+ try {
137
+ const now = new Date();
138
+ const hours = String(now.getHours()).padStart(2, '0');
139
+ const minutes = String(now.getMinutes()).padStart(2, '0');
140
+ const seconds = String(now.getSeconds()).padStart(2, '0');
141
+ const timestamp = `${hours}:${minutes}:${seconds}`;
142
+ const prefixColor = '\x1b[38;5;208m';
143
+ const reset = '\x1b[0m';
144
+ const timeColor = '\x1b[90m';
145
+ const stickyColor = '\x1b[33m';
146
+ const routeColor = resolveRouteColor(routeName);
147
+ const stopColor = '\x1b[38;5;214m';
148
+ const prefix = `${prefixColor}[virtual-router-hit]${reset}`;
149
+ const timeLabel = `${timeColor}${timestamp}${reset}`;
150
+ const { providerLabel, resolvedModel } = describeTargetProvider(providerKey, modelId);
151
+ const routeLabel = poolId ? `${routeName}/${poolId}` : routeName;
152
+ const targetLabel = `${routeLabel} -> ${providerLabel}${resolvedModel ? '.' + resolvedModel : ''}`;
153
+ const stickyText = formatStickyScope(stickyScope);
154
+ const stickyLabel = stickyText ? ` ${stickyColor}[sticky:${stickyText}]${reset}` : '';
155
+ const reasonLabel = hitReason ? ` reason=${hitReason}` : '';
156
+ let stopLabel = '';
157
+ if (routingState?.stopMessageText && typeof routingState.stopMessageMaxRepeats === 'number') {
158
+ const text = routingState.stopMessageText;
159
+ const safeText = text.length > 24 ? `${text.slice(0, 21)}…` : text;
160
+ const used = typeof routingState.stopMessageUsed === 'number' && Number.isFinite(routingState.stopMessageUsed)
161
+ ? routingState.stopMessageUsed
162
+ : 0;
163
+ const updatedAt = typeof routingState.stopMessageUpdatedAt === 'number' && Number.isFinite(routingState.stopMessageUpdatedAt)
164
+ ? routingState.stopMessageUpdatedAt
165
+ : undefined;
166
+ const lastUsedAt = typeof routingState.stopMessageLastUsedAt === 'number' && Number.isFinite(routingState.stopMessageLastUsedAt)
167
+ ? routingState.stopMessageLastUsedAt
168
+ : undefined;
169
+ const parts = [`"${safeText}"`, `${used}/${Math.floor(routingState.stopMessageMaxRepeats)}`];
170
+ if (updatedAt) {
171
+ parts.push(`set=${new Date(updatedAt).toLocaleString(undefined, { hour12: false })}`);
172
+ }
173
+ if (lastUsedAt) {
174
+ parts.push(`last=${new Date(lastUsedAt).toLocaleString(undefined, { hour12: false })}`);
175
+ }
176
+ stopLabel = ` ${stopColor}[stopMessage:${parts.join(' ')}]${reset}`;
177
+ }
178
+ return `${prefix} ${timeLabel} ${routeColor}${targetLabel}${stickyLabel}${reasonLabel}${stopLabel}${reset}`;
179
+ }
180
+ catch {
181
+ const now = new Date();
182
+ const timestamp = now.toLocaleTimeString('zh-CN', { hour12: false });
183
+ const routeLabel = poolId ? `${routeName}/${poolId}` : routeName;
184
+ const stickyText = formatStickyScope(stickyScope);
185
+ const stickyLabel = stickyText ? ` [sticky:${stickyText}]` : '';
186
+ let stopLabel = '';
187
+ if (routingState?.stopMessageText && typeof routingState.stopMessageMaxRepeats === 'number') {
188
+ const text = routingState.stopMessageText;
189
+ const safeText = text.length > 24 ? `${text.slice(0, 21)}…` : text;
190
+ const used = typeof routingState.stopMessageUsed === 'number' && Number.isFinite(routingState.stopMessageUsed)
191
+ ? routingState.stopMessageUsed
192
+ : 0;
193
+ stopLabel = ` [stopMessage:"${safeText}" ${used}/${Math.floor(routingState.stopMessageMaxRepeats)}]`;
194
+ }
195
+ return `[virtual-router-hit] ${timestamp} ${routeLabel} -> ${providerKey}${modelId ? '.' + modelId : ''}${stickyLabel}${hitReason ? ` reason=${hitReason}` : ''}${stopLabel}`;
196
+ }
197
+ }
@@ -0,0 +1,32 @@
1
+ import type { ClassificationResult, RoutePoolTier, RouterMetadataInput, RoutingFeatures } from './types.js';
2
+ import type { RoutingInstructionState } from './routing-instructions.js';
3
+ import type { ContextAdvisor } from './context-advisor.js';
4
+ import type { RouteLoadBalancer } from './load-balancer.js';
5
+ import type { ProviderHealthManager } from './health-manager.js';
6
+ import type { ProviderRegistry } from './provider-registry.js';
7
+ type SelectionDeps = {
8
+ routing: Record<string, RoutePoolTier[]>;
9
+ providerRegistry: ProviderRegistry;
10
+ healthManager: ProviderHealthManager;
11
+ contextAdvisor: ContextAdvisor;
12
+ loadBalancer: RouteLoadBalancer;
13
+ isProviderCoolingDown: (providerKey: string) => boolean;
14
+ resolveStickyKey: (metadata: RouterMetadataInput) => string | undefined;
15
+ };
16
+ export declare function selectProviderImpl(requestedRoute: string, metadata: RouterMetadataInput, classification: ClassificationResult, features: RoutingFeatures, activeState: RoutingInstructionState, deps: SelectionDeps, options?: {
17
+ routingState?: RoutingInstructionState;
18
+ }): {
19
+ providerKey: string;
20
+ routeUsed: string;
21
+ pool: string[];
22
+ poolId?: string;
23
+ };
24
+ export declare function selectFromStickyPool(stickyKeySet: Set<string>, metadata: RouterMetadataInput, features: RoutingFeatures, state: RoutingInstructionState, deps: SelectionDeps, options: {
25
+ allowAliasRotation?: boolean;
26
+ }): {
27
+ providerKey: string;
28
+ routeUsed: string;
29
+ pool: string[];
30
+ poolId?: string;
31
+ } | null;
32
+ export {};