@jsonstudio/llms 0.6.1172 → 0.6.1397

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 (167) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/compat/profiles/chat-gemini.json +5 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  11. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  12. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +748 -52
  13. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  14. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +142 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +390 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  42. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  43. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  47. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.d.ts +10 -0
  52. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.js +172 -0
  53. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  54. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  57. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  58. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  59. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.d.ts +10 -0
  61. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js +71 -0
  62. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  63. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  64. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  65. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  66. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  67. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  68. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.d.ts +14 -0
  69. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.js +289 -0
  70. package/dist/conversion/hub/process/chat-process.js +256 -16
  71. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  72. package/dist/conversion/hub/response/provider-response.js +91 -27
  73. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  74. package/dist/conversion/hub/response/response-mappers.js +30 -6
  75. package/dist/conversion/hub/response/response-runtime.js +4 -38
  76. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  77. package/dist/conversion/hub/standardized-bridge.js +23 -15
  78. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  79. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  80. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  81. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  82. package/dist/conversion/shared/jsonish.js +1 -1
  83. package/dist/conversion/shared/mcp-injection.js +67 -33
  84. package/dist/conversion/shared/openai-finalizer.js +2 -1
  85. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  86. package/dist/conversion/shared/responses-output-builder.js +6 -0
  87. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  88. package/dist/conversion/shared/runtime-metadata.js +23 -0
  89. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  90. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  91. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  92. package/dist/conversion/shared/tool-governor.js +3 -3
  93. package/dist/filters/engine.js +5 -5
  94. package/dist/filters/special/request-tool-list-filter.js +194 -60
  95. package/dist/filters/special/request-tools-normalize.js +1 -1
  96. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  97. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  98. package/dist/filters/special/tool-filter-hooks.js +58 -62
  99. package/dist/guidance/index.js +5 -1
  100. package/dist/http/sse-response.js +6 -6
  101. package/dist/router/virtual-router/bootstrap.js +54 -4
  102. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  103. package/dist/router/virtual-router/engine-health.js +11 -110
  104. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +30 -0
  105. package/dist/router/virtual-router/engine-selection/alias-selection.js +237 -0
  106. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  107. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  108. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  109. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  110. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  111. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  112. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  113. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  114. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  115. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  116. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  117. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  118. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  119. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  120. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  121. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  122. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  123. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  124. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  125. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +423 -0
  126. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  127. package/dist/router/virtual-router/engine-selection/tier-selection.js +228 -0
  128. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  129. package/dist/router/virtual-router/engine-selection.js +10 -962
  130. package/dist/router/virtual-router/engine.d.ts +1 -0
  131. package/dist/router/virtual-router/engine.js +64 -11
  132. package/dist/router/virtual-router/routing-instructions.js +6 -1
  133. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  134. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  135. package/dist/router/virtual-router/types.d.ts +38 -1
  136. package/dist/servertool/clock/config.d.ts +8 -0
  137. package/dist/servertool/clock/config.js +22 -0
  138. package/dist/servertool/clock/log.d.ts +3 -0
  139. package/dist/servertool/clock/log.js +13 -0
  140. package/dist/servertool/clock/task-store.d.ts +1 -1
  141. package/dist/servertool/clock/task-store.js +1 -1
  142. package/dist/servertool/clock/tasks.js +1 -1
  143. package/dist/servertool/engine.js +146 -21
  144. package/dist/servertool/handlers/clock-auto.js +11 -6
  145. package/dist/servertool/handlers/clock.js +36 -10
  146. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  147. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  148. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  149. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  150. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  151. package/dist/servertool/handlers/vision.js +4 -1
  152. package/dist/servertool/handlers/web-search.js +3 -1
  153. package/dist/servertool/pending-session.d.ts +19 -0
  154. package/dist/servertool/pending-session.js +97 -0
  155. package/dist/servertool/reenter-backend.js +5 -3
  156. package/dist/servertool/server-side-tools.js +235 -6
  157. package/dist/servertool/types.d.ts +13 -0
  158. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  159. package/dist/sse/shared/chat-serializer.js +2 -2
  160. package/dist/sse/shared/constants.js +1 -1
  161. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  162. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  163. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  164. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  165. package/dist/tools/exec-command/normalize.js +4 -0
  166. package/dist/tools/exec-command/regression-capturer.js +1 -1
  167. package/package.json +10 -5
@@ -1094,7 +1094,7 @@ function normalizeScopeList(value) {
1094
1094
  }
1095
1095
  if (typeof value === 'string' && value.trim()) {
1096
1096
  const normalized = value
1097
- .split(/[\,\s]+/)
1097
+ .split(/[,\s]+/)
1098
1098
  .map((item) => item.trim())
1099
1099
  .filter(Boolean);
1100
1100
  return normalized.length ? normalized : undefined;
@@ -1146,9 +1146,6 @@ function normalizeLoadBalancing(input) {
1146
1146
  return undefined;
1147
1147
  const record = input;
1148
1148
  const strategyRaw = typeof record.strategy === 'string' ? record.strategy.trim().toLowerCase() : '';
1149
- if (!strategyRaw)
1150
- return undefined;
1151
- const strategy = strategyRaw === 'weighted' || strategyRaw === 'sticky' ? strategyRaw : 'round-robin';
1152
1149
  const weightsRaw = asRecord(record.weights);
1153
1150
  const weightsEntries = {};
1154
1151
  for (const [key, value] of Object.entries(weightsRaw)) {
@@ -1192,13 +1189,66 @@ function normalizeLoadBalancing(input) {
1192
1189
  : {})
1193
1190
  }
1194
1191
  : undefined;
1192
+ const aliasSelection = normalizeAliasSelection(record.aliasSelection);
1193
+ const hasNonStrategyConfig = Object.keys(weightsEntries).length > 0 ||
1194
+ Boolean(healthWeighted) ||
1195
+ Boolean(contextWeighted) ||
1196
+ Boolean(aliasSelection);
1197
+ if (!strategyRaw && !hasNonStrategyConfig) {
1198
+ return undefined;
1199
+ }
1200
+ const strategy = strategyRaw === 'weighted' || strategyRaw === 'sticky' ? strategyRaw : 'round-robin';
1195
1201
  return {
1196
1202
  strategy,
1197
1203
  ...(Object.keys(weightsEntries).length ? { weights: weightsEntries } : {}),
1204
+ ...(aliasSelection ? { aliasSelection } : {}),
1198
1205
  ...(healthWeighted ? { healthWeighted } : {}),
1199
1206
  ...(contextWeighted ? { contextWeighted } : {})
1200
1207
  };
1201
1208
  }
1209
+ function normalizeAliasSelection(raw) {
1210
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1211
+ return undefined;
1212
+ }
1213
+ const record = raw;
1214
+ const enabled = typeof record.enabled === 'boolean' ? record.enabled : undefined;
1215
+ const defaultStrategy = coerceAliasSelectionStrategy(record.defaultStrategy);
1216
+ const providersRaw = asRecord(record.providers);
1217
+ const providers = {};
1218
+ for (const [providerId, value] of Object.entries(providersRaw)) {
1219
+ const strategy = coerceAliasSelectionStrategy(value);
1220
+ if (strategy) {
1221
+ providers[providerId] = strategy;
1222
+ }
1223
+ }
1224
+ const out = {
1225
+ ...(enabled !== undefined ? { enabled } : {}),
1226
+ ...(defaultStrategy ? { defaultStrategy } : {}),
1227
+ ...(Object.keys(providers).length ? { providers } : {})
1228
+ };
1229
+ return Object.keys(out).length ? out : undefined;
1230
+ }
1231
+ function coerceAliasSelectionStrategy(value) {
1232
+ if (typeof value !== 'string') {
1233
+ return undefined;
1234
+ }
1235
+ const normalized = value.trim().toLowerCase();
1236
+ if (!normalized) {
1237
+ return undefined;
1238
+ }
1239
+ if (normalized === 'none')
1240
+ return 'none';
1241
+ if (normalized === 'sticky-queue' || normalized === 'sticky_queue' || normalized === 'stickyqueue') {
1242
+ return 'sticky-queue';
1243
+ }
1244
+ if (normalized === 'best-quota' ||
1245
+ normalized === 'best_quota' ||
1246
+ normalized === 'quota-best' ||
1247
+ normalized === 'quota_best') {
1248
+ return 'best-quota';
1249
+ }
1250
+ return undefined;
1251
+ }
1202
1252
  function coerceRatio(value) {
1203
1253
  if (typeof value === 'number' && Number.isFinite(value)) {
1204
1254
  return value;
@@ -7,7 +7,6 @@ type DebugLike = {
7
7
  export declare function resetRateLimitBackoffForProvider(providerKey: string): void;
8
8
  export declare function handleProviderFailureImpl(event: ProviderFailureEvent, healthManager: ProviderHealthManager, healthConfig: Required<ProviderHealthConfig>, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void): void;
9
9
  export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthConfig: Required<ProviderHealthConfig>): ProviderFailureEvent | null;
10
- export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
11
10
  /**
12
11
  * 处理来自 Host 侧的配额恢复事件:
13
12
  * - 清除指定 providerKey 在健康管理器中的熔断/冷却状态;
@@ -18,5 +17,6 @@ export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, provi
18
17
  */
19
18
  export declare function applyQuotaRecoveryImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, clearProviderCooldown: (providerKey: string) => void, debug?: DebugLike): boolean;
20
19
  export declare function applyQuotaDepletedImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): boolean;
20
+ export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, _providerRegistry: ProviderRegistry, _healthManager: ProviderHealthManager, _markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): boolean;
21
21
  export declare function deriveReason(code: string, stage: string, statusCode?: number): string;
22
22
  export {};
@@ -1,4 +1,3 @@
1
- const SERIES_COOLDOWN_DETAIL_KEY = 'virtualRouterSeriesCooldown';
2
1
  const QUOTA_RECOVERY_DETAIL_KEY = 'virtualRouterQuotaRecovery';
3
2
  const QUOTA_DEPLETED_DETAIL_KEY = 'virtualRouterQuotaDepleted';
4
3
  function parseDurationToMs(value) {
@@ -196,46 +195,6 @@ export function mapProviderErrorImpl(event, healthConfig) {
196
195
  }
197
196
  };
198
197
  }
199
- export function applySeriesCooldownImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
200
- const seriesDetail = extractSeriesCooldownDetail(event);
201
- if (!seriesDetail) {
202
- return;
203
- }
204
- const targetKeys = resolveSeriesCooldownTargets(seriesDetail, event, providerRegistry);
205
- if (targetKeys.length === 0) {
206
- debug?.log?.('[virtual-router] series cooldown skipped: no targets', {
207
- providerId: seriesDetail.providerId,
208
- providerKey: seriesDetail.providerKey,
209
- series: seriesDetail.series
210
- });
211
- return;
212
- }
213
- const affected = [];
214
- for (const providerKey of targetKeys) {
215
- try {
216
- const profile = providerRegistry.get(providerKey);
217
- const modelSeries = resolveModelSeries(profile.modelId);
218
- if (modelSeries !== seriesDetail.series) {
219
- continue;
220
- }
221
- healthManager.tripProvider(providerKey, 'rate_limit', seriesDetail.cooldownMs);
222
- markProviderCooldown(providerKey, seriesDetail.cooldownMs);
223
- affected.push(providerKey);
224
- }
225
- catch {
226
- // ignore lookup failures; invalid keys may show up if config drifted
227
- }
228
- }
229
- if (affected.length) {
230
- debug?.log?.('[virtual-router] series cooldown', {
231
- providerId: seriesDetail.providerId,
232
- providerKey: seriesDetail.providerKey,
233
- series: seriesDetail.series,
234
- cooldownMs: seriesDetail.cooldownMs,
235
- affected
236
- });
237
- }
238
- }
239
198
  function extractQuotaRecoveryDetail(event) {
240
199
  if (!event || !event.details || typeof event.details !== 'object') {
241
200
  return null;
@@ -326,63 +285,21 @@ export function applyQuotaDepletedImpl(event, healthManager, markProviderCooldow
326
285
  }
327
286
  return true;
328
287
  }
329
- function resolveSeriesCooldownTargets(detail, event, providerRegistry) {
330
- const candidates = new Set();
331
- const push = (key) => {
332
- if (typeof key !== 'string') {
333
- return;
334
- }
335
- const trimmed = key.trim();
336
- if (!trimmed) {
337
- return;
338
- }
339
- if (providerRegistry.has(trimmed)) {
340
- candidates.add(trimmed);
341
- }
342
- };
343
- push(detail.providerKey);
344
- const runtimeKey = (event.runtime?.target && typeof event.runtime.target === 'object'
345
- ? event.runtime.target.providerKey
346
- : undefined) || event.runtime?.providerKey;
347
- push(runtimeKey);
348
- return Array.from(candidates);
349
- }
350
- function extractSeriesCooldownDetail(event) {
288
+ export function applySeriesCooldownImpl(event, _providerRegistry, _healthManager, _markProviderCooldown, debug) {
351
289
  if (!event || !event.details || typeof event.details !== 'object') {
352
- return null;
290
+ return false;
353
291
  }
354
- const raw = event.details[SERIES_COOLDOWN_DETAIL_KEY];
292
+ const raw = event.details.virtualRouterSeriesCooldown;
355
293
  if (!raw || typeof raw !== 'object') {
356
- return null;
357
- }
358
- const record = raw;
359
- const providerIdRaw = record.providerId;
360
- const seriesRaw = record.series;
361
- const providerKeyRaw = record.providerKey;
362
- const cooldownRaw = record.cooldownMs;
363
- if (typeof providerIdRaw !== 'string' || !providerIdRaw.trim()) {
364
- return null;
365
- }
366
- const normalizedSeries = typeof seriesRaw === 'string' ? seriesRaw.trim().toLowerCase() : '';
367
- if (normalizedSeries !== 'gemini-pro' && normalizedSeries !== 'gemini-flash' && normalizedSeries !== 'claude') {
368
- return null;
369
- }
370
- const cooldownMs = typeof cooldownRaw === 'number'
371
- ? cooldownRaw
372
- : typeof cooldownRaw === 'string'
373
- ? Number.parseFloat(cooldownRaw)
374
- : Number.NaN;
375
- if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
376
- return null;
294
+ return false;
377
295
  }
378
- return {
379
- providerId: providerIdRaw.trim(),
380
- ...(typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length
381
- ? { providerKey: providerKeyRaw.trim() }
382
- : {}),
383
- series: normalizedSeries,
384
- cooldownMs: Math.round(cooldownMs)
385
- };
296
+ const detail = raw;
297
+ debug?.log?.('[virtual-router] series cooldown ignored', {
298
+ providerId: detail.providerId,
299
+ series: detail.series,
300
+ cooldownMs: detail.cooldownMs
301
+ });
302
+ return true;
386
303
  }
387
304
  export function deriveReason(code, stage, statusCode) {
388
305
  if (code.includes('RATE') || code.includes('429'))
@@ -401,19 +318,3 @@ export function deriveReason(code, stage, statusCode) {
401
318
  return 'client_error';
402
319
  return 'unknown';
403
320
  }
404
- function resolveModelSeries(modelId) {
405
- if (!modelId) {
406
- return 'default';
407
- }
408
- const lower = modelId.toLowerCase();
409
- if (lower.includes('claude') || lower.includes('opus')) {
410
- return 'claude';
411
- }
412
- if (lower.includes('flash')) {
413
- return 'gemini-flash';
414
- }
415
- if (lower.includes('gemini') || lower.includes('pro')) {
416
- return 'gemini-pro';
417
- }
418
- return 'default';
419
- }
@@ -0,0 +1,30 @@
1
+ import type { AliasSelectionConfig, AliasSelectionStrategy } from '../types.js';
2
+ export type AliasQueueStore = Map<string, string[]>;
3
+ export declare const DEFAULT_PROVIDER_ALIAS_SELECTION: Record<string, AliasSelectionStrategy>;
4
+ export declare function resolveAliasSelectionStrategy(providerId: string, cfg: AliasSelectionConfig | undefined): AliasSelectionStrategy;
5
+ export declare function pinCandidatesByAliasQueue(opts: {
6
+ queueStore: AliasQueueStore | undefined;
7
+ providerId: string;
8
+ modelId: string;
9
+ candidates: string[];
10
+ orderedTargets: string[];
11
+ excludedProviderKeys: Set<string>;
12
+ aliasOfKey: (providerKey: string) => string | null;
13
+ modelIdOfKey: (providerKey: string) => string | null;
14
+ availabilityCheck: (providerKey: string) => boolean;
15
+ }): string[] | null;
16
+ export declare function pinCandidatesByBestQuota(opts: {
17
+ providerId: string;
18
+ modelId: string;
19
+ candidates: string[];
20
+ orderedTargets: string[];
21
+ aliasOfKey: (providerKey: string) => string | null;
22
+ modelIdOfKey: (providerKey: string) => string | null;
23
+ quotaView: ((providerKey: string) => {
24
+ remainingFraction?: number | null;
25
+ inPool: boolean;
26
+ cooldownUntil?: number | null;
27
+ blacklistUntil?: number | null;
28
+ } | null) | undefined;
29
+ now: number;
30
+ }): string[] | null;
@@ -0,0 +1,237 @@
1
+ const COOLDOWN_EMPTY_THRESHOLD_MS = 30_000;
2
+ // Default provider-level strategy table.
3
+ // This is a data-only default; callers can override via `loadBalancing.aliasSelection.providers`.
4
+ export const DEFAULT_PROVIDER_ALIAS_SELECTION = {
5
+ // Antigravity: prefer the alias with highest remaining quota; fall back to sticky-queue when quota is unknown.
6
+ antigravity: 'best-quota'
7
+ };
8
+ export function resolveAliasSelectionStrategy(providerId, cfg) {
9
+ if (!providerId)
10
+ return 'none';
11
+ if (cfg?.enabled === false)
12
+ return 'none';
13
+ const overrides = cfg?.providers ?? {};
14
+ const override = overrides[providerId];
15
+ if (override === 'none' || override === 'sticky-queue' || override === 'best-quota') {
16
+ return override;
17
+ }
18
+ const def = cfg?.defaultStrategy;
19
+ if (def === 'none' || def === 'sticky-queue' || def === 'best-quota') {
20
+ return def;
21
+ }
22
+ const table = DEFAULT_PROVIDER_ALIAS_SELECTION[providerId];
23
+ return table ?? 'none';
24
+ }
25
+ export function pinCandidatesByAliasQueue(opts) {
26
+ const { queueStore, providerId, modelId, candidates, orderedTargets, excludedProviderKeys, aliasOfKey, modelIdOfKey, availabilityCheck } = opts;
27
+ if (!queueStore)
28
+ return null;
29
+ if (!providerId || !modelId)
30
+ return null;
31
+ if (!Array.isArray(candidates) || candidates.length < 2)
32
+ return null;
33
+ const aliasBuckets = new Map();
34
+ for (const key of candidates) {
35
+ if (!key || typeof key !== 'string')
36
+ continue;
37
+ if (!key.startsWith(`${providerId}.`))
38
+ return null;
39
+ const m = modelIdOfKey(key);
40
+ if (!m || m !== modelId)
41
+ return null;
42
+ const alias = aliasOfKey(key);
43
+ if (!alias)
44
+ return null;
45
+ const list = aliasBuckets.get(alias) ?? [];
46
+ list.push(key);
47
+ aliasBuckets.set(alias, list);
48
+ }
49
+ if (aliasBuckets.size <= 1)
50
+ return null;
51
+ const queueKey = `${providerId}::${modelId}`;
52
+ const desiredOrder = resolveAliasOrderFromTargets({
53
+ orderedTargets,
54
+ providerId,
55
+ modelId,
56
+ aliasOfKey,
57
+ modelIdOfKey,
58
+ allowedAliases: new Set(aliasBuckets.keys())
59
+ });
60
+ let queue = mergeAliasQueue(queueStore.get(queueKey) ?? [], desiredOrder);
61
+ // If this is a retry attempt and the previous alias was excluded, rotate it to the tail.
62
+ if (excludedProviderKeys && excludedProviderKeys.size) {
63
+ const excludedAliases = [];
64
+ for (const ex of excludedProviderKeys) {
65
+ if (!ex || typeof ex !== 'string')
66
+ continue;
67
+ if (!ex.startsWith(`${providerId}.`))
68
+ continue;
69
+ const exModel = modelIdOfKey(ex);
70
+ if (!exModel || exModel !== modelId)
71
+ continue;
72
+ const exAlias = aliasOfKey(ex);
73
+ if (exAlias)
74
+ excludedAliases.push(exAlias);
75
+ }
76
+ if (excludedAliases.length) {
77
+ queue = rotateQueueToTail(queue, excludedAliases);
78
+ }
79
+ }
80
+ // Ensure the head alias points to an available candidate; otherwise rotate until we find one.
81
+ if (queue.length) {
82
+ for (let i = 0; i < queue.length; i += 1) {
83
+ const head = queue[0];
84
+ const keys = aliasBuckets.get(head) ?? [];
85
+ const hasAvailable = keys.some((key) => availabilityCheck(key));
86
+ if (hasAvailable) {
87
+ break;
88
+ }
89
+ queue = rotateQueueToTail(queue, [head]);
90
+ }
91
+ }
92
+ // Persist queue updates (even if unchanged, ensure first-time init is stored).
93
+ queueStore.set(queueKey, queue);
94
+ const selectedAlias = queue[0];
95
+ if (!selectedAlias)
96
+ return null;
97
+ const selectedKeys = aliasBuckets.get(selectedAlias) ?? [];
98
+ if (!selectedKeys.length)
99
+ return null;
100
+ // Preserve original candidate order.
101
+ const selectedSet = new Set(selectedKeys);
102
+ return candidates.filter((key) => selectedSet.has(key));
103
+ }
104
+ export function pinCandidatesByBestQuota(opts) {
105
+ const { providerId, modelId, candidates, orderedTargets, aliasOfKey, modelIdOfKey, quotaView, now } = opts;
106
+ if (!quotaView)
107
+ return null;
108
+ if (!providerId || !modelId)
109
+ return null;
110
+ if (!Array.isArray(candidates) || candidates.length < 2)
111
+ return null;
112
+ const aliasBuckets = new Map();
113
+ const aliasOrder = new Map();
114
+ let order = 0;
115
+ for (const key of candidates) {
116
+ if (!key || typeof key !== 'string')
117
+ continue;
118
+ if (!key.startsWith(`${providerId}.`))
119
+ return null;
120
+ const m = modelIdOfKey(key);
121
+ if (!m || m !== modelId)
122
+ return null;
123
+ const alias = aliasOfKey(key);
124
+ if (!alias)
125
+ return null;
126
+ const list = aliasBuckets.get(alias) ?? [];
127
+ list.push(key);
128
+ aliasBuckets.set(alias, list);
129
+ if (!aliasOrder.has(alias)) {
130
+ aliasOrder.set(alias, order++);
131
+ }
132
+ }
133
+ if (aliasBuckets.size <= 1)
134
+ return null;
135
+ const preferredOrder = resolveAliasOrderFromTargets({
136
+ orderedTargets,
137
+ providerId,
138
+ modelId,
139
+ aliasOfKey,
140
+ modelIdOfKey,
141
+ allowedAliases: new Set(aliasBuckets.keys())
142
+ });
143
+ for (const alias of preferredOrder) {
144
+ if (!aliasOrder.has(alias)) {
145
+ aliasOrder.set(alias, order++);
146
+ }
147
+ }
148
+ const eligible = [];
149
+ for (const [alias, keys] of aliasBuckets.entries()) {
150
+ const entry = quotaView(keys[0] ?? '');
151
+ if (!entry || entry.inPool === false) {
152
+ continue;
153
+ }
154
+ if (entry.blacklistUntil && entry.blacklistUntil > now) {
155
+ continue;
156
+ }
157
+ if (entry.cooldownUntil && entry.cooldownUntil - now >= COOLDOWN_EMPTY_THRESHOLD_MS) {
158
+ continue;
159
+ }
160
+ const remainingRaw = entry.remainingFraction;
161
+ const remaining = typeof remainingRaw === 'number' && Number.isFinite(remainingRaw) ? remainingRaw : 0;
162
+ if (remaining <= 0) {
163
+ continue;
164
+ }
165
+ eligible.push({
166
+ alias,
167
+ score: remaining,
168
+ order: aliasOrder.get(alias) ?? Number.MAX_SAFE_INTEGER
169
+ });
170
+ }
171
+ if (!eligible.length) {
172
+ return null;
173
+ }
174
+ eligible.sort((a, b) => (b.score - a.score) || (a.order - b.order));
175
+ const selectedAlias = eligible[0]?.alias;
176
+ if (!selectedAlias)
177
+ return null;
178
+ const selectedKeys = aliasBuckets.get(selectedAlias) ?? [];
179
+ if (!selectedKeys.length)
180
+ return null;
181
+ const selectedSet = new Set(selectedKeys);
182
+ return candidates.filter((key) => selectedSet.has(key));
183
+ }
184
+ function resolveAliasOrderFromTargets(opts) {
185
+ const { orderedTargets, providerId, modelId, aliasOfKey, modelIdOfKey, allowedAliases } = opts;
186
+ if (!Array.isArray(orderedTargets) || orderedTargets.length === 0) {
187
+ return Array.from(allowedAliases);
188
+ }
189
+ const out = [];
190
+ const seen = new Set();
191
+ for (const key of orderedTargets) {
192
+ if (!key || typeof key !== 'string')
193
+ continue;
194
+ if (!key.startsWith(`${providerId}.`))
195
+ continue;
196
+ const m = modelIdOfKey(key);
197
+ if (!m || m !== modelId)
198
+ continue;
199
+ const alias = aliasOfKey(key);
200
+ if (!alias || !allowedAliases.has(alias) || seen.has(alias))
201
+ continue;
202
+ seen.add(alias);
203
+ out.push(alias);
204
+ }
205
+ for (const alias of Array.from(allowedAliases)) {
206
+ if (!seen.has(alias))
207
+ out.push(alias);
208
+ }
209
+ return out;
210
+ }
211
+ function mergeAliasQueue(existing, desired) {
212
+ if (!Array.isArray(existing) || existing.length === 0) {
213
+ return [...desired];
214
+ }
215
+ const desiredSet = new Set(desired);
216
+ const merged = existing.filter((a) => desiredSet.has(a));
217
+ const mergedSet = new Set(merged);
218
+ for (const a of desired) {
219
+ if (!mergedSet.has(a))
220
+ merged.push(a);
221
+ }
222
+ return merged;
223
+ }
224
+ function rotateQueueToTail(queue, aliases) {
225
+ if (!Array.isArray(queue) || queue.length < 2)
226
+ return queue;
227
+ if (!Array.isArray(aliases) || aliases.length === 0)
228
+ return queue;
229
+ const toMove = new Set(aliases);
230
+ const kept = queue.filter((a) => !toMove.has(a));
231
+ const moved = [];
232
+ for (const a of queue) {
233
+ if (toMove.has(a) && !moved.includes(a))
234
+ moved.push(a);
235
+ }
236
+ return [...kept, ...moved];
237
+ }
@@ -0,0 +1,11 @@
1
+ import { type ResolvedContextWeightedConfig } from '../context-weighted.js';
2
+ import type { ContextAdvisorResult } from '../context-advisor.js';
3
+ export declare function computeContextWeightMultipliers(opts: {
4
+ candidates: string[];
5
+ usage: ContextAdvisorResult['usage'] | undefined;
6
+ warnRatio: number;
7
+ cfg: ResolvedContextWeightedConfig;
8
+ }): {
9
+ ref: number;
10
+ eff: Record<string, number>;
11
+ } | null;
@@ -0,0 +1,23 @@
1
+ import { computeEffectiveSafeWindowTokens } from '../context-weighted.js';
2
+ export function computeContextWeightMultipliers(opts) {
3
+ const { candidates, usage, warnRatio, cfg } = opts;
4
+ if (!cfg.enabled) {
5
+ return null;
6
+ }
7
+ const eff = {};
8
+ let ref = 1;
9
+ for (const key of candidates) {
10
+ const entry = usage?.[key];
11
+ const limit = entry && typeof entry.limit === 'number' && Number.isFinite(entry.limit) ? Math.floor(entry.limit) : 0;
12
+ const safeEff = computeEffectiveSafeWindowTokens({
13
+ modelMaxTokens: Math.max(1, limit),
14
+ warnRatio,
15
+ clientCapTokens: cfg.clientCapTokens
16
+ });
17
+ eff[key] = safeEff;
18
+ if (safeEff > ref) {
19
+ ref = safeEff;
20
+ }
21
+ }
22
+ return { ref, eff };
23
+ }
@@ -0,0 +1,9 @@
1
+ import type { RouterMetadataInput, RoutingFeatures } from '../types.js';
2
+ import type { RoutingInstructionState } from '../routing-instructions.js';
3
+ import type { SelectionDeps } from './selection-deps.js';
4
+ export declare function selectDirectProviderModel(providerId: string, modelId: string, metadata: RouterMetadataInput, features: RoutingFeatures, activeState: RoutingInstructionState, deps: SelectionDeps): {
5
+ providerKey: string;
6
+ routeUsed: string;
7
+ pool: string[];
8
+ poolId?: string;
9
+ } | null;
@@ -0,0 +1,49 @@
1
+ import { trySelectFromTier } from './tier-selection.js';
2
+ export function selectDirectProviderModel(providerId, modelId, metadata, features, activeState, deps) {
3
+ const normalizedProvider = typeof providerId === 'string' ? providerId.trim() : '';
4
+ const normalizedModel = typeof modelId === 'string' ? modelId.trim() : '';
5
+ if (!normalizedProvider || !normalizedModel) {
6
+ return null;
7
+ }
8
+ const providerKeys = deps.providerRegistry.listProviderKeys(normalizedProvider);
9
+ if (providerKeys.length === 0) {
10
+ return null;
11
+ }
12
+ const matchingKeys = providerKeys.filter((key) => {
13
+ try {
14
+ const profile = deps.providerRegistry.get(key);
15
+ return profile?.modelId === normalizedModel;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ });
21
+ if (matchingKeys.length === 0) {
22
+ return null;
23
+ }
24
+ const attempted = [];
25
+ const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
26
+ ? Math.max(0, features.estimatedTokens)
27
+ : 0;
28
+ const tier = {
29
+ id: `direct:${normalizedProvider}.${normalizedModel}`,
30
+ targets: matchingKeys,
31
+ priority: 100,
32
+ mode: 'round-robin',
33
+ backup: false
34
+ };
35
+ const { providerKey, poolTargets, tierId, failureHint } = trySelectFromTier('direct', tier, undefined, estimatedTokens, features, deps, {
36
+ disabledProviders: new Set(activeState.disabledProviders),
37
+ disabledKeysMap: new Map(activeState.disabledKeys),
38
+ allowedProviders: new Set(activeState.allowedProviders),
39
+ disabledModels: new Map(activeState.disabledModels),
40
+ allowAliasRotation: true
41
+ });
42
+ if (providerKey) {
43
+ return { providerKey, routeUsed: 'direct', pool: poolTargets, poolId: tierId };
44
+ }
45
+ if (failureHint) {
46
+ attempted.push(failureHint);
47
+ }
48
+ return null;
49
+ }
@@ -0,0 +1,6 @@
1
+ import type { RoutingInstructionState } from '../routing-instructions.js';
2
+ import type { ProviderRegistry } from '../provider-registry.js';
3
+ export declare function resolveInstructionTarget(target: NonNullable<RoutingInstructionState['forcedTarget']>, providerRegistry: ProviderRegistry): {
4
+ mode: 'exact' | 'filter';
5
+ keys: string[];
6
+ } | null;
@@ -0,0 +1,54 @@
1
+ import { getProviderModelId } from './key-parsing.js';
2
+ export function resolveInstructionTarget(target, providerRegistry) {
3
+ if (!target || !target.provider) {
4
+ return null;
5
+ }
6
+ const providerId = target.provider;
7
+ const providerKeys = providerRegistry.listProviderKeys(providerId);
8
+ if (providerKeys.length === 0) {
9
+ return null;
10
+ }
11
+ const alias = typeof target.keyAlias === 'string' ? target.keyAlias.trim() : '';
12
+ const aliasExplicit = alias.length > 0 && target.pathLength === 3;
13
+ if (aliasExplicit) {
14
+ const prefix = `${providerId}.${alias}.`;
15
+ const aliasKeys = providerKeys.filter((key) => key.startsWith(prefix));
16
+ if (aliasKeys.length > 0) {
17
+ if (target.model && target.model.trim()) {
18
+ const normalizedModel = target.model.trim();
19
+ const matching = aliasKeys.filter((key) => getProviderModelId(key, providerRegistry) === normalizedModel);
20
+ if (matching.length > 0) {
21
+ // Prefer exact to keep sticky pool deterministic when only one key matches.
22
+ if (matching.length === 1) {
23
+ return { mode: 'exact', keys: [matching[0]] };
24
+ }
25
+ return { mode: 'filter', keys: matching };
26
+ }
27
+ }
28
+ return { mode: 'filter', keys: aliasKeys };
29
+ }
30
+ }
31
+ if (typeof target.keyIndex === 'number' && target.keyIndex > 0) {
32
+ const runtimeKey = providerRegistry.resolveRuntimeKeyByIndex(providerId, target.keyIndex);
33
+ if (runtimeKey) {
34
+ return { mode: 'exact', keys: [runtimeKey] };
35
+ }
36
+ }
37
+ if (target.model && target.model.trim()) {
38
+ const normalizedModel = target.model.trim();
39
+ const matchingKeys = providerKeys.filter((key) => {
40
+ const modelId = getProviderModelId(key, providerRegistry);
41
+ return modelId === normalizedModel;
42
+ });
43
+ if (matchingKeys.length > 0) {
44
+ return { mode: 'filter', keys: matchingKeys };
45
+ }
46
+ }
47
+ if (alias && !aliasExplicit) {
48
+ const legacyKey = providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
49
+ if (legacyKey) {
50
+ return { mode: 'exact', keys: [legacyKey] };
51
+ }
52
+ }
53
+ return { mode: 'filter', keys: providerKeys };
54
+ }