@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
@@ -107,6 +107,112 @@ function injectNestedApplyPatchPolicyNotice(messages, rewriteCount) {
107
107
  content: buildNestedApplyPatchPolicyNotice(rewriteCount)
108
108
  });
109
109
  }
110
+ function shellSingleQuote(text) {
111
+ return `'${String(text || '').replace(/'/g, `'\\''`)}'`;
112
+ }
113
+ function buildExecCommandGuardScript(reason, message) {
114
+ const fallback = 'blocked by exec_command guard policy.';
115
+ const detail = reason === 'forbidden_git_reset_hard'
116
+ ? 'blocked by exec_command guard: git reset --hard is forbidden. Use git reset --mixed REF or git restore --source REF -- FILE.'
117
+ : reason === 'forbidden_git_checkout_scope'
118
+ ? 'blocked by exec_command guard: git checkout is allowed only for a single file. Use git checkout -- FILE or git checkout REF -- FILE.'
119
+ : message && message.trim()
120
+ ? `blocked by exec_command guard: ${message.trim()}`
121
+ : fallback;
122
+ const compact = detail.replace(/\s+/g, ' ').trim() || fallback;
123
+ return `bash -lc "printf '%s\\n' ${shellSingleQuote(compact)} >&2; exit 2"`;
124
+ }
125
+ function buildBlockedExecCommandArgs(rawArgs, reason, message) {
126
+ let parsed = {};
127
+ try {
128
+ const repaired = repairArgumentsToString(rawArgs);
129
+ try {
130
+ parsed = JSON.parse(repaired);
131
+ }
132
+ catch {
133
+ parsed = parseLenient(repaired);
134
+ }
135
+ }
136
+ catch {
137
+ parsed = {};
138
+ }
139
+ const out = {};
140
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
141
+ const workdir = typeof parsed.workdir === 'string'
142
+ ? parsed.workdir
143
+ : typeof parsed.cwd === 'string'
144
+ ? parsed.cwd
145
+ : undefined;
146
+ if (workdir && workdir.trim().length > 0) {
147
+ out.workdir = workdir.trim();
148
+ }
149
+ }
150
+ out.cmd = buildExecCommandGuardScript(reason, message);
151
+ try {
152
+ return JSON.stringify(out);
153
+ }
154
+ catch {
155
+ return JSON.stringify({
156
+ cmd: `bash -lc 'printf "%s\\n" "blocked by exec_command guard policy." >&2; exit 2'`
157
+ });
158
+ }
159
+ }
160
+ const EXEC_COMMAND_NAME_AS_COMMAND_PATTERN = /^(?:rg|wc|cat|ls|find|grep|git|sed|head|tail|awk|bash|sh|zsh|node|npm|pnpm|yarn|bd|echo|cp|mv|rm|mkdir|python|python3|perl|ruby)\b/i;
161
+ function repairCommandNameAsExecToolCall(fn) {
162
+ try {
163
+ if (!fn)
164
+ return false;
165
+ const rawName = typeof fn.name === 'string' ? String(fn.name).trim() : '';
166
+ if (!rawName)
167
+ return false;
168
+ const lowered = rawName.toLowerCase();
169
+ if (lowered === 'exec_command' || lowered === 'shell_command' || lowered === 'shell' || lowered === 'bash') {
170
+ return false;
171
+ }
172
+ // Malformed shape seen in the wild: command string is put into function.name.
173
+ if (!EXEC_COMMAND_NAME_AS_COMMAND_PATTERN.test(rawName)) {
174
+ return false;
175
+ }
176
+ const repaired = repairArgumentsToString(fn.arguments);
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(repaired);
180
+ }
181
+ catch {
182
+ parsed = parseLenient(repaired);
183
+ }
184
+ const argsObj = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
185
+ ? { ...parsed }
186
+ : {};
187
+ const existingCmd = typeof argsObj.cmd === 'string' && String(argsObj.cmd).trim().length
188
+ ? String(argsObj.cmd).trim()
189
+ : typeof argsObj.command === 'string' && String(argsObj.command).trim().length
190
+ ? String(argsObj.command).trim()
191
+ : '';
192
+ const cmd = existingCmd || rawName;
193
+ argsObj.cmd = cmd;
194
+ argsObj.command = cmd;
195
+ if (typeof argsObj.cwd === 'string' && (!argsObj.workdir || typeof argsObj.workdir !== 'string')) {
196
+ argsObj.workdir = String(argsObj.cwd);
197
+ }
198
+ const validation = validateToolCall('exec_command', JSON.stringify(argsObj));
199
+ if (validation.ok && typeof validation.normalizedArgs === 'string') {
200
+ fn.arguments = validation.normalizedArgs;
201
+ }
202
+ else {
203
+ const fallback = { cmd, command: cmd };
204
+ if (typeof argsObj.workdir === 'string' && String(argsObj.workdir).trim().length > 0) {
205
+ fallback.workdir = String(argsObj.workdir).trim();
206
+ }
207
+ fn.arguments = JSON.stringify(fallback);
208
+ }
209
+ fn.name = 'exec_command';
210
+ return true;
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ }
110
216
  function tryWriteSnapshot(options, stage, data) {
111
217
  try {
112
218
  // 仅在 verbose 级别保存快照(环境变量)
@@ -332,6 +438,7 @@ export function normalizeApplyPatchToolCallsOnResponse(chat) {
332
438
  for (const tc of tcs) {
333
439
  try {
334
440
  const fn = tc && tc.function ? tc.function : undefined;
441
+ repairCommandNameAsExecToolCall(fn);
335
442
  rewriteExecCommandApplyPatchCall(fn);
336
443
  const name = typeof fn?.name === 'string' ? String(fn.name).trim().toLowerCase() : '';
337
444
  if (name !== 'apply_patch')
@@ -409,6 +516,7 @@ function normalizeSpecialToolCallsOnRequest(request) {
409
516
  for (const tc of tcs) {
410
517
  try {
411
518
  const fn = tc && tc.function ? tc.function : undefined;
519
+ repairCommandNameAsExecToolCall(fn);
412
520
  if (rewriteExecCommandApplyPatchCall(fn)) {
413
521
  rewrittenNestedApplyPatchCount += 1;
414
522
  }
@@ -447,21 +555,30 @@ function normalizeSpecialToolCallsOnRequest(request) {
447
555
  // exec_command 兼容:TOON / map / string 一律收敛为 { cmd, command, workdir, ... }
448
556
  if (name === 'exec_command') {
449
557
  const argsStr = repairArgumentsToString(rawArgs);
450
- let parsed;
451
- try {
452
- parsed = JSON.parse(argsStr);
558
+ const validation = validateToolCall('exec_command', argsStr);
559
+ if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
560
+ fn.arguments = validation.normalizedArgs;
453
561
  }
454
- catch {
455
- parsed = parseLenient(argsStr);
562
+ else if (validation && !validation.ok) {
563
+ fn.arguments = buildBlockedExecCommandArgs(rawArgs, validation.reason, validation.message);
456
564
  }
457
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
458
- const normalized = normalizeExecCommandArgs(parsed);
459
- const next = normalized.ok ? normalized.normalized : parsed;
565
+ else {
566
+ let parsed;
460
567
  try {
461
- fn.arguments = JSON.stringify(next ?? {});
568
+ parsed = JSON.parse(argsStr);
462
569
  }
463
570
  catch {
464
- fn.arguments = '{}';
571
+ parsed = parseLenient(argsStr);
572
+ }
573
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
574
+ const normalized = normalizeExecCommandArgs(parsed);
575
+ const next = normalized.ok ? normalized.normalized : parsed;
576
+ try {
577
+ fn.arguments = JSON.stringify(next ?? {});
578
+ }
579
+ catch {
580
+ fn.arguments = '{}';
581
+ }
465
582
  }
466
583
  }
467
584
  }
@@ -568,6 +685,15 @@ function enhanceResponseToolArguments(chat) {
568
685
  }
569
686
  }
570
687
  }
688
+ else if (name === 'exec_command') {
689
+ const validation = validateToolCall('exec_command', repaired);
690
+ if (validation.ok && typeof validation.normalizedArgs === 'string') {
691
+ finalStr = validation.normalizedArgs;
692
+ }
693
+ else if (!validation.ok) {
694
+ finalStr = buildBlockedExecCommandArgs(repaired, validation.reason, validation.message);
695
+ }
696
+ }
571
697
  if (fn)
572
698
  fn.arguments = finalStr;
573
699
  }
@@ -2,10 +2,10 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import * as fsp from 'node:fs/promises';
4
4
  function mapEndpointToFolder(ep) {
5
- const e = String(ep || '').toLowerCase();
6
- if (e.includes('/v1/responses'))
5
+ const e = String(ep || '').trim().toLowerCase();
6
+ if (e.includes('/v1/responses') || e.includes('/responses.submit'))
7
7
  return 'openai-responses';
8
- if (e.includes('/v1/messages') || e.includes('anthropic'))
8
+ if (e.includes('/v1/messages'))
9
9
  return 'anthropic-messages';
10
10
  return 'openai-chat';
11
11
  }
@@ -0,0 +1,6 @@
1
+ import { type ProviderAuthConfig } from '../types.js';
2
+ export interface ProviderAuthEntry {
3
+ keyAlias: string;
4
+ auth: ProviderAuthConfig;
5
+ }
6
+ export declare function extractProviderAuthEntries(providerId: string, raw: unknown): ProviderAuthEntry[];
@@ -0,0 +1,288 @@
1
+ import { scanDeepSeekAccountTokenFiles, scanOAuthTokenFiles } from '../token-file-scanner.js';
2
+ import { VirtualRouterError, VirtualRouterErrorCode } from '../types.js';
3
+ import { asRecord, normalizeAlias, pushUnique, readOptionalString } from './utils.js';
4
+ const MULTI_TOKEN_OAUTH_PROVIDERS = new Set(['iflow', 'qwen', 'gemini-cli', 'antigravity']);
5
+ export function extractProviderAuthEntries(providerId, raw) {
6
+ const provider = asRecord(raw);
7
+ const auth = asRecord(provider.auth);
8
+ const entries = [];
9
+ const aliasSet = new Set();
10
+ const baseTypeInfo = interpretAuthType(auth.type);
11
+ const baseType = baseTypeInfo.type;
12
+ const baseTypeSource = typeof auth.type === 'string' ? auth.type : undefined;
13
+ const defaults = collectAuthDefaults(auth);
14
+ const buildAuthCandidate = (typeHint, extras = {}) => {
15
+ const source = typeof typeHint === 'string' && typeHint.trim() ? typeHint.trim() : baseTypeSource;
16
+ const typeInfo = interpretAuthType(source ?? baseType);
17
+ const rawType = typeInfo.raw ?? source ?? baseTypeSource;
18
+ return {
19
+ ...extras,
20
+ type: typeInfo.type,
21
+ rawType,
22
+ oauthProviderId: extras.oauthProviderId ?? typeInfo.oauthProviderId
23
+ };
24
+ };
25
+ const pushEntry = (candidateAlias, authConfig) => {
26
+ const alias = normalizeAlias(candidateAlias, aliasSet);
27
+ const typeSource = authConfig.rawType ?? authConfig.type ?? baseTypeSource ?? baseType;
28
+ const typeInfo = interpretAuthType(typeSource);
29
+ const entryType = typeInfo.type;
30
+ const oauthProviderId = authConfig.oauthProviderId ?? typeInfo.oauthProviderId ?? baseTypeInfo.oauthProviderId;
31
+ if (entryType === 'oauth' && !oauthProviderId) {
32
+ throw new VirtualRouterError(`Provider ${providerId} OAuth auth entries must declare provider-specific type (e.g. "qwen-oauth")`, VirtualRouterErrorCode.CONFIG_ERROR);
33
+ }
34
+ const normalized = {
35
+ type: entryType,
36
+ rawType: typeof typeSource === 'string' ? typeSource : undefined,
37
+ oauthProviderId,
38
+ value: readOptionalString(authConfig.value ?? authConfig.apiKey),
39
+ secretRef: readOptionalString(authConfig.secretRef)
40
+ };
41
+ normalized.tokenFile = readOptionalString(authConfig.tokenFile);
42
+ normalized.tokenUrl = readOptionalString(authConfig.tokenUrl ?? authConfig.token_url);
43
+ normalized.deviceCodeUrl = readOptionalString(authConfig.deviceCodeUrl ?? authConfig.device_code_url);
44
+ normalized.clientId = readOptionalString(authConfig.clientId ?? authConfig.client_id);
45
+ normalized.clientSecret = readOptionalString(authConfig.clientSecret ?? authConfig.client_secret);
46
+ normalized.authorizationUrl = readOptionalString(authConfig.authorizationUrl ??
47
+ authConfig.authorization_url ??
48
+ authConfig.authUrl);
49
+ normalized.userInfoUrl = readOptionalString(authConfig.userInfoUrl ?? authConfig.user_info_url);
50
+ normalized.refreshUrl = readOptionalString(authConfig.refreshUrl ?? authConfig.refresh_url);
51
+ normalized.scopes = normalizeScopeList(authConfig.scopes ?? authConfig.scope);
52
+ normalized.secretRef ??= defaults.secretRef;
53
+ normalized.tokenFile ??= defaults.tokenFile;
54
+ normalized.tokenUrl ??= defaults.tokenUrl;
55
+ normalized.deviceCodeUrl ??= defaults.deviceCodeUrl;
56
+ normalized.clientId ??= defaults.clientId;
57
+ normalized.clientSecret ??= defaults.clientSecret;
58
+ normalized.authorizationUrl ??= defaults.authorizationUrl;
59
+ normalized.userInfoUrl ??= defaults.userInfoUrl;
60
+ normalized.refreshUrl ??= defaults.refreshUrl;
61
+ normalized.scopes = mergeScopes(normalized.scopes, defaults.scopes);
62
+ if (entryType === 'apiKey' && !normalized.secretRef) {
63
+ normalized.secretRef = `${providerId}.${alias}`;
64
+ }
65
+ entries.push({ keyAlias: alias, auth: normalized });
66
+ aliasSet.add(alias);
67
+ };
68
+ const fromRecord = (record) => {
69
+ const data = asRecord(record);
70
+ const alias = readOptionalString(data.alias);
71
+ const typeValue = data.type ?? baseTypeSource ?? baseType;
72
+ pushEntry(alias, buildAuthCandidate(typeValue, {
73
+ value: data.value ?? data.apiKey,
74
+ secretRef: data.secretRef,
75
+ tokenFile: data.tokenFile,
76
+ tokenUrl: data.tokenUrl ?? data.token_url,
77
+ deviceCodeUrl: data.deviceCodeUrl ?? data.device_code_url,
78
+ clientId: data.clientId ?? data.client_id,
79
+ clientSecret: data.clientSecret ?? data.client_secret,
80
+ authorizationUrl: data.authorizationUrl ??
81
+ data.authorization_url ??
82
+ data.authUrl,
83
+ userInfoUrl: data.userInfoUrl ?? data.user_info_url,
84
+ refreshUrl: data.refreshUrl ?? data.refresh_url,
85
+ scopes: data.scopes ?? data.scope
86
+ }));
87
+ };
88
+ if (Array.isArray(auth.entries)) {
89
+ for (const entry of auth.entries) {
90
+ fromRecord(entry);
91
+ }
92
+ }
93
+ if (Array.isArray(auth.keys)) {
94
+ for (const entry of auth.keys) {
95
+ fromRecord(entry);
96
+ }
97
+ }
98
+ else {
99
+ const keysObject = asRecord(auth.keys);
100
+ for (const [alias, entry] of Object.entries(keysObject)) {
101
+ if (entry && typeof entry === 'object') {
102
+ fromRecord({ alias, ...entry });
103
+ }
104
+ else if (typeof entry === 'string') {
105
+ pushEntry(alias, buildAuthCandidate(baseTypeSource, { value: entry }));
106
+ }
107
+ }
108
+ }
109
+ const apiKeyField = provider.apiKey ?? provider.apiKeys ?? auth.apiKey;
110
+ if (Array.isArray(apiKeyField)) {
111
+ for (const item of apiKeyField) {
112
+ if (typeof item === 'string' && item.trim()) {
113
+ pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: item.trim() }));
114
+ }
115
+ else if (item && typeof item === 'object') {
116
+ fromRecord(item);
117
+ }
118
+ }
119
+ }
120
+ else if (typeof apiKeyField === 'string' && apiKeyField.trim()) {
121
+ pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: apiKeyField.trim() }));
122
+ }
123
+ const hasExplicitEntries = entries.length > 0;
124
+ if (baseType === 'oauth' && !hasExplicitEntries) {
125
+ const scanCandidates = new Set();
126
+ const pushCandidate = (value) => {
127
+ if (typeof value === 'string' && value.trim()) {
128
+ scanCandidates.add(value.trim().toLowerCase());
129
+ }
130
+ };
131
+ pushCandidate(auth.oauthProviderId);
132
+ pushCandidate(baseTypeInfo.oauthProviderId);
133
+ pushCandidate(providerId);
134
+ for (const candidate of scanCandidates) {
135
+ if (!MULTI_TOKEN_OAUTH_PROVIDERS.has(candidate)) {
136
+ continue;
137
+ }
138
+ const tokenFiles = scanOAuthTokenFiles(candidate);
139
+ if (!tokenFiles.length) {
140
+ continue;
141
+ }
142
+ const baseTypeAlias = baseTypeInfo.oauthProviderId?.toLowerCase();
143
+ for (const match of tokenFiles) {
144
+ const alias = match.alias && match.alias !== 'default'
145
+ ? `${match.sequence}-${match.alias}`
146
+ : String(match.sequence);
147
+ const typeHint = baseTypeSource && baseTypeAlias === candidate
148
+ ? baseTypeSource
149
+ : `${candidate}-oauth`;
150
+ const authConfig = {
151
+ ...defaults,
152
+ type: typeHint,
153
+ tokenFile: match.filePath,
154
+ oauthProviderId: candidate
155
+ };
156
+ pushEntry(alias, authConfig);
157
+ }
158
+ }
159
+ }
160
+ const baseRawType = String(baseTypeInfo.raw ?? baseTypeSource ?? '').trim().toLowerCase();
161
+ if (baseType === 'apiKey' && baseRawType === 'deepseek-account' && !hasExplicitEntries) {
162
+ const tokenFiles = scanDeepSeekAccountTokenFiles();
163
+ for (const match of tokenFiles) {
164
+ const authConfig = {
165
+ ...defaults,
166
+ type: baseTypeSource ?? 'deepseek-account',
167
+ tokenFile: match.filePath
168
+ };
169
+ pushEntry(match.alias, authConfig);
170
+ }
171
+ }
172
+ if (!entries.length) {
173
+ const fallbackExtras = {
174
+ value: readOptionalString(auth.value),
175
+ secretRef: readOptionalString(auth.secretRef),
176
+ tokenFile: readOptionalString(auth.tokenFile ?? auth.file),
177
+ tokenUrl: readOptionalString(auth.tokenUrl ?? auth.token_url),
178
+ deviceCodeUrl: readOptionalString(auth.deviceCodeUrl ?? auth.device_code_url),
179
+ clientId: readOptionalString(auth.clientId ?? auth.client_id),
180
+ clientSecret: readOptionalString(auth.clientSecret ?? auth.client_secret),
181
+ authorizationUrl: readOptionalString(auth.authorizationUrl ?? auth.authorization_url ?? auth.authUrl),
182
+ userInfoUrl: readOptionalString(auth.userInfoUrl ?? auth.user_info_url),
183
+ refreshUrl: readOptionalString(auth.refreshUrl ?? auth.refresh_url),
184
+ scopes: normalizeScopeList(auth.scopes ?? auth.scope),
185
+ cookieFile: readOptionalString(auth.cookieFile)
186
+ };
187
+ const fallbackHasData = Boolean(fallbackExtras.value ||
188
+ fallbackExtras.secretRef ||
189
+ fallbackExtras.tokenFile ||
190
+ fallbackExtras.tokenUrl ||
191
+ fallbackExtras.deviceCodeUrl ||
192
+ fallbackExtras.clientId ||
193
+ fallbackExtras.clientSecret ||
194
+ fallbackExtras.cookieFile ||
195
+ (fallbackExtras.scopes &&
196
+ Array.isArray(fallbackExtras.scopes) &&
197
+ fallbackExtras.scopes.length));
198
+ if (fallbackHasData) {
199
+ pushEntry(undefined, buildAuthCandidate(baseTypeSource, fallbackExtras));
200
+ }
201
+ }
202
+ if (!entries.length && baseType === 'apiKey') {
203
+ const authDeclared = Object.prototype.hasOwnProperty.call(provider, 'auth') ||
204
+ Object.prototype.hasOwnProperty.call(provider, 'apiKey') ||
205
+ Object.prototype.hasOwnProperty.call(provider, 'apiKeys') ||
206
+ Object.prototype.hasOwnProperty.call(provider, 'authType');
207
+ if (authDeclared) {
208
+ pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: '' }));
209
+ }
210
+ }
211
+ if (!entries.length) {
212
+ throw new VirtualRouterError(`Provider ${providerId} is missing auth configuration`, VirtualRouterErrorCode.CONFIG_ERROR);
213
+ }
214
+ return entries;
215
+ }
216
+ function collectAuthDefaults(auth) {
217
+ return {
218
+ secretRef: readOptionalString(auth.secretRef) ?? readOptionalString(auth.file),
219
+ tokenFile: readOptionalString(auth.tokenFile) ?? readOptionalString(auth.file),
220
+ tokenUrl: readOptionalString(auth.tokenUrl ?? auth.token_url),
221
+ deviceCodeUrl: readOptionalString(auth.deviceCodeUrl ?? auth.device_code_url),
222
+ clientId: readOptionalString(auth.clientId ?? auth.client_id),
223
+ clientSecret: readOptionalString(auth.clientSecret ?? auth.client_secret),
224
+ authorizationUrl: readOptionalString(auth.authorizationUrl ?? auth.authorization_url ?? auth.authUrl),
225
+ userInfoUrl: readOptionalString(auth.userInfoUrl ?? auth.user_info_url),
226
+ refreshUrl: readOptionalString(auth.refreshUrl ?? auth.refresh_url),
227
+ scopes: normalizeScopeList(auth.scopes ?? auth.scope)
228
+ };
229
+ }
230
+ function normalizeScopeList(value) {
231
+ if (Array.isArray(value)) {
232
+ const normalized = [];
233
+ for (const item of value) {
234
+ const str = readOptionalString(item);
235
+ if (str) {
236
+ pushUnique(normalized, str);
237
+ }
238
+ }
239
+ return normalized.length ? normalized : undefined;
240
+ }
241
+ if (typeof value === 'string' && value.trim()) {
242
+ const normalized = value
243
+ .split(/[,\s]+/)
244
+ .map((item) => item.trim())
245
+ .filter(Boolean);
246
+ return normalized.length ? normalized : undefined;
247
+ }
248
+ return undefined;
249
+ }
250
+ function mergeScopes(primary, fallback) {
251
+ if ((!primary || !primary.length) && (!fallback || !fallback.length)) {
252
+ return undefined;
253
+ }
254
+ const merged = new Set();
255
+ for (const scope of primary ?? []) {
256
+ if (scope.trim())
257
+ merged.add(scope.trim());
258
+ }
259
+ for (const scope of fallback ?? []) {
260
+ if (scope.trim())
261
+ merged.add(scope.trim());
262
+ }
263
+ return merged.size ? Array.from(merged) : undefined;
264
+ }
265
+ function interpretAuthType(value) {
266
+ if (typeof value !== 'string') {
267
+ return { type: 'apiKey' };
268
+ }
269
+ const trimmed = value.trim();
270
+ if (!trimmed) {
271
+ return { type: 'apiKey' };
272
+ }
273
+ const lower = trimmed.toLowerCase();
274
+ if (lower === 'apikey' || lower === 'api-key') {
275
+ return { type: 'apiKey', raw: trimmed };
276
+ }
277
+ if (lower === 'oauth') {
278
+ return { type: 'oauth', raw: trimmed };
279
+ }
280
+ const match = lower.match(/^([a-z0-9._-]+)-oauth$/);
281
+ if (match) {
282
+ return { type: 'oauth', oauthProviderId: match[1], raw: trimmed };
283
+ }
284
+ if (lower.includes('oauth')) {
285
+ return { type: 'oauth', raw: trimmed };
286
+ }
287
+ return { type: 'apiKey', raw: trimmed };
288
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Claude Code specific helpers for Virtual Router bootstrap.
3
+ */
4
+ export declare const CLAUDE_CODE_DEFAULT_USER_AGENT = "claude-cli/2.0.76 (external, cli)";
5
+ export declare const CLAUDE_CODE_DEFAULT_X_APP = "claude-cli";
6
+ export declare const CLAUDE_CODE_DEFAULT_ANTHROPIC_BETA = "claude-code";
7
+ /**
8
+ * Parse the Claude Code app version from a user agent string.
9
+ * Returns the version string (e.g., "2.0.76") or null if not found.
10
+ */
11
+ export declare function parseClaudeCodeAppVersionFromUserAgent(userAgent: string): string | null;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Claude Code specific helpers for Virtual Router bootstrap.
3
+ */
4
+ export const CLAUDE_CODE_DEFAULT_USER_AGENT = 'claude-cli/2.0.76 (external, cli)';
5
+ export const CLAUDE_CODE_DEFAULT_X_APP = 'claude-cli';
6
+ export const CLAUDE_CODE_DEFAULT_ANTHROPIC_BETA = 'claude-code';
7
+ /**
8
+ * Parse the Claude Code app version from a user agent string.
9
+ * Returns the version string (e.g., "2.0.76") or null if not found.
10
+ */
11
+ export function parseClaudeCodeAppVersionFromUserAgent(userAgent) {
12
+ const ua = typeof userAgent === 'string' ? userAgent.trim() : '';
13
+ if (!ua)
14
+ return null;
15
+ // Example: 'claude-cli/2.0.76 (external, cli)'
16
+ const match = /claude-cli\/([\d.]+)/.exec(ua);
17
+ return match?.[1] ?? null;
18
+ }
@@ -0,0 +1,5 @@
1
+ import type { LoadBalancingPolicy, ProviderHealthConfig, VirtualRouterClassifierConfig, VirtualRouterContextRoutingConfig } from '../types.js';
2
+ export declare const DEFAULT_CLASSIFIER: Required<VirtualRouterClassifierConfig>;
3
+ export declare const DEFAULT_LOAD_BALANCING: LoadBalancingPolicy;
4
+ export declare const DEFAULT_HEALTH: ProviderHealthConfig;
5
+ export declare const DEFAULT_CONTEXT_ROUTING: VirtualRouterContextRoutingConfig;
@@ -0,0 +1,13 @@
1
+ export const DEFAULT_CLASSIFIER = {
2
+ longContextThresholdTokens: 180000,
3
+ thinkingKeywords: ['think step', 'analysis', 'reasoning', '仔细分析', '深度思考'],
4
+ codingKeywords: ['apply_patch', 'write_file', 'create_file', 'shell', '修改文件', '写入文件'],
5
+ backgroundKeywords: ['background', 'context dump', '上下文'],
6
+ visionKeywords: ['vision', 'image', 'picture', 'photo']
7
+ };
8
+ export const DEFAULT_LOAD_BALANCING = { strategy: 'round-robin' };
9
+ export const DEFAULT_HEALTH = { failureThreshold: 3, cooldownMs: 30_000, fatalCooldownMs: 300_000 };
10
+ export const DEFAULT_CONTEXT_ROUTING = {
11
+ warnRatio: 0.9,
12
+ hardLimit: false
13
+ };
@@ -0,0 +1,4 @@
1
+ import type { VirtualRouterClockConfig, VirtualRouterConfig, VirtualRouterContextRoutingConfig } from '../types.js';
2
+ export declare function normalizeExecCommandGuard(input: unknown): VirtualRouterConfig['execCommandGuard'] | undefined;
3
+ export declare function normalizeClock(raw: unknown): VirtualRouterClockConfig | undefined;
4
+ export declare function normalizeContextRouting(input: unknown): VirtualRouterContextRoutingConfig;
@@ -0,0 +1,106 @@
1
+ import { DEFAULT_CONTEXT_ROUTING } from './config-defaults.js';
2
+ export function normalizeExecCommandGuard(input) {
3
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
4
+ return undefined;
5
+ }
6
+ const record = input;
7
+ const enabledRaw = record.enabled;
8
+ const enabled = enabledRaw === true ||
9
+ (typeof enabledRaw === 'string' && enabledRaw.trim().toLowerCase() === 'true') ||
10
+ (typeof enabledRaw === 'number' && enabledRaw === 1);
11
+ if (!enabled) {
12
+ return undefined;
13
+ }
14
+ const policyFileRaw = record.policyFile ?? record.policy_file;
15
+ const policyFile = typeof policyFileRaw === 'string' && policyFileRaw.trim().length ? policyFileRaw.trim() : undefined;
16
+ return {
17
+ enabled: true,
18
+ ...(policyFile ? { policyFile } : {})
19
+ };
20
+ }
21
+ export function normalizeClock(raw) {
22
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
23
+ return undefined;
24
+ }
25
+ const record = raw;
26
+ const enabled = record.enabled === true ||
27
+ (typeof record.enabled === 'string' && record.enabled.trim().toLowerCase() === 'true') ||
28
+ (typeof record.enabled === 'number' && record.enabled === 1);
29
+ if (!enabled) {
30
+ return undefined;
31
+ }
32
+ const out = { enabled: true };
33
+ if (typeof record.retentionMs === 'number' && Number.isFinite(record.retentionMs) && record.retentionMs >= 0) {
34
+ out.retentionMs = Math.floor(record.retentionMs);
35
+ }
36
+ if (typeof record.dueWindowMs === 'number' && Number.isFinite(record.dueWindowMs) && record.dueWindowMs >= 0) {
37
+ out.dueWindowMs = Math.floor(record.dueWindowMs);
38
+ }
39
+ if (typeof record.tickMs === 'number' && Number.isFinite(record.tickMs) && record.tickMs >= 0) {
40
+ out.tickMs = Math.floor(record.tickMs);
41
+ }
42
+ if (record.holdNonStreaming === true ||
43
+ (typeof record.holdNonStreaming === 'string' && record.holdNonStreaming.trim().toLowerCase() === 'true') ||
44
+ (typeof record.holdNonStreaming === 'number' && record.holdNonStreaming === 1)) {
45
+ out.holdNonStreaming = true;
46
+ }
47
+ if (typeof record.holdMaxMs === 'number' && Number.isFinite(record.holdMaxMs) && record.holdMaxMs >= 0) {
48
+ out.holdMaxMs = Math.floor(record.holdMaxMs);
49
+ }
50
+ return out;
51
+ }
52
+ export function normalizeContextRouting(input) {
53
+ if (!input || typeof input !== 'object') {
54
+ return { ...DEFAULT_CONTEXT_ROUTING };
55
+ }
56
+ const record = input;
57
+ const warnCandidate = coerceRatio(record.warnRatio) ?? coerceRatio(record.warn_ratio);
58
+ const hardLimitCandidate = coerceBoolean(record.hardLimit) ?? coerceBoolean(record.hard_limit);
59
+ const warnRatio = clampWarnRatio(warnCandidate ?? DEFAULT_CONTEXT_ROUTING.warnRatio);
60
+ const hardLimit = typeof hardLimitCandidate === 'boolean' ? hardLimitCandidate : DEFAULT_CONTEXT_ROUTING.hardLimit;
61
+ return {
62
+ warnRatio,
63
+ hardLimit
64
+ };
65
+ }
66
+ function coerceRatio(value) {
67
+ if (typeof value === 'number' && Number.isFinite(value)) {
68
+ return value;
69
+ }
70
+ if (typeof value === 'string') {
71
+ const trimmed = value.trim();
72
+ if (!trimmed) {
73
+ return undefined;
74
+ }
75
+ const parsed = Number(trimmed);
76
+ if (Number.isFinite(parsed)) {
77
+ return parsed;
78
+ }
79
+ }
80
+ return undefined;
81
+ }
82
+ function clampWarnRatio(value) {
83
+ if (!Number.isFinite(value)) {
84
+ return DEFAULT_CONTEXT_ROUTING.warnRatio;
85
+ }
86
+ const clamped = Math.max(0.1, Math.min(value, 0.99));
87
+ return Number.isFinite(clamped) ? clamped : DEFAULT_CONTEXT_ROUTING.warnRatio;
88
+ }
89
+ function coerceBoolean(value) {
90
+ if (typeof value === 'boolean') {
91
+ return value;
92
+ }
93
+ if (typeof value === 'string') {
94
+ const normalized = value.trim().toLowerCase();
95
+ if (!normalized) {
96
+ return undefined;
97
+ }
98
+ if (['true', '1', 'yes', 'y'].includes(normalized)) {
99
+ return true;
100
+ }
101
+ if (['false', '0', 'no', 'n'].includes(normalized)) {
102
+ return false;
103
+ }
104
+ }
105
+ return undefined;
106
+ }
@@ -0,0 +1,7 @@
1
+ import { type ProviderHealthConfig, type ProviderProfile, type ProviderRuntimeProfile } from '../types.js';
2
+ export declare function buildProviderProfiles(targetKeys: Set<string>, runtimeEntries: Record<string, ProviderRuntimeProfile>): {
3
+ profiles: Record<string, ProviderProfile>;
4
+ targetRuntime: Record<string, ProviderRuntimeProfile>;
5
+ };
6
+ export declare function resolveContextTokens(runtime: ProviderRuntimeProfile, modelId: string): number;
7
+ export declare function normalizeHealth(input: unknown): ProviderHealthConfig | undefined;