@jsonstudio/llms 0.6.1892 → 0.6.2172

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 (159) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.js +16 -2
  2. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
  4. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
  5. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
  7. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
  8. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
  9. package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
  10. package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
  11. package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
  12. package/dist/conversion/compat/antigravity-session-signature.js +15 -0
  13. package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
  14. package/dist/conversion/compat/profiles/chat-glm.json +22 -0
  15. package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
  16. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
  17. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
  18. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
  19. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
  20. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
  22. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
  24. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
  25. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
  27. package/dist/conversion/hub/process/chat-process.js +85 -18
  28. package/dist/conversion/hub/response/provider-response.js +21 -50
  29. package/dist/conversion/hub/response/response-runtime.js +71 -10
  30. package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
  31. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
  32. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
  33. package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
  34. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
  35. package/dist/conversion/responses/responses-openai-bridge.js +193 -504
  36. package/dist/conversion/shared/anthropic-message-utils.js +82 -2
  37. package/dist/conversion/shared/bridge-message-utils.js +92 -39
  38. package/dist/conversion/shared/snapshot-hooks.js +8 -13
  39. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
  40. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
  41. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
  42. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
  43. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
  44. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
  45. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
  46. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
  47. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
  48. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
  49. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
  50. package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
  51. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
  52. package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
  53. package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
  54. package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
  55. package/dist/conversion/shared/tool-governor.js +136 -10
  56. package/dist/filters/utils/snapshot-writer.js +3 -3
  57. package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
  58. package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
  59. package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
  60. package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
  61. package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
  62. package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
  63. package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
  64. package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
  65. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
  66. package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
  67. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
  68. package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
  69. package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
  70. package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
  71. package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
  72. package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
  73. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
  74. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
  75. package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
  76. package/dist/router/virtual-router/bootstrap/utils.js +41 -0
  77. package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
  78. package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
  79. package/dist/router/virtual-router/bootstrap.d.ts +0 -4
  80. package/dist/router/virtual-router/bootstrap.js +31 -1275
  81. package/dist/router/virtual-router/classifier.js +32 -14
  82. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
  83. package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
  84. package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
  85. package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
  86. package/dist/router/virtual-router/engine/route-analytics.js +44 -0
  87. package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
  88. package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
  89. package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
  90. package/dist/router/virtual-router/engine-logging.d.ts +42 -1
  91. package/dist/router/virtual-router/engine-logging.js +82 -15
  92. package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
  93. package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
  94. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
  95. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
  96. package/dist/router/virtual-router/engine-selection/tier-selection.js +31 -1
  97. package/dist/router/virtual-router/engine.d.ts +21 -7
  98. package/dist/router/virtual-router/engine.js +198 -194
  99. package/dist/router/virtual-router/features.js +12 -4
  100. package/dist/router/virtual-router/message-utils.d.ts +8 -0
  101. package/dist/router/virtual-router/message-utils.js +170 -45
  102. package/dist/router/virtual-router/pre-command-file-resolver.js +40 -2
  103. package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
  104. package/dist/router/virtual-router/routing-instructions.js +18 -2
  105. package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
  106. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
  107. package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
  108. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  109. package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
  110. package/dist/router/virtual-router/token-counter.js +51 -10
  111. package/dist/router/virtual-router/tool-signals.js +4 -0
  112. package/dist/router/virtual-router/types.d.ts +15 -0
  113. package/dist/servertool/clock/session-scope.d.ts +3 -0
  114. package/dist/servertool/clock/session-scope.js +52 -0
  115. package/dist/servertool/clock/state.js +9 -0
  116. package/dist/servertool/clock/tasks.js +12 -1
  117. package/dist/servertool/clock/types.d.ts +3 -0
  118. package/dist/servertool/engine.js +177 -31
  119. package/dist/servertool/handlers/clock-auto.js +2 -8
  120. package/dist/servertool/handlers/clock.js +6 -9
  121. package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
  122. package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
  123. package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
  124. package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
  125. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
  126. package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
  127. package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
  128. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
  129. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
  130. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -1
  131. package/dist/servertool/handlers/stop-message-auto.js +80 -556
  132. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
  133. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
  134. package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
  135. package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
  136. package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
  137. package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
  138. package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
  139. package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
  140. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
  141. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
  142. package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
  143. package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
  144. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
  145. package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
  146. package/dist/servertool/handlers/web-search.js +117 -0
  147. package/dist/servertool/server-side-tools.d.ts +0 -1
  148. package/dist/servertool/server-side-tools.js +4 -3
  149. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  150. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
  151. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
  152. package/dist/telemetry/stats-center.d.ts +9 -0
  153. package/dist/telemetry/stats-center.js +29 -1
  154. package/dist/tools/apply-patch/structured/coercion.js +3 -11
  155. package/dist/tools/exec-command/validator.d.ts +1 -0
  156. package/dist/tools/exec-command/validator.js +132 -0
  157. package/dist/tools/tool-registry.d.ts +1 -0
  158. package/dist/tools/tool-registry.js +1 -1
  159. package/package.json +1 -1
@@ -1,35 +1,12 @@
1
- import { DEFAULT_MODEL_CONTEXT_TOKENS, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
2
- import { scanDeepSeekAccountTokenFiles, scanOAuthTokenFiles } from './token-file-scanner.js';
3
- const DEFAULT_CLASSIFIER = {
4
- longContextThresholdTokens: 180000,
5
- thinkingKeywords: ['think step', 'analysis', 'reasoning', '仔细分析', '深度思考'],
6
- codingKeywords: ['apply_patch', 'write_file', 'create_file', 'shell', '修改文件', '写入文件'],
7
- backgroundKeywords: ['background', 'context dump', '上下文'],
8
- visionKeywords: ['vision', 'image', 'picture', 'photo']
9
- };
10
- const DEFAULT_LOAD_BALANCING = { strategy: 'round-robin' };
11
- const DEFAULT_HEALTH = { failureThreshold: 3, cooldownMs: 30_000, fatalCooldownMs: 300_000 };
12
- const DEFAULT_CONTEXT_ROUTING = {
13
- warnRatio: 0.9,
14
- hardLimit: false
15
- };
16
- const CLAUDE_CODE_DEFAULT_USER_AGENT = 'claude-cli/2.0.76 (external, cli)';
17
- const CLAUDE_CODE_DEFAULT_X_APP = 'claude-cli';
18
- // Claude Code upstream gates may require anthropic-beta to be present (value is service-specific).
19
- const CLAUDE_CODE_DEFAULT_ANTHROPIC_BETA = 'claude-code';
20
- function parseClaudeCodeAppVersionFromUserAgent(userAgent) {
21
- const ua = typeof userAgent === 'string' ? userAgent.trim() : '';
22
- if (!ua) {
23
- return null;
24
- }
25
- const match = /claude-cli\/([0-9][^ )]*)/i.exec(ua);
26
- const version = match?.[1]?.trim() ?? '';
27
- return version ? version : null;
28
- }
29
- /**
30
- * 将用户提供的 Virtual Router 配置(或包含 virtualrouter 字段的整体配置)
31
- * 规范化为 VirtualRouterConfig,供 HubPipeline / VirtualRouterEngine 直接使用。
32
- */
1
+ import { VirtualRouterError, VirtualRouterErrorCode } from './types.js';
2
+ import { DEFAULT_CLASSIFIER, DEFAULT_CONTEXT_ROUTING, DEFAULT_HEALTH, DEFAULT_LOAD_BALANCING } from './bootstrap/config-defaults.js';
3
+ import { asRecord } from './bootstrap/utils.js';
4
+ import { normalizeClock, normalizeContextRouting, normalizeExecCommandGuard } from './bootstrap/config-normalizers.js';
5
+ import { normalizeHealth, buildProviderProfiles } from './bootstrap/profile-builder.js';
6
+ import { normalizeRouting, expandRoutingTable, buildRuntimeKey } from './bootstrap/routing-config.js';
7
+ import { normalizeProvider } from './bootstrap/provider-normalization.js';
8
+ import { extractProviderAuthEntries } from './bootstrap/auth-utils.js';
9
+ import { normalizeWebSearch, validateWebSearchRouting } from './bootstrap/web-search-config.js';
33
10
  export function bootstrapVirtualRouterConfig(input) {
34
11
  const section = extractVirtualRouterSection(input);
35
12
  const providersSource = asRecord(section.providers);
@@ -49,10 +26,6 @@ export function bootstrapVirtualRouterConfig(input) {
49
26
  if (!routing.default || routing.default.length === 0) {
50
27
  throw new VirtualRouterError('Virtual Router default route must contain at least one provider target', VirtualRouterErrorCode.CONFIG_ERROR);
51
28
  }
52
- // Build provider profiles for:
53
- // - routing targets (targetKeys), and
54
- // - all declared provider models across all auth aliases (modelIndex × aliasIndex),
55
- // so that "direct model" and "prefer model" routing can bypass route pools when needed.
56
29
  const expandedTargetKeys = new Set(targetKeys);
57
30
  for (const [providerId, aliases] of aliasIndex.entries()) {
58
31
  const models = modelIndex.get(providerId)?.models ?? [];
@@ -94,9 +67,7 @@ export function bootstrapVirtualRouterConfig(input) {
94
67
  }
95
68
  function extractVirtualRouterSection(input) {
96
69
  const root = asRecord(input);
97
- const section = root.virtualrouter && typeof root.virtualrouter === 'object'
98
- ? asRecord(root.virtualrouter)
99
- : root;
70
+ const section = root.virtualrouter && typeof root.virtualrouter === 'object' ? asRecord(root.virtualrouter) : root;
100
71
  const providers = asRecord(section.providers ?? root.providers);
101
72
  const routing = asRecord(section.routing ?? root.routing);
102
73
  const classifier = (section.classifier ?? root.classifier);
@@ -108,47 +79,13 @@ function extractVirtualRouterSection(input) {
108
79
  const clock = section.clock ?? root.clock;
109
80
  return { providers, routing, classifier, loadBalancing, health, contextRouting, webSearch, execCommandGuard, clock };
110
81
  }
111
- function normalizeClock(raw) {
112
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
113
- return undefined;
114
- }
115
- const record = raw;
116
- const enabled = record.enabled === true ||
117
- (typeof record.enabled === 'string' && record.enabled.trim().toLowerCase() === 'true') ||
118
- (typeof record.enabled === 'number' && record.enabled === 1);
119
- if (!enabled) {
120
- return undefined;
121
- }
122
- const out = { enabled: true };
123
- if (typeof record.retentionMs === 'number' && Number.isFinite(record.retentionMs) && record.retentionMs >= 0) {
124
- out.retentionMs = Math.floor(record.retentionMs);
125
- }
126
- if (typeof record.dueWindowMs === 'number' && Number.isFinite(record.dueWindowMs) && record.dueWindowMs >= 0) {
127
- out.dueWindowMs = Math.floor(record.dueWindowMs);
128
- }
129
- if (typeof record.tickMs === 'number' && Number.isFinite(record.tickMs) && record.tickMs >= 0) {
130
- out.tickMs = Math.floor(record.tickMs);
131
- }
132
- if (record.holdNonStreaming === true ||
133
- (typeof record.holdNonStreaming === 'string' && record.holdNonStreaming.trim().toLowerCase() === 'true') ||
134
- (typeof record.holdNonStreaming === 'number' && record.holdNonStreaming === 1)) {
135
- out.holdNonStreaming = true;
136
- }
137
- if (typeof record.holdMaxMs === 'number' && Number.isFinite(record.holdMaxMs) && record.holdMaxMs >= 0) {
138
- out.holdMaxMs = Math.floor(record.holdMaxMs);
139
- }
140
- return out;
141
- }
142
82
  function buildProviderRuntimeEntries(providers) {
143
83
  const runtimeEntries = {};
144
84
  const aliasIndex = new Map();
145
85
  const modelIndex = new Map();
146
86
  for (const [providerId, providerRaw] of Object.entries(providers)) {
147
87
  const normalizedProvider = normalizeProvider(providerId, providerRaw);
148
- const rawModelsNode = providerRaw?.models;
149
- const modelsDeclared = rawModelsNode !== undefined;
150
- const modelsNode = asRecord(rawModelsNode);
151
- modelIndex.set(providerId, { declared: modelsDeclared, models: Object.keys(modelsNode).filter(Boolean) });
88
+ modelIndex.set(providerId, collectProviderModels(providerRaw, normalizedProvider));
152
89
  const authEntries = extractProviderAuthEntries(providerId, providerRaw);
153
90
  if (!authEntries.length) {
154
91
  throw new VirtualRouterError(`Provider ${providerId} requires at least one auth entry`, VirtualRouterErrorCode.CONFIG_ERROR);
@@ -172,8 +109,6 @@ function buildProviderRuntimeEntries(providers) {
172
109
  userInfoUrl: entry.auth.userInfoUrl,
173
110
  refreshUrl: entry.auth.refreshUrl
174
111
  };
175
- // 为 OAuth 类型的 auth 设置 tokenFile 为 alias(如果没有显式配置 tokenFile)
176
- // 这允许 oauth-lifecycle.ts 的 resolveTokenFilePath 函数正确解析并匹配现有文件
177
112
  if (!runtimeAuth.tokenFile && (runtimeAuth.rawType?.includes('oauth') || runtimeAuth.type === 'oauth')) {
178
113
  runtimeAuth.tokenFile = entry.keyAlias;
179
114
  }
@@ -203,290 +138,30 @@ function buildProviderRuntimeEntries(providers) {
203
138
  }
204
139
  return { runtimeEntries, aliasIndex, modelIndex };
205
140
  }
206
- function expandRoutingTable(routingSource, aliasIndex, modelIndex) {
207
- const routing = {};
208
- const targetKeys = new Set();
209
- for (const [routeName, pools] of Object.entries(routingSource)) {
210
- const expandedPools = [];
211
- for (const pool of pools) {
212
- const expandedTargets = [];
213
- let orderCounter = 0;
214
- for (const entry of pool.targets) {
215
- const parsed = parseRouteEntry(entry, aliasIndex);
216
- if (!parsed) {
217
- continue;
218
- }
219
- if (!aliasIndex.has(parsed.providerId)) {
220
- throw new VirtualRouterError(`Route "${routeName}" references unknown provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
221
- }
222
- const modelInfo = modelIndex.get(parsed.providerId);
223
- if (modelInfo?.declared) {
224
- if (!parsed.modelId) {
225
- throw new VirtualRouterError(`Route "${routeName}" references empty model id for provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
226
- }
227
- const knownModels = modelInfo.models ?? [];
228
- if (!knownModels.length) {
229
- throw new VirtualRouterError(`Route "${routeName}" references provider "${parsed.providerId}" but provider declares no models`, VirtualRouterErrorCode.CONFIG_ERROR);
230
- }
231
- if (!knownModels.includes(parsed.modelId)) {
232
- throw new VirtualRouterError(`Route "${routeName}" references unknown model "${parsed.modelId}" for provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
233
- }
234
- }
235
- const aliases = parsed.keyAlias ? [parsed.keyAlias] : aliasIndex.get(parsed.providerId);
236
- if (!aliases.length) {
237
- throw new VirtualRouterError(`Provider ${parsed.providerId} has no auth aliases but is referenced in routing`, VirtualRouterErrorCode.CONFIG_ERROR);
238
- }
239
- for (const alias of aliases) {
240
- const runtimeKey = buildRuntimeKey(parsed.providerId, alias);
241
- const targetKey = `${runtimeKey}.${parsed.modelId}`;
242
- const existing = expandedTargets.find((candidate) => candidate.key === targetKey);
243
- if (existing) {
244
- if (parsed.priority > existing.priority) {
245
- existing.priority = parsed.priority;
246
- }
247
- continue;
248
- }
249
- expandedTargets.push({ key: targetKey, priority: parsed.priority, order: orderCounter });
250
- orderCounter += 1;
251
- targetKeys.add(targetKey);
252
- }
253
- }
254
- if (expandedTargets.length) {
255
- const sortedTargets = pool.mode === 'priority'
256
- ? [...expandedTargets]
257
- .sort((a, b) => {
258
- if (a.priority !== b.priority) {
259
- return b.priority - a.priority;
260
- }
261
- return a.order - b.order;
262
- })
263
- .map((candidate) => candidate.key)
264
- : expandedTargets.map((candidate) => candidate.key);
265
- expandedPools.push({
266
- id: pool.id,
267
- priority: pool.priority,
268
- backup: pool.backup,
269
- targets: sortedTargets,
270
- ...(pool.mode ? { mode: pool.mode } : {}),
271
- ...(pool.force ? { force: true } : {})
272
- });
273
- }
274
- }
275
- routing[routeName] = expandedPools;
276
- }
277
- return { routing, targetKeys };
278
- }
279
- function buildProviderProfiles(targetKeys, runtimeEntries) {
280
- const profiles = {};
281
- const targetRuntime = {};
282
- for (const targetKey of targetKeys) {
283
- const parsed = parseTargetKey(targetKey);
284
- if (!parsed)
285
- continue;
286
- const runtimeKey = buildRuntimeKey(parsed.providerId, parsed.keyAlias);
287
- const runtime = runtimeEntries[runtimeKey];
288
- if (!runtime) {
289
- throw new VirtualRouterError(`Routing target ${targetKey} references unknown runtime key ${runtimeKey}`, VirtualRouterErrorCode.CONFIG_ERROR);
290
- }
291
- const streamingPref = runtime.modelStreaming?.[parsed.modelId] !== undefined
292
- ? runtime.modelStreaming?.[parsed.modelId]
293
- : runtime.streaming;
294
- const contextTokens = resolveContextTokens(runtime, parsed.modelId);
295
- profiles[targetKey] = {
296
- providerKey: targetKey,
297
- providerType: runtime.providerType,
298
- endpoint: runtime.endpoint,
299
- auth: { ...runtime.auth },
300
- outboundProfile: runtime.outboundProfile,
301
- compatibilityProfile: runtime.compatibilityProfile,
302
- runtimeKey,
303
- modelId: parsed.modelId,
304
- processMode: runtime.processMode || 'chat',
305
- responsesConfig: runtime.responsesConfig,
306
- streaming: streamingPref,
307
- maxContextTokens: contextTokens,
308
- ...(runtime.deepseek ? { deepseek: runtime.deepseek } : {}),
309
- ...(runtime.serverToolsDisabled ? { serverToolsDisabled: true } : {})
310
- };
311
- targetRuntime[targetKey] = {
312
- ...runtime,
313
- modelId: parsed.modelId,
314
- streaming: streamingPref,
315
- maxContextTokens: contextTokens
316
- };
141
+ function collectProviderModels(providerRaw, normalizedProvider) {
142
+ const rawModelsNode = providerRaw.models;
143
+ const modelsDeclared = rawModelsNode !== undefined;
144
+ const modelsNode = asRecord(rawModelsNode);
145
+ const baseModels = Object.keys(modelsNode).filter(Boolean);
146
+ if (normalizedProvider.compatibilityProfile !== 'chat:deepseek-web') {
147
+ return { declared: modelsDeclared, models: baseModels };
317
148
  }
318
- return { profiles, targetRuntime };
319
- }
320
- function resolveContextTokens(runtime, modelId) {
321
- const specific = runtime.modelContextTokens?.[modelId];
322
- if (typeof specific === 'number' && Number.isFinite(specific) && specific > 0) {
323
- return Math.floor(specific);
149
+ const withAliases = new Set(baseModels);
150
+ const hasChatBase = withAliases.has('deepseek-chat') || withAliases.has('deepseek-v3');
151
+ const hasReasonerBase = withAliases.has('deepseek-reasoner') || withAliases.has('deepseek-r1');
152
+ if (hasChatBase) {
153
+ withAliases.add('deepseek-chat-search');
154
+ withAliases.add('deepseek-v3-search');
324
155
  }
325
- const fallback = runtime.defaultContextTokens ?? runtime.maxContextTokens;
326
- if (typeof fallback === 'number' && Number.isFinite(fallback) && fallback > 0) {
327
- return Math.floor(fallback);
156
+ if (hasReasonerBase) {
157
+ withAliases.add('deepseek-reasoner-search');
158
+ withAliases.add('deepseek-r1-search');
328
159
  }
329
- return DEFAULT_MODEL_CONTEXT_TOKENS;
330
- }
331
- function normalizeRouting(source) {
332
- const routing = {};
333
- for (const [routeName, entries] of Object.entries(source)) {
334
- if (!Array.isArray(entries) || !entries.length) {
335
- routing[routeName] = [];
336
- continue;
337
- }
338
- const allStrings = entries.every((entry) => typeof entry === 'string' || entry === null || entry === undefined);
339
- if (allStrings) {
340
- const targets = normalizeTargetList(entries);
341
- routing[routeName] = targets.length ? [buildLegacyRoutePool(routeName, targets)] : [];
342
- continue;
343
- }
344
- const normalized = [];
345
- const total = entries.length || 1;
346
- for (let index = 0; index < entries.length; index += 1) {
347
- const entry = entries[index];
348
- const pool = normalizeRoutePoolEntry(routeName, entry, index, total);
349
- if (pool && pool.targets.length) {
350
- normalized.push(pool);
351
- }
352
- }
353
- routing[routeName] = normalized;
354
- }
355
- return routing;
356
- }
357
- function buildLegacyRoutePool(routeName, targets) {
358
- return {
359
- id: `${routeName}:pool0`,
360
- priority: targets.length,
361
- backup: false,
362
- targets
363
- };
364
- }
365
- function normalizeRoutePoolEntry(routeName, entry, index, total) {
366
- if (typeof entry === 'string') {
367
- const targets = normalizeTargetList(entry);
368
- return targets.length
369
- ? {
370
- id: `${routeName}:pool${index + 1}`,
371
- priority: total - index,
372
- backup: false,
373
- targets
374
- }
375
- : null;
376
- }
377
- if (!entry || typeof entry !== 'object') {
378
- return null;
379
- }
380
- const record = entry;
381
- const id = readOptionalString(record.id) ??
382
- readOptionalString(record?.poolId) ??
383
- `${routeName}:pool${index + 1}`;
384
- const backup = record.backup === true ||
385
- record.isBackup === true ||
386
- (typeof record.type === 'string' && record.type.toLowerCase() === 'backup');
387
- const priority = normalizePriorityValue(record.priority, total - index);
388
- const targets = normalizeRouteTargets(record);
389
- const mode = normalizeRoutePoolMode(record.mode ?? record?.strategy ?? record?.routingMode);
390
- const force = record.force === true ||
391
- (typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true');
392
- return targets.length
393
- ? {
394
- id,
395
- priority,
396
- backup,
397
- targets,
398
- ...(mode ? { mode } : {}),
399
- ...(force ? { force: true } : {})
400
- }
401
- : null;
402
- }
403
- function normalizeRoutePoolMode(value) {
404
- if (typeof value !== 'string') {
405
- return undefined;
406
- }
407
- const normalized = value.trim().toLowerCase();
408
- if (!normalized) {
409
- return undefined;
410
- }
411
- if (normalized === 'priority') {
412
- return 'priority';
413
- }
414
- if (normalized === 'round-robin' ||
415
- normalized === 'round_robin' ||
416
- normalized === 'roundrobin' ||
417
- normalized === 'rr') {
418
- return 'round-robin';
419
- }
420
- return undefined;
421
- }
422
- function normalizeRouteTargets(record) {
423
- const buckets = [
424
- record.targets,
425
- record.providers,
426
- record.pool,
427
- record.entries,
428
- record.items,
429
- record.routes
430
- ];
431
- const normalized = [];
432
- for (const bucket of buckets) {
433
- for (const target of normalizeTargetList(bucket)) {
434
- if (!normalized.includes(target)) {
435
- normalized.push(target);
436
- }
437
- }
438
- }
439
- const singular = [record.target, record.provider];
440
- for (const candidate of singular) {
441
- for (const target of normalizeTargetList(candidate)) {
442
- if (!normalized.includes(target)) {
443
- normalized.push(target);
444
- }
445
- }
446
- }
447
- return normalized;
448
- }
449
- function normalizeTargetList(value) {
450
- if (Array.isArray(value)) {
451
- const normalized = [];
452
- for (const entry of value) {
453
- if (typeof entry === 'string') {
454
- const trimmed = entry.trim();
455
- if (trimmed && !normalized.includes(trimmed)) {
456
- normalized.push(trimmed);
457
- }
458
- }
459
- }
460
- return normalized;
461
- }
462
- if (typeof value === 'string') {
463
- const trimmed = value.trim();
464
- return trimmed ? [trimmed] : [];
465
- }
466
- if (typeof value === 'number') {
467
- const str = String(value).trim();
468
- return str ? [str] : [];
469
- }
470
- return [];
471
- }
472
- function normalizePriorityValue(value, fallback) {
473
- if (typeof value === 'number' && Number.isFinite(value)) {
474
- return value;
475
- }
476
- if (typeof value === 'string') {
477
- const trimmed = value.trim();
478
- if (trimmed) {
479
- const parsed = Number(trimmed);
480
- if (Number.isFinite(parsed)) {
481
- return parsed;
482
- }
483
- }
484
- }
485
- return fallback;
160
+ return { declared: modelsDeclared, models: Array.from(withAliases) };
486
161
  }
487
162
  function normalizeClassifier(input) {
488
163
  const normalized = asRecord(input);
489
- const result = {
164
+ return {
490
165
  longContextThresholdTokens: typeof normalized.longContextThresholdTokens === 'number'
491
166
  ? normalized.longContextThresholdTokens
492
167
  : DEFAULT_CLASSIFIER.longContextThresholdTokens,
@@ -495,7 +170,6 @@ function normalizeClassifier(input) {
495
170
  backgroundKeywords: normalizeStringArray(normalized.backgroundKeywords, DEFAULT_CLASSIFIER.backgroundKeywords),
496
171
  visionKeywords: normalizeStringArray(normalized.visionKeywords, DEFAULT_CLASSIFIER.visionKeywords)
497
172
  };
498
- return result;
499
173
  }
500
174
  function normalizeStringArray(value, fallback) {
501
175
  if (!Array.isArray(value)) {
@@ -504,800 +178,6 @@ function normalizeStringArray(value, fallback) {
504
178
  const normalized = value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
505
179
  return normalized.length ? normalized : [...fallback];
506
180
  }
507
- function normalizeProvider(providerId, raw) {
508
- const provider = asRecord(raw);
509
- const providerType = detectProviderType(provider);
510
- const endpoint = typeof provider.endpoint === 'string' && provider.endpoint.trim()
511
- ? provider.endpoint.trim()
512
- : typeof provider.baseURL === 'string' && provider.baseURL.trim()
513
- ? provider.baseURL.trim()
514
- : typeof provider.baseUrl === 'string' && provider.baseUrl.trim()
515
- ? provider.baseUrl.trim()
516
- : '';
517
- const compatibilityProfile = resolveCompatibilityProfile(providerId, provider);
518
- const headers = maybeInjectClaudeCodeHeaders(providerId, providerType, compatibilityProfile, normalizeHeaders(provider.headers));
519
- const responsesNode = asRecord(provider.responses);
520
- const responsesConfig = normalizeResponsesConfig({
521
- providerId,
522
- providerType,
523
- compatibilityProfile,
524
- provider,
525
- node: responsesNode
526
- });
527
- const processMode = normalizeProcessMode(provider.process);
528
- const streaming = resolveProviderStreamingPreference(provider, responsesNode);
529
- const modelStreaming = normalizeModelStreaming(provider);
530
- const { modelContextTokens, defaultContextTokens } = normalizeModelContextTokens(provider);
531
- const deepseek = normalizeDeepSeekOptions(provider);
532
- const serverToolsDisabled = provider.serverToolsDisabled === true ||
533
- (typeof provider.serverToolsDisabled === 'string' &&
534
- provider.serverToolsDisabled.trim().toLowerCase() === 'true') ||
535
- (provider.serverTools &&
536
- typeof provider.serverTools === 'object' &&
537
- provider.serverTools.enabled === false);
538
- return {
539
- providerId,
540
- providerType,
541
- endpoint,
542
- headers,
543
- outboundProfile: mapOutboundProfile(providerType),
544
- compatibilityProfile,
545
- processMode,
546
- responsesConfig,
547
- streaming,
548
- modelStreaming,
549
- modelContextTokens,
550
- defaultContextTokens,
551
- ...(deepseek ? { deepseek } : {}),
552
- ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
553
- };
554
- }
555
- function hasHeader(headers, name) {
556
- if (!headers) {
557
- return false;
558
- }
559
- const lowered = name.trim().toLowerCase();
560
- if (!lowered) {
561
- return false;
562
- }
563
- for (const key of Object.keys(headers)) {
564
- if (key.trim().toLowerCase() === lowered) {
565
- const value = headers[key];
566
- if (typeof value === 'string' && value.trim()) {
567
- return true;
568
- }
569
- }
570
- }
571
- return false;
572
- }
573
- function maybeInjectClaudeCodeHeaders(_providerId, providerType, compatibilityProfile, headers) {
574
- const profile = typeof compatibilityProfile === 'string' ? compatibilityProfile.trim().toLowerCase() : '';
575
- if (!profile || (profile !== 'anthropic:claude-code' && profile !== 'chat:claude-code')) {
576
- return headers;
577
- }
578
- if (!String(providerType).toLowerCase().includes('anthropic')) {
579
- return headers;
580
- }
581
- const base = { ...(headers ?? {}) };
582
- if (!hasHeader(base, 'User-Agent')) {
583
- base['User-Agent'] = CLAUDE_CODE_DEFAULT_USER_AGENT;
584
- }
585
- if (!hasHeader(base, 'X-App')) {
586
- base['X-App'] = CLAUDE_CODE_DEFAULT_X_APP;
587
- }
588
- if (!hasHeader(base, 'X-App-Version')) {
589
- const version = parseClaudeCodeAppVersionFromUserAgent(base['User-Agent'] ?? '');
590
- if (version) {
591
- base['X-App-Version'] = version;
592
- }
593
- }
594
- if (!hasHeader(base, 'anthropic-beta')) {
595
- base['anthropic-beta'] = CLAUDE_CODE_DEFAULT_ANTHROPIC_BETA;
596
- }
597
- return base;
598
- }
599
- function normalizeModelStreaming(provider) {
600
- const modelsNode = asRecord(provider.models);
601
- if (!modelsNode) {
602
- return undefined;
603
- }
604
- const normalized = {};
605
- for (const [modelId, modelRaw] of Object.entries(modelsNode)) {
606
- if (!modelRaw || typeof modelRaw !== 'object') {
607
- continue;
608
- }
609
- const preference = resolveStreamingPreference(modelRaw);
610
- if (preference) {
611
- normalized[modelId] = preference;
612
- }
613
- }
614
- return Object.keys(normalized).length ? normalized : undefined;
615
- }
616
- function normalizeModelContextTokens(provider) {
617
- const modelsNode = asRecord(provider.models);
618
- const normalized = {};
619
- for (const [modelId, modelRaw] of Object.entries(modelsNode)) {
620
- if (!modelRaw || typeof modelRaw !== 'object') {
621
- continue;
622
- }
623
- const candidate = readContextTokens(modelRaw);
624
- if (candidate) {
625
- normalized[modelId] = candidate;
626
- }
627
- }
628
- const configNode = asRecord(provider.config);
629
- const defaultsNode = asRecord(configNode?.userConfigDefaults);
630
- const defaultCandidate = readContextTokens(provider) ??
631
- readContextTokens(configNode) ??
632
- readContextTokens(defaultsNode);
633
- return {
634
- modelContextTokens: Object.keys(normalized).length ? normalized : undefined,
635
- defaultContextTokens: defaultCandidate
636
- };
637
- }
638
- function normalizeDeepSeekOptions(provider) {
639
- const direct = asRecord(provider.deepseek);
640
- const ext = asRecord(asRecord(provider.extensions)?.deepseek);
641
- const source = Object.keys(direct).length ? direct : ext;
642
- if (!source || !Object.keys(source).length) {
643
- return undefined;
644
- }
645
- const strictToolRequired = typeof source.strictToolRequired === 'boolean'
646
- ? source.strictToolRequired
647
- : typeof source.strictToolRequired === 'string'
648
- ? source.strictToolRequired.trim().toLowerCase() === 'true'
649
- : undefined;
650
- const textToolFallback = typeof source.textToolFallback === 'boolean'
651
- ? source.textToolFallback
652
- : typeof source.textToolFallback === 'string'
653
- ? source.textToolFallback.trim().toLowerCase() === 'true'
654
- : undefined;
655
- if (strictToolRequired === undefined && textToolFallback === undefined) {
656
- return undefined;
657
- }
658
- return {
659
- ...(strictToolRequired !== undefined ? { strictToolRequired } : {}),
660
- ...(textToolFallback !== undefined ? { textToolFallback } : {})
661
- };
662
- }
663
- function resolveStreamingPreference(model) {
664
- return (coerceStreamingPreference(model.streaming) ??
665
- coerceStreamingPreference(model.stream) ??
666
- coerceStreamingPreference(model.supportsStreaming));
667
- }
668
- function coerceStreamingPreference(value) {
669
- if (typeof value === 'string') {
670
- const normalized = value.trim().toLowerCase();
671
- if (normalized === 'always' || normalized === 'auto' || normalized === 'never') {
672
- return normalized;
673
- }
674
- if (normalized === 'true') {
675
- return 'always';
676
- }
677
- if (normalized === 'false') {
678
- return 'never';
679
- }
680
- }
681
- if (typeof value === 'boolean') {
682
- return value ? 'always' : 'never';
683
- }
684
- if (value && typeof value === 'object') {
685
- const record = value;
686
- if (record.mode !== undefined) {
687
- return coerceStreamingPreference(record.mode);
688
- }
689
- if (record.value !== undefined) {
690
- return coerceStreamingPreference(record.value);
691
- }
692
- if (record.enabled !== undefined) {
693
- return coerceStreamingPreference(record.enabled);
694
- }
695
- }
696
- return undefined;
697
- }
698
- function normalizeResponsesConfig(options) {
699
- const source = options.node ?? asRecord(options.provider.responses);
700
- const rawStyle = typeof source.toolCallIdStyle === 'string' ? source.toolCallIdStyle.trim().toLowerCase() : undefined;
701
- if (rawStyle === 'fc' || rawStyle === 'preserve') {
702
- return { toolCallIdStyle: rawStyle };
703
- }
704
- const providerType = typeof options.providerType === 'string' ? options.providerType.trim().toLowerCase() : '';
705
- if (!providerType.includes('responses')) {
706
- return undefined;
707
- }
708
- const providerId = typeof options.providerId === 'string' ? options.providerId.trim().toLowerCase() : '';
709
- const compat = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
710
- // Default tool-call id style:
711
- // - Standard OpenAI /v1/responses requires function_call ids to start with "fc_".
712
- // - LM Studio (OpenAI-compatible) often emits `call_*` ids and expects them to be preserved.
713
- const isLmstudio = providerId === 'lmstudio' || compat === 'chat:lmstudio';
714
- return { toolCallIdStyle: isLmstudio ? 'preserve' : 'fc' };
715
- }
716
- function resolveProviderStreamingPreference(provider, responsesNode) {
717
- const configNode = asRecord(provider.config);
718
- const configResponses = configNode ? asRecord(configNode.responses) : undefined;
719
- return (coerceStreamingPreference(provider.streaming ?? provider.stream ?? provider.supportsStreaming ?? provider.streamingPreference) ??
720
- coerceStreamingPreference(responsesNode?.streaming ?? responsesNode?.stream ?? responsesNode?.supportsStreaming) ??
721
- coerceStreamingPreference(configResponses?.streaming ?? configResponses?.stream));
722
- }
723
- function resolveCompatibilityProfile(providerId, provider) {
724
- if (typeof provider.compatibilityProfile === 'string' && provider.compatibilityProfile.trim()) {
725
- return provider.compatibilityProfile.trim();
726
- }
727
- const legacyFields = [];
728
- if (typeof provider.compat === 'string') {
729
- legacyFields.push('compat');
730
- }
731
- if (typeof provider.compatibility_profile === 'string') {
732
- legacyFields.push('compatibility_profile');
733
- }
734
- if (legacyFields.length > 0) {
735
- throw new VirtualRouterError(`Provider "${providerId}" uses legacy compatibility field(s): ${legacyFields.join(', ')}. Rename to "compatibilityProfile".`, VirtualRouterErrorCode.CONFIG_ERROR);
736
- }
737
- const normalizedId = providerId.trim().toLowerCase();
738
- const providerType = String(provider.providerType ?? provider.type ?? provider.protocol ?? '').toLowerCase();
739
- if (normalizedId === 'antigravity' ||
740
- normalizedId === 'gemini-cli' ||
741
- providerType.includes('antigravity') ||
742
- providerType.includes('gemini-cli')) {
743
- return 'chat:gemini-cli';
744
- }
745
- return 'compat:passthrough';
746
- }
747
- function normalizeProcessMode(value) {
748
- if (typeof value !== 'string') {
749
- return 'chat';
750
- }
751
- const normalized = value.trim().toLowerCase();
752
- if (normalized === 'passthrough') {
753
- return 'passthrough';
754
- }
755
- return 'chat';
756
- }
757
- function normalizeContextRouting(input) {
758
- if (!input || typeof input !== 'object') {
759
- return { ...DEFAULT_CONTEXT_ROUTING };
760
- }
761
- const record = input;
762
- const warnCandidate = coerceRatio(record.warnRatio) ??
763
- coerceRatio(record?.warn_ratio);
764
- const hardLimitCandidate = coerceBoolean(record.hardLimit) ??
765
- coerceBoolean(record?.hard_limit);
766
- const warnRatio = clampWarnRatio(warnCandidate ?? DEFAULT_CONTEXT_ROUTING.warnRatio);
767
- const hardLimit = typeof hardLimitCandidate === 'boolean' ? hardLimitCandidate : DEFAULT_CONTEXT_ROUTING.hardLimit;
768
- return {
769
- warnRatio,
770
- hardLimit
771
- };
772
- }
773
- function validateWebSearchRouting(webSearch, routingSource) {
774
- if (!webSearch) {
775
- return;
776
- }
777
- // webSearch 与 search 路由独立配置:只允许从 routing.web_search 中解析搜索后端。
778
- const routePools = routingSource['web_search'];
779
- if (!Array.isArray(routePools) || !routePools.length) {
780
- throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
781
- }
782
- const targets = new Set();
783
- for (const pool of routePools) {
784
- if (!pool || !Array.isArray(pool.targets)) {
785
- continue;
786
- }
787
- for (const target of pool.targets) {
788
- if (typeof target === 'string' && target.trim()) {
789
- targets.add(target.trim());
790
- }
791
- }
792
- }
793
- for (const engine of webSearch.engines) {
794
- if (!targets.has(engine.providerKey)) {
795
- throw new VirtualRouterError(`Virtual Router webSearch engine "${engine.id}" references providerKey "${engine.providerKey}" which is not present in routing.web_search/search`, VirtualRouterErrorCode.CONFIG_ERROR);
796
- }
797
- }
798
- }
799
- function normalizeWebSearch(input, routingSource) {
800
- if (!input || typeof input !== 'object') {
801
- return undefined;
802
- }
803
- const record = input;
804
- const enginesNode = Array.isArray(record.engines) ? record.engines : [];
805
- const engines = [];
806
- for (const raw of enginesNode) {
807
- if (!raw || typeof raw !== 'object') {
808
- continue;
809
- }
810
- const node = raw;
811
- const idRaw = node.id;
812
- const providerKeyRaw = node.providerKey ?? node.provider ?? node.target;
813
- const id = typeof idRaw === 'string' && idRaw.trim()
814
- ? idRaw.trim()
815
- : undefined;
816
- const providerKey = typeof providerKeyRaw === 'string' && providerKeyRaw.trim()
817
- ? providerKeyRaw.trim()
818
- : undefined;
819
- if (!id || !providerKey) {
820
- continue;
821
- }
822
- const description = typeof node.description === 'string' && node.description.trim()
823
- ? node.description.trim()
824
- : undefined;
825
- const isDefault = node.default === true ||
826
- (typeof node.default === 'string' && node.default.trim().toLowerCase() === 'true');
827
- const serverToolsDisabled = node.serverToolsDisabled === true ||
828
- (typeof node.serverToolsDisabled === 'string' &&
829
- node.serverToolsDisabled.trim().toLowerCase() === 'true') ||
830
- (node.serverTools &&
831
- typeof node.serverTools === 'object' &&
832
- node.serverTools.enabled === false);
833
- // Deduplicate by id; first wins, subsequent are ignored.
834
- if (engines.some((engine) => engine.id === id)) {
835
- continue;
836
- }
837
- engines.push({
838
- id,
839
- providerKey,
840
- description,
841
- default: isDefault,
842
- ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
843
- });
844
- }
845
- if (!engines.length) {
846
- return undefined;
847
- }
848
- let injectPolicy;
849
- let force;
850
- const rawPolicy = record.injectPolicy ?? record?.inject_policy;
851
- if (typeof rawPolicy === 'string') {
852
- const normalized = rawPolicy.trim().toLowerCase();
853
- if (normalized === 'always' || normalized === 'selective') {
854
- injectPolicy = normalized;
855
- }
856
- }
857
- if (record.force === true ||
858
- (typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true')) {
859
- force = true;
860
- }
861
- else {
862
- // 仅从 routing.web_search 推导 force 标记,避免与 routing.search 的行为混淆。
863
- const webSearchPools = routingSource['web_search'] ?? [];
864
- if (Array.isArray(webSearchPools) && webSearchPools.some((pool) => pool.force)) {
865
- force = true;
866
- }
867
- }
868
- return {
869
- engines,
870
- injectPolicy: injectPolicy ?? 'selective',
871
- ...(force ? { force } : {})
872
- };
873
- }
874
- function normalizeExecCommandGuard(input) {
875
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
876
- return undefined;
877
- }
878
- const record = input;
879
- const enabledRaw = record.enabled;
880
- const enabled = enabledRaw === true ||
881
- (typeof enabledRaw === 'string' && enabledRaw.trim().toLowerCase() === 'true') ||
882
- (typeof enabledRaw === 'number' && enabledRaw === 1);
883
- if (!enabled) {
884
- return undefined;
885
- }
886
- const policyFileRaw = record.policyFile ?? record?.policy_file;
887
- const policyFile = typeof policyFileRaw === 'string' && policyFileRaw.trim().length ? policyFileRaw.trim() : undefined;
888
- return {
889
- enabled: true,
890
- ...(policyFile ? { policyFile } : {})
891
- };
892
- }
893
- function extractProviderAuthEntries(providerId, raw) {
894
- const provider = asRecord(raw);
895
- const auth = asRecord(provider.auth);
896
- const entries = [];
897
- const aliasSet = new Set();
898
- const baseTypeInfo = interpretAuthType(auth.type);
899
- const baseType = baseTypeInfo.type;
900
- const baseTypeSource = typeof auth.type === 'string' ? auth.type : undefined;
901
- const defaults = collectAuthDefaults(auth);
902
- const buildAuthCandidate = (typeHint, extras = {}) => {
903
- const source = typeof typeHint === 'string' && typeHint.trim()
904
- ? typeHint.trim()
905
- : baseTypeSource;
906
- const typeInfo = interpretAuthType(source ?? baseType);
907
- const rawType = typeInfo.raw ?? source ?? baseTypeSource;
908
- return {
909
- ...extras,
910
- type: typeInfo.type,
911
- rawType,
912
- oauthProviderId: extras?.oauthProviderId ?? typeInfo.oauthProviderId
913
- };
914
- };
915
- const pushEntry = (candidateAlias, authConfig) => {
916
- const alias = normalizeAlias(candidateAlias, aliasSet);
917
- const typeSource = authConfig?.rawType ??
918
- authConfig?.type ??
919
- baseTypeSource ??
920
- baseType;
921
- const typeInfo = interpretAuthType(typeSource);
922
- const entryType = typeInfo.type;
923
- const oauthProviderId = authConfig?.oauthProviderId ??
924
- typeInfo.oauthProviderId ??
925
- baseTypeInfo.oauthProviderId;
926
- if (entryType === 'oauth' && !oauthProviderId) {
927
- throw new VirtualRouterError(`Provider ${providerId} OAuth auth entries must declare provider-specific type (e.g. "qwen-oauth")`, VirtualRouterErrorCode.CONFIG_ERROR);
928
- }
929
- const normalized = {
930
- type: entryType,
931
- rawType: typeof typeSource === 'string' ? typeSource : undefined,
932
- oauthProviderId,
933
- value: readOptionalString(authConfig?.value ?? authConfig?.apiKey),
934
- secretRef: readOptionalString(authConfig?.secretRef)
935
- };
936
- normalized.tokenFile = readOptionalString(authConfig?.tokenFile);
937
- normalized.tokenUrl = readOptionalString(authConfig?.tokenUrl ?? authConfig?.token_url);
938
- normalized.deviceCodeUrl = readOptionalString(authConfig?.deviceCodeUrl ?? authConfig?.device_code_url);
939
- normalized.clientId = readOptionalString(authConfig?.clientId ?? authConfig?.client_id);
940
- normalized.clientSecret = readOptionalString(authConfig?.clientSecret ?? authConfig?.client_secret);
941
- normalized.authorizationUrl = readOptionalString(authConfig?.authorizationUrl ??
942
- authConfig?.authorization_url ??
943
- authConfig?.authUrl);
944
- normalized.userInfoUrl = readOptionalString(authConfig?.userInfoUrl ?? authConfig?.user_info_url);
945
- normalized.refreshUrl = readOptionalString(authConfig?.refreshUrl ?? authConfig?.refresh_url);
946
- normalized.scopes = normalizeScopeList(authConfig?.scopes ?? authConfig?.scope);
947
- normalized.secretRef ??= defaults.secretRef;
948
- normalized.tokenFile ??= defaults.tokenFile;
949
- normalized.tokenUrl ??= defaults.tokenUrl;
950
- normalized.deviceCodeUrl ??= defaults.deviceCodeUrl;
951
- normalized.clientId ??= defaults.clientId;
952
- normalized.clientSecret ??= defaults.clientSecret;
953
- normalized.authorizationUrl ??= defaults.authorizationUrl;
954
- normalized.userInfoUrl ??= defaults.userInfoUrl;
955
- normalized.refreshUrl ??= defaults.refreshUrl;
956
- normalized.scopes = mergeScopes(normalized.scopes, defaults.scopes);
957
- if (entryType === 'apiKey') {
958
- if (!normalized.secretRef && normalized.value) {
959
- normalized.secretRef = `${providerId}.${alias}`;
960
- }
961
- else if (!normalized.secretRef) {
962
- normalized.secretRef = `${providerId}.${alias}`;
963
- }
964
- }
965
- entries.push({ keyAlias: alias, auth: normalized });
966
- aliasSet.add(alias);
967
- };
968
- const fromRecord = (record) => {
969
- const data = asRecord(record);
970
- const alias = readOptionalString(data.alias);
971
- const typeValue = data.type ?? baseTypeSource ?? baseType;
972
- pushEntry(alias, buildAuthCandidate(typeValue, {
973
- value: data.value ?? data.apiKey,
974
- secretRef: data.secretRef,
975
- tokenFile: data.tokenFile,
976
- tokenUrl: data.tokenUrl ?? data.token_url,
977
- deviceCodeUrl: data.deviceCodeUrl ?? data.device_code_url,
978
- clientId: data.clientId ?? data.client_id,
979
- clientSecret: data.clientSecret ?? data.client_secret,
980
- authorizationUrl: data.authorizationUrl ??
981
- data.authorization_url ??
982
- data.authUrl,
983
- userInfoUrl: data.userInfoUrl ?? data.user_info_url,
984
- refreshUrl: data.refreshUrl ?? data.refresh_url,
985
- scopes: data.scopes ?? data.scope
986
- }));
987
- };
988
- if (Array.isArray(auth.entries)) {
989
- for (const entry of auth.entries) {
990
- fromRecord(entry);
991
- }
992
- }
993
- if (Array.isArray(auth.keys)) {
994
- for (const entry of auth.keys) {
995
- fromRecord(entry);
996
- }
997
- }
998
- else {
999
- const keysObject = asRecord(auth.keys);
1000
- for (const [alias, entry] of Object.entries(keysObject)) {
1001
- if (entry && typeof entry === 'object') {
1002
- fromRecord({ alias, ...entry });
1003
- }
1004
- else if (typeof entry === 'string') {
1005
- pushEntry(alias, buildAuthCandidate(baseTypeSource, { value: entry }));
1006
- }
1007
- }
1008
- }
1009
- const apiKeyField = provider.apiKey ?? provider.apiKeys ?? auth.apiKey;
1010
- if (Array.isArray(apiKeyField)) {
1011
- for (const item of apiKeyField) {
1012
- if (typeof item === 'string' && item.trim()) {
1013
- pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: item.trim() }));
1014
- }
1015
- else if (item && typeof item === 'object') {
1016
- fromRecord(item);
1017
- }
1018
- }
1019
- }
1020
- else if (typeof apiKeyField === 'string' && apiKeyField.trim()) {
1021
- pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: apiKeyField.trim() }));
1022
- }
1023
- const hasExplicitEntries = entries.length > 0;
1024
- // 自动多 token 扫描:仅在未显式声明多 key、且为受支持的 OAuth 提供方时触发
1025
- if (baseType === 'oauth' && !hasExplicitEntries) {
1026
- const scanCandidates = new Set();
1027
- const pushCandidate = (value) => {
1028
- if (typeof value === 'string' && value.trim()) {
1029
- scanCandidates.add(value.trim().toLowerCase());
1030
- }
1031
- };
1032
- pushCandidate(auth?.oauthProviderId);
1033
- pushCandidate(baseTypeInfo.oauthProviderId);
1034
- pushCandidate(providerId);
1035
- for (const candidate of scanCandidates) {
1036
- if (!MULTI_TOKEN_OAUTH_PROVIDERS.has(candidate)) {
1037
- continue;
1038
- }
1039
- const tokenFiles = scanOAuthTokenFiles(candidate);
1040
- if (!tokenFiles.length) {
1041
- continue;
1042
- }
1043
- const baseTypeAlias = baseTypeInfo.oauthProviderId?.toLowerCase();
1044
- for (const match of tokenFiles) {
1045
- const alias = match.alias && match.alias !== 'default'
1046
- ? `${match.sequence}-${match.alias}`
1047
- : String(match.sequence);
1048
- const typeHint = baseTypeSource && baseTypeAlias === candidate
1049
- ? baseTypeSource
1050
- : `${candidate}-oauth`;
1051
- const authConfig = {
1052
- ...defaults,
1053
- type: typeHint,
1054
- tokenFile: match.filePath,
1055
- oauthProviderId: candidate
1056
- };
1057
- pushEntry(alias, authConfig);
1058
- }
1059
- }
1060
- }
1061
- // DeepSeek account 多 token 自动扫描:
1062
- // - 仅在未显式声明多 key 时触发;
1063
- // - 从 ~/.routecodex/auth/deepseek-account-*.json 自动发现多个账号;
1064
- // - alias 直接取文件名后缀,路由目标 deepseek-web.<model> 会自动扩展到所有 alias。
1065
- const baseRawType = String(baseTypeInfo.raw ?? baseTypeSource ?? '').trim().toLowerCase();
1066
- if (baseType === 'apiKey' && baseRawType === 'deepseek-account' && !hasExplicitEntries) {
1067
- const tokenFiles = scanDeepSeekAccountTokenFiles();
1068
- for (const match of tokenFiles) {
1069
- const authConfig = {
1070
- ...defaults,
1071
- type: baseTypeSource ?? 'deepseek-account',
1072
- tokenFile: match.filePath
1073
- };
1074
- pushEntry(match.alias, authConfig);
1075
- }
1076
- }
1077
- if (!entries.length) {
1078
- const fallbackExtras = {
1079
- value: readOptionalString(auth.value),
1080
- secretRef: readOptionalString(auth.secretRef),
1081
- tokenFile: readOptionalString(auth.tokenFile ?? auth.file),
1082
- tokenUrl: readOptionalString(auth.tokenUrl ?? auth.token_url),
1083
- deviceCodeUrl: readOptionalString(auth.deviceCodeUrl ?? auth.device_code_url),
1084
- clientId: readOptionalString(auth.clientId ?? auth.client_id),
1085
- clientSecret: readOptionalString(auth.clientSecret ?? auth.client_secret),
1086
- authorizationUrl: readOptionalString(auth?.authorizationUrl ?? auth?.authorization_url ?? auth?.authUrl),
1087
- userInfoUrl: readOptionalString(auth?.userInfoUrl ?? auth?.user_info_url),
1088
- refreshUrl: readOptionalString(auth?.refreshUrl ?? auth?.refresh_url),
1089
- scopes: normalizeScopeList(auth?.scopes ?? auth?.scope),
1090
- cookieFile: readOptionalString(auth?.cookieFile)
1091
- };
1092
- const fallbackHasData = Boolean(fallbackExtras.value ||
1093
- fallbackExtras.secretRef ||
1094
- fallbackExtras.tokenFile ||
1095
- fallbackExtras.tokenUrl ||
1096
- fallbackExtras.deviceCodeUrl ||
1097
- fallbackExtras.clientId ||
1098
- fallbackExtras.clientSecret ||
1099
- fallbackExtras.cookieFile ||
1100
- (fallbackExtras.scopes &&
1101
- Array.isArray(fallbackExtras.scopes) &&
1102
- fallbackExtras.scopes.length));
1103
- if (fallbackHasData) {
1104
- pushEntry(undefined, buildAuthCandidate(baseTypeSource, fallbackExtras));
1105
- }
1106
- }
1107
- // Allow explicit apiKey auth with empty value (local no-auth providers like LM Studio).
1108
- // If the config declares an auth node (even with an empty apiKey), treat it as an intentional no-auth setup.
1109
- if (!entries.length && baseType === 'apiKey') {
1110
- const authDeclared = Object.prototype.hasOwnProperty.call(provider, 'auth') ||
1111
- Object.prototype.hasOwnProperty.call(provider, 'apiKey') ||
1112
- Object.prototype.hasOwnProperty.call(provider, 'apiKeys') ||
1113
- Object.prototype.hasOwnProperty.call(provider, 'authType');
1114
- if (authDeclared) {
1115
- pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: '' }));
1116
- }
1117
- }
1118
- if (!entries.length) {
1119
- throw new VirtualRouterError(`Provider ${providerId} is missing auth configuration`, VirtualRouterErrorCode.CONFIG_ERROR);
1120
- }
1121
- return entries;
1122
- }
1123
- function parseRouteEntry(entry, aliasIndex) {
1124
- const value = typeof entry === 'string' ? entry.trim() : '';
1125
- if (!value)
1126
- return null;
1127
- const firstDot = value.indexOf('.');
1128
- if (firstDot <= 0 || firstDot === value.length - 1)
1129
- return null;
1130
- const providerId = value.slice(0, firstDot);
1131
- const remainder = value.slice(firstDot + 1);
1132
- const aliases = aliasIndex.get(providerId);
1133
- if (aliases && aliases.length) {
1134
- const secondDot = remainder.indexOf('.');
1135
- if (secondDot > 0 && secondDot < remainder.length - 1) {
1136
- const aliasCandidate = remainder.slice(0, secondDot);
1137
- if (aliases.includes(aliasCandidate)) {
1138
- const parsed = splitModelPriority(remainder.slice(secondDot + 1));
1139
- return {
1140
- providerId,
1141
- keyAlias: aliasCandidate,
1142
- modelId: parsed.modelId,
1143
- priority: parsed.priority
1144
- };
1145
- }
1146
- }
1147
- }
1148
- const parsed = splitModelPriority(remainder);
1149
- return { providerId, modelId: parsed.modelId, priority: parsed.priority };
1150
- }
1151
- function splitModelPriority(raw) {
1152
- const value = typeof raw === 'string' ? raw.trim() : '';
1153
- if (!value) {
1154
- return { modelId: value, priority: 100 };
1155
- }
1156
- const match = value.match(/^(.*):(\d+)$/);
1157
- if (!match) {
1158
- return { modelId: value, priority: 100 };
1159
- }
1160
- const modelId = (match[1] ?? '').trim();
1161
- const priorityRaw = (match[2] ?? '').trim();
1162
- const parsed = Number(priorityRaw);
1163
- if (!modelId) {
1164
- return { modelId: value, priority: 100 };
1165
- }
1166
- if (!Number.isFinite(parsed)) {
1167
- return { modelId, priority: 100 };
1168
- }
1169
- return { modelId, priority: parsed };
1170
- }
1171
- function parseTargetKey(targetKey) {
1172
- const value = typeof targetKey === 'string' ? targetKey.trim() : '';
1173
- if (!value)
1174
- return null;
1175
- const firstDot = value.indexOf('.');
1176
- if (firstDot <= 0 || firstDot === value.length - 1)
1177
- return null;
1178
- const providerId = value.slice(0, firstDot);
1179
- const remainder = value.slice(firstDot + 1);
1180
- const secondDot = remainder.indexOf('.');
1181
- if (secondDot <= 0 || secondDot === remainder.length - 1)
1182
- return null;
1183
- return {
1184
- providerId,
1185
- keyAlias: remainder.slice(0, secondDot),
1186
- modelId: remainder.slice(secondDot + 1)
1187
- };
1188
- }
1189
- function detectProviderType(provider) {
1190
- const raw = (provider.providerType || provider.protocol || provider.type || '').toString().toLowerCase();
1191
- const id = (provider.providerId || provider.id || '').toString().toLowerCase();
1192
- const match = (value, keyword) => value.includes(keyword);
1193
- const source = `${raw}|${id}`;
1194
- const normalized = (src) => (src && src.trim() ? src.trim() : '');
1195
- const lexicon = normalized(source);
1196
- if (!lexicon)
1197
- return 'openai';
1198
- if (match(lexicon, 'anthropic') || match(lexicon, 'claude'))
1199
- return 'anthropic';
1200
- if (match(lexicon, 'responses'))
1201
- return 'responses';
1202
- if (match(lexicon, 'gemini'))
1203
- return 'gemini';
1204
- if (match(lexicon, 'iflow'))
1205
- return 'iflow';
1206
- if (match(lexicon, 'qwen'))
1207
- return 'qwen';
1208
- if (match(lexicon, 'glm'))
1209
- return 'glm';
1210
- if (match(lexicon, 'lmstudio'))
1211
- return 'lmstudio';
1212
- return raw || 'openai';
1213
- }
1214
- function mapOutboundProfile(providerType) {
1215
- const value = providerType.toLowerCase();
1216
- if (value === 'anthropic')
1217
- return 'anthropic-messages';
1218
- if (value === 'responses')
1219
- return 'openai-responses';
1220
- if (value === 'gemini')
1221
- return 'gemini-chat';
1222
- return 'openai-chat';
1223
- }
1224
- function collectAuthDefaults(auth) {
1225
- return {
1226
- secretRef: readOptionalString(auth.secretRef) ?? readOptionalString(auth?.file),
1227
- tokenFile: readOptionalString(auth.tokenFile) ?? readOptionalString(auth?.file),
1228
- tokenUrl: readOptionalString(auth.tokenUrl ?? auth.token_url),
1229
- deviceCodeUrl: readOptionalString(auth.deviceCodeUrl ?? auth.device_code_url),
1230
- clientId: readOptionalString(auth.clientId ?? auth.client_id),
1231
- clientSecret: readOptionalString(auth.clientSecret ?? auth.client_secret),
1232
- authorizationUrl: readOptionalString(auth?.authorizationUrl ?? auth?.authorization_url ?? auth?.authUrl),
1233
- userInfoUrl: readOptionalString(auth?.userInfoUrl ?? auth?.user_info_url),
1234
- refreshUrl: readOptionalString(auth?.refreshUrl ?? auth?.refresh_url),
1235
- scopes: normalizeScopeList(auth?.scopes ?? auth?.scope)
1236
- };
1237
- }
1238
- function readOptionalString(value) {
1239
- if (typeof value !== 'string') {
1240
- return undefined;
1241
- }
1242
- const trimmed = value.trim();
1243
- return trimmed ? trimmed : undefined;
1244
- }
1245
- function normalizeScopeList(value) {
1246
- if (Array.isArray(value)) {
1247
- const normalized = value
1248
- .map((item) => readOptionalString(item))
1249
- .filter((item) => typeof item === 'string');
1250
- return normalized.length ? normalized : undefined;
1251
- }
1252
- if (typeof value === 'string' && value.trim()) {
1253
- const normalized = value
1254
- .split(/[,\s]+/)
1255
- .map((item) => item.trim())
1256
- .filter(Boolean);
1257
- return normalized.length ? normalized : undefined;
1258
- }
1259
- return undefined;
1260
- }
1261
- function mergeScopes(primary, fallback) {
1262
- if ((!primary || !primary.length) && (!fallback || !fallback.length)) {
1263
- return undefined;
1264
- }
1265
- const merged = new Set();
1266
- for (const scope of primary ?? []) {
1267
- if (scope.trim())
1268
- merged.add(scope.trim());
1269
- }
1270
- for (const scope of fallback ?? []) {
1271
- if (scope.trim())
1272
- merged.add(scope.trim());
1273
- }
1274
- return merged.size ? Array.from(merged) : undefined;
1275
- }
1276
- const MULTI_TOKEN_OAUTH_PROVIDERS = new Set(['iflow', 'qwen', 'gemini-cli', 'antigravity']);
1277
- function interpretAuthType(value) {
1278
- if (typeof value !== 'string') {
1279
- return { type: 'apiKey' };
1280
- }
1281
- const trimmed = value.trim();
1282
- if (!trimmed) {
1283
- return { type: 'apiKey' };
1284
- }
1285
- const lower = trimmed.toLowerCase();
1286
- if (lower === 'apikey' || lower === 'api-key') {
1287
- return { type: 'apiKey', raw: trimmed };
1288
- }
1289
- if (lower === 'oauth') {
1290
- return { type: 'oauth', raw: trimmed };
1291
- }
1292
- const match = lower.match(/^([a-z0-9._-]+)-oauth$/);
1293
- if (match) {
1294
- return { type: 'oauth', oauthProviderId: match[1], raw: trimmed };
1295
- }
1296
- if (lower.includes('oauth')) {
1297
- return { type: 'oauth', raw: trimmed };
1298
- }
1299
- return { type: 'apiKey', raw: trimmed };
1300
- }
1301
181
  function normalizeLoadBalancing(input) {
1302
182
  if (!input || typeof input !== 'object')
1303
183
  return undefined;
@@ -1372,7 +252,8 @@ function normalizeAliasSelection(raw) {
1372
252
  const defaultStrategy = coerceAliasSelectionStrategy(record.defaultStrategy);
1373
253
  const sessionLeaseCooldownMs = typeof record.sessionLeaseCooldownMs === 'number' && Number.isFinite(record.sessionLeaseCooldownMs)
1374
254
  ? Math.max(0, Math.floor(record.sessionLeaseCooldownMs))
1375
- : typeof record.sessionLease_cooldown_ms === 'number' && Number.isFinite(record.sessionLease_cooldown_ms)
255
+ : typeof record.sessionLease_cooldown_ms === 'number' &&
256
+ Number.isFinite(record.sessionLease_cooldown_ms)
1376
257
  ? Math.max(0, Math.floor(record.sessionLease_cooldown_ms))
1377
258
  : undefined;
1378
259
  const antigravitySessionBindingRaw = typeof record.antigravitySessionBinding === 'string'
@@ -1417,128 +298,3 @@ function coerceAliasSelectionStrategy(value) {
1417
298
  }
1418
299
  return undefined;
1419
300
  }
1420
- function coerceRatio(value) {
1421
- if (typeof value === 'number' && Number.isFinite(value)) {
1422
- return value;
1423
- }
1424
- if (typeof value === 'string') {
1425
- const trimmed = value.trim();
1426
- if (!trimmed) {
1427
- return undefined;
1428
- }
1429
- const parsed = Number(trimmed);
1430
- if (Number.isFinite(parsed)) {
1431
- return parsed;
1432
- }
1433
- }
1434
- return undefined;
1435
- }
1436
- function clampWarnRatio(value) {
1437
- if (!Number.isFinite(value)) {
1438
- return DEFAULT_CONTEXT_ROUTING.warnRatio;
1439
- }
1440
- const clamped = Math.max(0.1, Math.min(value, 0.99));
1441
- return Number.isFinite(clamped) ? clamped : DEFAULT_CONTEXT_ROUTING.warnRatio;
1442
- }
1443
- function coerceBoolean(value) {
1444
- if (typeof value === 'boolean') {
1445
- return value;
1446
- }
1447
- if (typeof value === 'string') {
1448
- const normalized = value.trim().toLowerCase();
1449
- if (!normalized) {
1450
- return undefined;
1451
- }
1452
- if (['true', '1', 'yes', 'y'].includes(normalized)) {
1453
- return true;
1454
- }
1455
- if (['false', '0', 'no', 'n'].includes(normalized)) {
1456
- return false;
1457
- }
1458
- }
1459
- return undefined;
1460
- }
1461
- function normalizeHealth(input) {
1462
- if (!input || typeof input !== 'object')
1463
- return undefined;
1464
- const record = input;
1465
- const failureThreshold = typeof record.failureThreshold === 'number' ? record.failureThreshold : undefined;
1466
- const cooldownMs = typeof record.cooldownMs === 'number' ? record.cooldownMs : undefined;
1467
- const fatalCooldownMs = typeof record.fatalCooldownMs === 'number' ? record.fatalCooldownMs : undefined;
1468
- if (typeof failureThreshold !== 'number' || typeof cooldownMs !== 'number') {
1469
- return undefined;
1470
- }
1471
- return fatalCooldownMs !== undefined
1472
- ? { failureThreshold, cooldownMs, fatalCooldownMs }
1473
- : { failureThreshold, cooldownMs };
1474
- }
1475
- function readContextTokens(record) {
1476
- if (!record) {
1477
- return undefined;
1478
- }
1479
- const keys = [
1480
- 'maxContextTokens',
1481
- 'max_context_tokens',
1482
- 'maxContext',
1483
- 'max_context',
1484
- 'contextTokens',
1485
- 'context_tokens'
1486
- ];
1487
- for (const key of keys) {
1488
- const value = record[key];
1489
- const parsed = normalizePositiveInteger(value);
1490
- if (parsed) {
1491
- return parsed;
1492
- }
1493
- }
1494
- return undefined;
1495
- }
1496
- function normalizePositiveInteger(value) {
1497
- if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
1498
- return Math.floor(value);
1499
- }
1500
- if (typeof value === 'string') {
1501
- const trimmed = value.trim();
1502
- if (!trimmed) {
1503
- return undefined;
1504
- }
1505
- const parsed = Number(trimmed);
1506
- if (Number.isFinite(parsed) && parsed > 0) {
1507
- return Math.floor(parsed);
1508
- }
1509
- }
1510
- return undefined;
1511
- }
1512
- function normalizeHeaders(input) {
1513
- if (!input || typeof input !== 'object') {
1514
- return undefined;
1515
- }
1516
- const entries = {};
1517
- for (const [key, value] of Object.entries(input)) {
1518
- if (typeof value === 'string') {
1519
- entries[key] = value;
1520
- }
1521
- }
1522
- return Object.keys(entries).length ? entries : undefined;
1523
- }
1524
- function asRecord(value) {
1525
- return (value && typeof value === 'object' ? value : {});
1526
- }
1527
- function normalizeAlias(candidate, existing) {
1528
- const base = candidate && candidate.trim() ? candidate.trim() : `key${existing.size + 1}`;
1529
- let alias = base;
1530
- let i = 1;
1531
- while (existing.has(alias)) {
1532
- alias = `${base}_${i}`;
1533
- i += 1;
1534
- }
1535
- return alias;
1536
- }
1537
- function pushUnique(list, value) {
1538
- if (!list.includes(value)) {
1539
- list.push(value);
1540
- }
1541
- }
1542
- function buildRuntimeKey(providerId, keyAlias) {
1543
- return `${providerId}.${keyAlias}`;
1544
- }