@jsonstudio/llms 0.6.1164 → 0.6.1354

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 (164) 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/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +65 -5
  95. package/dist/router/virtual-router/context-advisor.d.ts +4 -0
  96. package/dist/router/virtual-router/context-advisor.js +3 -0
  97. package/dist/router/virtual-router/context-weighted.d.ts +31 -0
  98. package/dist/router/virtual-router/context-weighted.js +54 -0
  99. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  100. package/dist/router/virtual-router/engine-health.js +11 -110
  101. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  102. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  103. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  104. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  105. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  106. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  107. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  108. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  109. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  110. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  111. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  112. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  113. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  114. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  115. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  116. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  117. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  118. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  119. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  120. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  121. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  122. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  123. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  124. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  125. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  126. package/dist/router/virtual-router/engine-selection.js +10 -815
  127. package/dist/router/virtual-router/engine.d.ts +1 -0
  128. package/dist/router/virtual-router/engine.js +55 -10
  129. package/dist/router/virtual-router/routing-instructions.js +6 -1
  130. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  131. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  132. package/dist/router/virtual-router/types.d.ts +53 -1
  133. package/dist/servertool/clock/config.d.ts +8 -0
  134. package/dist/servertool/clock/config.js +22 -0
  135. package/dist/servertool/clock/log.d.ts +3 -0
  136. package/dist/servertool/clock/log.js +13 -0
  137. package/dist/servertool/clock/task-store.d.ts +1 -1
  138. package/dist/servertool/clock/task-store.js +1 -1
  139. package/dist/servertool/clock/tasks.js +1 -1
  140. package/dist/servertool/engine.js +146 -21
  141. package/dist/servertool/handlers/clock-auto.js +11 -6
  142. package/dist/servertool/handlers/clock.js +36 -10
  143. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  144. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  145. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  146. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  147. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  148. package/dist/servertool/handlers/vision.js +4 -1
  149. package/dist/servertool/handlers/web-search.js +3 -1
  150. package/dist/servertool/pending-session.d.ts +19 -0
  151. package/dist/servertool/pending-session.js +97 -0
  152. package/dist/servertool/reenter-backend.js +5 -3
  153. package/dist/servertool/server-side-tools.js +235 -6
  154. package/dist/servertool/types.d.ts +13 -0
  155. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  156. package/dist/sse/shared/chat-serializer.js +2 -2
  157. package/dist/sse/shared/constants.js +1 -1
  158. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  159. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  160. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  161. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  162. package/dist/tools/exec-command/normalize.js +4 -0
  163. package/dist/tools/exec-command/regression-capturer.js +1 -1
  164. package/package.json +10 -5
@@ -1,5 +1,12 @@
1
- import { computeHealthWeight, resolveHealthWeightedConfig } from './health-weighted.js';
2
- import { DEFAULT_ROUTE, ROUTE_PRIORITY, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
1
+ import { DEFAULT_ROUTE, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
2
+ import { extractExcludedProviderKeySet, extractProviderId } from './engine-selection/key-parsing.js';
3
+ import { trySelectFromTier } from './engine-selection/tier-selection.js';
4
+ import { resolveInstructionTarget } from './engine-selection/instruction-target.js';
5
+ import { filterCandidatesByRoutingState } from './engine-selection/routing-state-filter.js';
6
+ import { selectFromStickyPool as selectFromStickyPoolImpl } from './engine-selection/sticky-pool.js';
7
+ export { selectDirectProviderModel } from './engine-selection/direct-provider-model.js';
8
+ export { selectFromStickyPool } from './engine-selection/sticky-pool.js';
9
+ import { buildRouteCandidates, extendRouteCandidatesForState, initializeRouteQueue, normalizeRouteAlias, routeHasTargets, sortRoutePools } from './engine-selection/route-utils.js';
3
10
  export function selectProviderImpl(requestedRoute, metadata, classification, features, activeState, deps, options = {}) {
4
11
  const state = options.routingState ?? activeState;
5
12
  const quotaView = deps.quotaView;
@@ -121,7 +128,7 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
121
128
  }
122
129
  }
123
130
  if (stickyKeySet && stickyKeySet.size > 0) {
124
- const stickySelection = selectFromStickyPool(stickyKeySet, metadata, features, state, deps, { allowAliasRotation });
131
+ const stickySelection = selectFromStickyPoolImpl(stickyKeySet, metadata, features, state, deps, { allowAliasRotation });
125
132
  if (stickySelection) {
126
133
  return stickySelection;
127
134
  }
@@ -168,72 +175,6 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
168
175
  allowAliasRotation
169
176
  });
170
177
  }
171
- function extendRouteCandidatesForState(candidates, state, routing) {
172
- // When provider allowlists are active (e.g. "<**!glm**>"), routing should not be bounded by
173
- // classifier candidates only. Otherwise, a perfectly valid provider that exists in config
174
- // (e.g. in a backup/default pool) can become unreachable and cause PROVIDER_NOT_AVAILABLE.
175
- //
176
- // We keep original ordering, then append all known routes (by priority) as a fallback search space.
177
- if (!state.allowedProviders || state.allowedProviders.size === 0) {
178
- return candidates;
179
- }
180
- const allRoutes = sortByPriority(Object.keys(routing).filter((routeName) => routeName && routeHasTargets(routing[routeName])));
181
- const expanded = Array.isArray(candidates) ? [...candidates] : [];
182
- for (const routeName of allRoutes) {
183
- if (!expanded.includes(routeName)) {
184
- expanded.push(routeName);
185
- }
186
- }
187
- return expanded;
188
- }
189
- export function selectDirectProviderModel(providerId, modelId, metadata, features, activeState, deps) {
190
- const normalizedProvider = typeof providerId === 'string' ? providerId.trim() : '';
191
- const normalizedModel = typeof modelId === 'string' ? modelId.trim() : '';
192
- if (!normalizedProvider || !normalizedModel) {
193
- return null;
194
- }
195
- const providerKeys = deps.providerRegistry.listProviderKeys(normalizedProvider);
196
- if (providerKeys.length === 0) {
197
- return null;
198
- }
199
- const matchingKeys = providerKeys.filter((key) => {
200
- try {
201
- const profile = deps.providerRegistry.get(key);
202
- return profile?.modelId === normalizedModel;
203
- }
204
- catch {
205
- return false;
206
- }
207
- });
208
- if (matchingKeys.length === 0) {
209
- return null;
210
- }
211
- const attempted = [];
212
- const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
213
- ? Math.max(0, features.estimatedTokens)
214
- : 0;
215
- const tier = {
216
- id: `direct:${normalizedProvider}.${normalizedModel}`,
217
- targets: matchingKeys,
218
- priority: 100,
219
- mode: 'round-robin',
220
- backup: false
221
- };
222
- const { providerKey, poolTargets, tierId, failureHint } = trySelectFromTier('direct', tier, undefined, estimatedTokens, features, deps, {
223
- disabledProviders: new Set(activeState.disabledProviders),
224
- disabledKeysMap: new Map(activeState.disabledKeys),
225
- allowedProviders: new Set(activeState.allowedProviders),
226
- disabledModels: new Map(activeState.disabledModels),
227
- allowAliasRotation: true
228
- });
229
- if (providerKey) {
230
- return { providerKey, routeUsed: 'direct', pool: poolTargets, poolId: tierId };
231
- }
232
- if (failureHint) {
233
- attempted.push(failureHint);
234
- }
235
- return null;
236
- }
237
178
  function selectFromCandidates(routes, metadata, classification, features, state, deps, options) {
238
179
  const allowedProviders = new Set(state.allowedProviders);
239
180
  const disabledProviders = new Set(state.disabledProviders);
@@ -282,749 +223,3 @@ function selectFromCandidates(routes, metadata, classification, features, state,
282
223
  const requestedRoute = normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
283
224
  throw new VirtualRouterError(`All providers unavailable for route ${requestedRoute}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { routeName: requestedRoute, attempted });
284
225
  }
285
- function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features, deps, options) {
286
- const { disabledProviders, disabledKeysMap, allowedProviders, disabledModels, requiredProviderKeys } = options;
287
- let targets = Array.isArray(tier.targets) ? tier.targets : [];
288
- const excludedRaw = features.metadata?.excludedProviderKeys &&
289
- Array.isArray(features.metadata.excludedProviderKeys)
290
- ? features.metadata.excludedProviderKeys
291
- : [];
292
- const excludedKeys = new Set(excludedRaw
293
- .map((val) => (typeof val === 'string' ? val.trim() : ''))
294
- .filter((val) => Boolean(val)));
295
- if (excludedKeys.size > 0) {
296
- targets = targets.filter((key) => !excludedKeys.has(key));
297
- }
298
- const isRecoveryAttempt = excludedKeys.size > 0;
299
- const singleCandidateFallback = targets.length === 1 ? targets[0] : undefined;
300
- if (targets.length > 0) {
301
- // Always respect cooldown signals. If a route/tier is depleted due to cooldown,
302
- // routing is expected to fall back to other tiers/routes (e.g. longcontext → default),
303
- // rather than repeatedly selecting the cooled-down provider.
304
- targets = targets.filter((key) => !deps.isProviderCoolingDown(key));
305
- }
306
- if (allowedProviders && allowedProviders.size > 0) {
307
- targets = targets.filter((key) => {
308
- const providerId = extractProviderId(key);
309
- return providerId && allowedProviders.has(providerId);
310
- });
311
- }
312
- if (disabledProviders && disabledProviders.size > 0) {
313
- targets = targets.filter((key) => {
314
- const providerId = extractProviderId(key);
315
- return providerId && !disabledProviders.has(providerId);
316
- });
317
- }
318
- if (disabledKeysMap && disabledKeysMap.size > 0) {
319
- targets = targets.filter((key) => {
320
- const providerId = extractProviderId(key);
321
- if (!providerId)
322
- return true;
323
- const disabledKeys = disabledKeysMap.get(providerId);
324
- if (!disabledKeys || disabledKeys.size === 0)
325
- return true;
326
- const keyAlias = extractKeyAlias(key);
327
- const keyIndex = extractKeyIndex(key);
328
- if (keyAlias && disabledKeys.has(keyAlias)) {
329
- return false;
330
- }
331
- if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
332
- return false;
333
- }
334
- return true;
335
- });
336
- }
337
- if (disabledModels && disabledModels.size > 0) {
338
- targets = targets.filter((key) => {
339
- const providerId = extractProviderId(key);
340
- if (!providerId) {
341
- return true;
342
- }
343
- const disabled = disabledModels.get(providerId);
344
- if (!disabled || disabled.size === 0) {
345
- return true;
346
- }
347
- const modelId = getProviderModelId(key, deps.providerRegistry);
348
- if (!modelId) {
349
- return true;
350
- }
351
- return !disabled.has(modelId);
352
- });
353
- }
354
- if (requiredProviderKeys && requiredProviderKeys.size > 0) {
355
- targets = targets.filter((key) => requiredProviderKeys.has(key));
356
- }
357
- const serverToolRequired = features.metadata?.serverToolRequired === true;
358
- if (serverToolRequired) {
359
- const filtered = [];
360
- for (const key of targets) {
361
- try {
362
- const profile = deps.providerRegistry.get(key);
363
- if (!profile.serverToolsDisabled) {
364
- filtered.push(key);
365
- }
366
- }
367
- catch {
368
- // ignore unknown providers when filtering for servertools
369
- }
370
- }
371
- targets = filtered;
372
- }
373
- if (features.hasImageAttachment && (routeName === DEFAULT_ROUTE || routeName === 'thinking')) {
374
- const prioritized = [];
375
- const fallthrough = [];
376
- for (const key of targets) {
377
- try {
378
- const profile = deps.providerRegistry.get(key);
379
- if (profile.providerType === 'responses') {
380
- prioritized.push(key);
381
- }
382
- else if (profile.providerType === 'gemini') {
383
- prioritized.push(key);
384
- }
385
- else {
386
- fallthrough.push(key);
387
- }
388
- }
389
- catch {
390
- fallthrough.push(key);
391
- }
392
- }
393
- if (prioritized.length) {
394
- targets = prioritized;
395
- }
396
- }
397
- if (!targets.length) {
398
- return { providerKey: null, poolTargets: [], tierId: tier.id, failureHint: `${routeName}:${tier.id}:empty` };
399
- }
400
- const contextResult = deps.contextAdvisor.classify(targets, estimatedTokens, (key) => deps.providerRegistry.get(key));
401
- const prioritizedPools = buildContextCandidatePools(contextResult);
402
- const quotaView = deps.quotaView;
403
- const now = quotaView ? Date.now() : 0;
404
- const healthWeightedCfg = resolveHealthWeightedConfig(deps.loadBalancer.getPolicy().healthWeighted);
405
- const selectFirstAvailable = (candidates) => {
406
- for (const key of candidates) {
407
- if (deps.healthManager.isAvailable(key)) {
408
- return key;
409
- }
410
- }
411
- return null;
412
- };
413
- const selectWithQuota = (candidates) => {
414
- if (!quotaView) {
415
- if (tier.mode === 'priority') {
416
- if (isRecoveryAttempt) {
417
- return selectFirstAvailable(candidates);
418
- }
419
- return deps.loadBalancer.select({
420
- routeName: `${routeName}:${tier.id}:priority`,
421
- candidates,
422
- stickyKey: options.allowAliasRotation ? undefined : stickyKey,
423
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
424
- }, 'round-robin');
425
- }
426
- const selected = deps.loadBalancer.select({
427
- routeName: `${routeName}:${tier.id}`,
428
- candidates,
429
- stickyKey: options.allowAliasRotation ? undefined : stickyKey,
430
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
431
- }, tier.mode === 'round-robin' ? 'round-robin' : undefined);
432
- return selected;
433
- }
434
- const buckets = new Map();
435
- let order = 0;
436
- for (const key of candidates) {
437
- const entry = quotaView(key);
438
- if (!entry) {
439
- const list = buckets.get(100) ?? [];
440
- list.push({ key, penalty: 0, order: order++ });
441
- buckets.set(100, list);
442
- continue;
443
- }
444
- if (!entry.inPool) {
445
- continue;
446
- }
447
- if (entry.cooldownUntil && entry.cooldownUntil > now) {
448
- continue;
449
- }
450
- if (entry.blacklistUntil && entry.blacklistUntil > now) {
451
- continue;
452
- }
453
- const tierPriority = typeof entry.priorityTier === 'number' && Number.isFinite(entry.priorityTier)
454
- ? entry.priorityTier
455
- : 100;
456
- const penaltyRaw = entry.selectionPenalty;
457
- const penalty = typeof penaltyRaw === 'number' && Number.isFinite(penaltyRaw) && penaltyRaw > 0 ? Math.floor(penaltyRaw) : 0;
458
- const list = buckets.get(tierPriority) ?? [];
459
- list.push({ key, penalty, order: order++ });
460
- buckets.set(tierPriority, list);
461
- }
462
- const sortedPriorities = Array.from(buckets.keys()).sort((a, b) => a - b);
463
- for (const priority of sortedPriorities) {
464
- const bucket = buckets.get(priority) ?? [];
465
- if (!bucket.length) {
466
- continue;
467
- }
468
- bucket.sort((a, b) => (a.penalty - b.penalty) || (a.order - b.order));
469
- const bucketCandidates = bucket.map((item) => item.key);
470
- // antigravity special: avoid rotating across keys while the current key is healthy.
471
- // Rationale: some upstream gateways reject rapid cross-key switching even when quota exists,
472
- // causing repeated 429s. We therefore pin a single key per (providerId, modelId) until it is
473
- // excluded by quota/cooldown, then fail over to the next available key.
474
- //
475
- // This is only applied when the request has no session-level sticky key, to avoid breaking
476
- // explicit session stickiness.
477
- const shouldPinAntigravityModel = (() => {
478
- // Only respect explicit session/conversation stickiness. requestId-scoped sticky keys
479
- // (used for request-chain pinning) should not prevent global antigravity key pinning.
480
- if (typeof stickyKey === 'string' && (stickyKey.startsWith('session:') || stickyKey.startsWith('conversation:'))) {
481
- return false;
482
- }
483
- if (bucketCandidates.length < 2) {
484
- return false;
485
- }
486
- let modelId = null;
487
- for (const key of bucketCandidates) {
488
- const providerId = extractProviderId(key);
489
- if (providerId !== 'antigravity') {
490
- return false;
491
- }
492
- const candidateModel = getProviderModelId(key, deps.providerRegistry);
493
- if (!candidateModel) {
494
- return false;
495
- }
496
- if (modelId === null) {
497
- modelId = candidateModel;
498
- }
499
- else if (modelId !== candidateModel) {
500
- return false;
501
- }
502
- }
503
- return Boolean(modelId);
504
- })();
505
- if (shouldPinAntigravityModel && !isRecoveryAttempt) {
506
- const pinned = selectFirstAvailable(bucketCandidates);
507
- if (pinned) {
508
- return pinned;
509
- }
510
- }
511
- const bucketWeights = {};
512
- const bucketMultipliers = {};
513
- for (const item of bucket) {
514
- if (healthWeightedCfg.enabled) {
515
- const entry = quotaView(item.key);
516
- const { weight, multiplier } = computeHealthWeight(entry, now, healthWeightedCfg);
517
- bucketWeights[item.key] = weight;
518
- bucketMultipliers[item.key] = multiplier;
519
- }
520
- else {
521
- // Legacy: penalty => lower weight, but never zero (unhealthy should still get a chance).
522
- bucketWeights[item.key] = Math.max(1, Math.floor(100 / (1 + Math.max(0, item.penalty))));
523
- bucketMultipliers[item.key] = 1;
524
- }
525
- }
526
- if (tier.mode === 'priority') {
527
- if (isRecoveryAttempt && healthWeightedCfg.enabled && healthWeightedCfg.recoverToBestOnRetry) {
528
- let best = null;
529
- let bestM = Number.NEGATIVE_INFINITY;
530
- for (const key of bucketCandidates) {
531
- if (!deps.healthManager.isAvailable(key))
532
- continue;
533
- const m = bucketMultipliers[key] ?? 1;
534
- if (m > bestM) {
535
- bestM = m;
536
- best = key;
537
- }
538
- }
539
- if (best) {
540
- return best;
541
- }
542
- continue;
543
- }
544
- else if (isRecoveryAttempt) {
545
- const recovered = selectFirstAvailable(bucketCandidates);
546
- if (recovered)
547
- return recovered;
548
- continue;
549
- }
550
- const selected = deps.loadBalancer.select({
551
- routeName: `${routeName}:${tier.id}:priority:${priority}`,
552
- candidates: bucketCandidates,
553
- stickyKey: options.allowAliasRotation ? undefined : stickyKey,
554
- weights: bucketWeights,
555
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
556
- }, 'round-robin');
557
- if (selected) {
558
- return selected;
559
- }
560
- }
561
- else {
562
- if (isRecoveryAttempt && healthWeightedCfg.enabled && healthWeightedCfg.recoverToBestOnRetry) {
563
- let best = null;
564
- let bestM = Number.NEGATIVE_INFINITY;
565
- for (const key of bucketCandidates) {
566
- if (!deps.healthManager.isAvailable(key))
567
- continue;
568
- const m = bucketMultipliers[key] ?? 1;
569
- if (m > bestM) {
570
- bestM = m;
571
- best = key;
572
- }
573
- }
574
- if (best) {
575
- return best;
576
- }
577
- continue;
578
- }
579
- else if (isRecoveryAttempt) {
580
- const recovered = selectFirstAvailable(bucketCandidates);
581
- if (recovered)
582
- return recovered;
583
- continue;
584
- }
585
- const selected = deps.loadBalancer.select({
586
- routeName: `${routeName}:${tier.id}`,
587
- candidates: bucketCandidates,
588
- stickyKey: options.allowAliasRotation ? undefined : stickyKey,
589
- weights: bucketWeights,
590
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
591
- }, tier.mode === 'round-robin' ? 'round-robin' : undefined);
592
- if (selected) {
593
- return selected;
594
- }
595
- }
596
- }
597
- return null;
598
- };
599
- for (const candidatePool of prioritizedPools) {
600
- const providerKey = selectWithQuota(candidatePool);
601
- if (providerKey) {
602
- return { providerKey, poolTargets: tier.targets, tierId: tier.id };
603
- }
604
- }
605
- return {
606
- providerKey: null,
607
- poolTargets: tier.targets,
608
- tierId: tier.id,
609
- failureHint: describeAttempt(routeName, tier.id, contextResult)
610
- };
611
- }
612
- export function selectFromStickyPool(stickyKeySet, metadata, features, state, deps, options) {
613
- if (!stickyKeySet || stickyKeySet.size === 0) {
614
- return null;
615
- }
616
- const allowedProviders = new Set(state.allowedProviders);
617
- const disabledProviders = new Set(state.disabledProviders);
618
- const disabledKeysMap = new Map(Array.from(state.disabledKeys.entries()).map(([provider, keys]) => [
619
- provider,
620
- new Set(Array.from(keys).map((k) => (typeof k === 'string' ? k : k + 1)))
621
- ]));
622
- const disabledModels = new Map(Array.from(state.disabledModels.entries()).map(([provider, models]) => [provider, new Set(models)]));
623
- let candidates = Array.from(stickyKeySet).filter((key) => !deps.isProviderCoolingDown(key));
624
- if (!candidates.length && stickyKeySet.size === 1) {
625
- candidates = Array.from(stickyKeySet);
626
- }
627
- const quotaView = deps.quotaView;
628
- const now = quotaView ? Date.now() : 0;
629
- if (quotaView) {
630
- const filtered = candidates.filter((key) => {
631
- const entry = quotaView(key);
632
- if (!entry) {
633
- return true;
634
- }
635
- if (!entry.inPool) {
636
- return false;
637
- }
638
- if (entry.cooldownUntil && entry.cooldownUntil > now) {
639
- return false;
640
- }
641
- if (entry.blacklistUntil && entry.blacklistUntil > now) {
642
- return false;
643
- }
644
- return true;
645
- });
646
- if (filtered.length > 0 || candidates.length !== 1) {
647
- candidates = filtered;
648
- }
649
- }
650
- if (allowedProviders.size > 0) {
651
- candidates = candidates.filter((key) => {
652
- const providerId = extractProviderId(key);
653
- return providerId && allowedProviders.has(providerId);
654
- });
655
- }
656
- if (disabledProviders.size > 0) {
657
- candidates = candidates.filter((key) => {
658
- const providerId = extractProviderId(key);
659
- return providerId && !disabledProviders.has(providerId);
660
- });
661
- }
662
- if (disabledKeysMap.size > 0 || disabledModels.size > 0) {
663
- candidates = candidates.filter((key) => {
664
- const providerId = extractProviderId(key);
665
- if (!providerId) {
666
- return true;
667
- }
668
- const disabledKeys = disabledKeysMap.get(providerId);
669
- if (disabledKeys && disabledKeys.size > 0) {
670
- const keyAlias = extractKeyAlias(key);
671
- const keyIndex = extractKeyIndex(key);
672
- if (keyAlias && disabledKeys.has(keyAlias)) {
673
- return false;
674
- }
675
- if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
676
- return false;
677
- }
678
- }
679
- const disabledModelSet = disabledModels.get(providerId);
680
- if (disabledModelSet && disabledModelSet.size > 0) {
681
- const modelId = getProviderModelId(key, deps.providerRegistry);
682
- if (modelId && disabledModelSet.has(modelId)) {
683
- return false;
684
- }
685
- }
686
- return true;
687
- });
688
- }
689
- if (!candidates.length) {
690
- return null;
691
- }
692
- const stickyKey = options.allowAliasRotation ? undefined : deps.resolveStickyKey(metadata);
693
- const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
694
- ? Math.max(0, features.estimatedTokens)
695
- : 0;
696
- const tier = {
697
- id: 'sticky-primary',
698
- targets: candidates,
699
- priority: 0
700
- };
701
- const { providerKey, poolTargets, tierId } = trySelectFromTier('sticky', tier, stickyKey, estimatedTokens, features, deps, {
702
- disabledProviders,
703
- disabledKeysMap,
704
- allowedProviders,
705
- disabledModels,
706
- requiredProviderKeys: stickyKeySet,
707
- allowAliasRotation: options.allowAliasRotation
708
- });
709
- if (!providerKey) {
710
- return null;
711
- }
712
- return {
713
- providerKey,
714
- routeUsed: 'sticky',
715
- pool: poolTargets,
716
- poolId: tierId
717
- };
718
- }
719
- function filterCandidatesByRoutingState(routes, state, routing, providerRegistry) {
720
- if (state.allowedProviders.size === 0 &&
721
- state.disabledProviders.size === 0 &&
722
- state.disabledKeys.size === 0 &&
723
- state.disabledModels.size === 0) {
724
- return routes;
725
- }
726
- return routes.filter((routeName) => {
727
- const pools = routing[routeName];
728
- if (!pools)
729
- return false;
730
- for (const pool of pools) {
731
- if (!Array.isArray(pool.targets) || pool.targets.length === 0) {
732
- continue;
733
- }
734
- for (const providerKey of pool.targets) {
735
- const providerId = extractProviderId(providerKey);
736
- if (!providerId)
737
- continue;
738
- if (state.allowedProviders.size > 0 && !state.allowedProviders.has(providerId)) {
739
- continue;
740
- }
741
- if (state.disabledProviders.has(providerId)) {
742
- continue;
743
- }
744
- const disabledKeys = state.disabledKeys.get(providerId);
745
- if (disabledKeys && disabledKeys.size > 0) {
746
- const keyAlias = extractKeyAlias(providerKey);
747
- const keyIndex = extractKeyIndex(providerKey);
748
- if (keyAlias && disabledKeys.has(keyAlias)) {
749
- continue;
750
- }
751
- if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
752
- continue;
753
- }
754
- }
755
- const disabledModels = state.disabledModels.get(providerId);
756
- if (disabledModels && disabledModels.size > 0) {
757
- const modelId = getProviderModelId(providerKey, providerRegistry);
758
- if (modelId && disabledModels.has(modelId)) {
759
- continue;
760
- }
761
- }
762
- return true;
763
- }
764
- }
765
- return false;
766
- });
767
- }
768
- function buildRouteCandidates(requestedRoute, classificationCandidates, features, routing, providerRegistry) {
769
- const forceVision = routeHasForceFlag('vision', routing);
770
- const normalized = normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
771
- const baseList = [];
772
- if (classificationCandidates && classificationCandidates.length) {
773
- for (const candidate of classificationCandidates) {
774
- baseList.push(normalizeRouteAlias(candidate));
775
- }
776
- }
777
- else if (normalized) {
778
- baseList.push(normalized);
779
- }
780
- if (features.hasImageAttachment && !forceVision) {
781
- const visionAwareRoutes = [DEFAULT_ROUTE, 'thinking'];
782
- for (const routeName of visionAwareRoutes) {
783
- if (routeHasTargets(routing[routeName])) {
784
- if (!baseList.includes(routeName)) {
785
- baseList.push(routeName);
786
- }
787
- }
788
- }
789
- }
790
- let ordered = sortByPriority(baseList);
791
- if (features.hasImageAttachment && !forceVision) {
792
- ordered = reorderForInlineVision(ordered, routing, providerRegistry);
793
- }
794
- const deduped = [];
795
- for (const routeName of ordered) {
796
- if (routeName && !deduped.includes(routeName)) {
797
- deduped.push(routeName);
798
- }
799
- }
800
- if (!deduped.includes(DEFAULT_ROUTE)) {
801
- deduped.push(DEFAULT_ROUTE);
802
- }
803
- const filtered = deduped.filter((routeName) => routeHasTargets(routing[routeName]));
804
- if (!filtered.includes(DEFAULT_ROUTE) && routeHasTargets(routing[DEFAULT_ROUTE])) {
805
- filtered.push(DEFAULT_ROUTE);
806
- }
807
- return filtered.length ? filtered : [DEFAULT_ROUTE];
808
- }
809
- function reorderForInlineVision(routeNames, routing, providerRegistry) {
810
- const unique = Array.from(new Set(routeNames.filter(Boolean)));
811
- if (!unique.length) {
812
- return unique;
813
- }
814
- const inlinePreferred = [];
815
- const inlineRoutes = [DEFAULT_ROUTE, 'thinking'];
816
- for (const routeName of inlineRoutes) {
817
- if (routeSupportsInlineVision(routeName, routing, providerRegistry) && !inlinePreferred.includes(routeName)) {
818
- inlinePreferred.push(routeName);
819
- }
820
- }
821
- if (!inlinePreferred.length) {
822
- return unique;
823
- }
824
- const remaining = [];
825
- for (const routeName of unique) {
826
- if (!inlinePreferred.includes(routeName)) {
827
- remaining.push(routeName);
828
- }
829
- }
830
- return [...inlinePreferred, ...remaining];
831
- }
832
- function routeSupportsInlineVision(routeName, routing, providerRegistry) {
833
- const pools = routing[routeName];
834
- if (!Array.isArray(pools)) {
835
- return false;
836
- }
837
- for (const pool of pools) {
838
- if (!Array.isArray(pool.targets)) {
839
- continue;
840
- }
841
- for (const providerKey of pool.targets) {
842
- try {
843
- const profile = providerRegistry.get(providerKey);
844
- if (profile.providerType === 'responses' || profile.providerType === 'gemini') {
845
- return true;
846
- }
847
- }
848
- catch {
849
- // ignore unknown providers when probing capabilities
850
- }
851
- }
852
- }
853
- return false;
854
- }
855
- function normalizeRouteAlias(routeName) {
856
- const base = routeName && routeName.trim() ? routeName.trim() : DEFAULT_ROUTE;
857
- return base;
858
- }
859
- function routeHasForceFlag(routeName, routing) {
860
- const pools = routing[routeName];
861
- if (!Array.isArray(pools)) {
862
- return false;
863
- }
864
- return pools.some((pool) => pool.force);
865
- }
866
- function routeHasTargets(pools) {
867
- if (!Array.isArray(pools)) {
868
- return false;
869
- }
870
- return pools.some((pool) => Array.isArray(pool.targets) && pool.targets.length > 0);
871
- }
872
- function sortRoutePools(pools) {
873
- if (!Array.isArray(pools)) {
874
- return [];
875
- }
876
- return pools
877
- .filter((pool) => Array.isArray(pool.targets) && pool.targets.length > 0)
878
- .sort((a, b) => {
879
- if (a.backup && !b.backup)
880
- return 1;
881
- if (!a.backup && b.backup)
882
- return -1;
883
- if (a.priority !== b.priority) {
884
- return b.priority - a.priority;
885
- }
886
- return a.id.localeCompare(b.id);
887
- });
888
- }
889
- function initializeRouteQueue(candidates) {
890
- return Array.from(new Set(candidates));
891
- }
892
- function buildContextCandidatePools(result) {
893
- const ordered = [];
894
- if (result.safe.length) {
895
- ordered.push(result.safe);
896
- }
897
- if (result.risky.length) {
898
- ordered.push(result.risky);
899
- }
900
- return ordered;
901
- }
902
- function describeAttempt(routeName, poolId, result) {
903
- const prefix = poolId ? `${routeName}:${poolId}` : routeName;
904
- if (result.safe.length > 0) {
905
- return `${prefix}:health`;
906
- }
907
- if (result.risky.length > 0) {
908
- return `${prefix}:context_risky`;
909
- }
910
- if (result.overflow.length > 0) {
911
- return `${prefix}:max_context_window`;
912
- }
913
- return prefix;
914
- }
915
- function extractProviderId(providerKey) {
916
- const firstDot = providerKey.indexOf('.');
917
- if (firstDot <= 0)
918
- return null;
919
- return providerKey.substring(0, firstDot);
920
- }
921
- function extractKeyAlias(providerKey) {
922
- const parts = providerKey.split('.');
923
- if (parts.length === 3) {
924
- return normalizeAliasDescriptor(parts[1]);
925
- }
926
- return null;
927
- }
928
- function normalizeAliasDescriptor(alias) {
929
- if (/^\d+-/.test(alias)) {
930
- return alias.replace(/^\d+-/, '');
931
- }
932
- return alias;
933
- }
934
- function extractKeyIndex(providerKey) {
935
- const parts = providerKey.split('.');
936
- if (parts.length === 2) {
937
- const index = parseInt(parts[1], 10);
938
- if (!isNaN(index) && index > 0) {
939
- return index;
940
- }
941
- }
942
- return undefined;
943
- }
944
- function getProviderModelId(providerKey, providerRegistry) {
945
- const profile = providerRegistry.get(providerKey);
946
- if (profile.modelId) {
947
- return profile.modelId;
948
- }
949
- const parts = providerKey.split('.');
950
- if (parts.length === 2) {
951
- return parts[1] || null;
952
- }
953
- if (parts.length === 3) {
954
- return parts[2] || null;
955
- }
956
- return null;
957
- }
958
- function extractExcludedProviderKeySet(metadata) {
959
- if (!metadata) {
960
- return new Set();
961
- }
962
- const raw = metadata.excludedProviderKeys;
963
- if (!Array.isArray(raw) || raw.length === 0) {
964
- return new Set();
965
- }
966
- const normalized = raw
967
- .map((value) => (typeof value === 'string' ? value.trim() : ''))
968
- .filter((value) => Boolean(value));
969
- return new Set(normalized);
970
- }
971
- function sortByPriority(routeNames) {
972
- return [...routeNames].sort((a, b) => routeWeight(a) - routeWeight(b));
973
- }
974
- function routeWeight(routeName) {
975
- const idx = ROUTE_PRIORITY.indexOf(routeName);
976
- return idx >= 0 ? idx : ROUTE_PRIORITY.length;
977
- }
978
- function resolveInstructionTarget(target, providerRegistry) {
979
- if (!target || !target.provider) {
980
- return null;
981
- }
982
- const providerId = target.provider;
983
- const providerKeys = providerRegistry.listProviderKeys(providerId);
984
- if (providerKeys.length === 0) {
985
- return null;
986
- }
987
- const alias = typeof target.keyAlias === 'string' ? target.keyAlias.trim() : '';
988
- const aliasExplicit = alias.length > 0 && target.pathLength === 3;
989
- if (aliasExplicit) {
990
- const prefix = `${providerId}.${alias}.`;
991
- const aliasKeys = providerKeys.filter((key) => key.startsWith(prefix));
992
- if (aliasKeys.length > 0) {
993
- if (target.model && target.model.trim()) {
994
- const normalizedModel = target.model.trim();
995
- const matching = aliasKeys.filter((key) => getProviderModelId(key, providerRegistry) === normalizedModel);
996
- if (matching.length > 0) {
997
- // Prefer exact to keep sticky pool deterministic when only one key matches.
998
- if (matching.length === 1) {
999
- return { mode: 'exact', keys: [matching[0]] };
1000
- }
1001
- return { mode: 'filter', keys: matching };
1002
- }
1003
- }
1004
- return { mode: 'filter', keys: aliasKeys };
1005
- }
1006
- }
1007
- if (typeof target.keyIndex === 'number' && target.keyIndex > 0) {
1008
- const runtimeKey = providerRegistry.resolveRuntimeKeyByIndex(providerId, target.keyIndex);
1009
- if (runtimeKey) {
1010
- return { mode: 'exact', keys: [runtimeKey] };
1011
- }
1012
- }
1013
- if (target.model && target.model.trim()) {
1014
- const normalizedModel = target.model.trim();
1015
- const matchingKeys = providerKeys.filter((key) => {
1016
- const modelId = getProviderModelId(key, providerRegistry);
1017
- return modelId === normalizedModel;
1018
- });
1019
- if (matchingKeys.length > 0) {
1020
- return { mode: 'filter', keys: matchingKeys };
1021
- }
1022
- }
1023
- if (alias && !aliasExplicit) {
1024
- const legacyKey = providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
1025
- if (legacyKey) {
1026
- return { mode: 'exact', keys: [legacyKey] };
1027
- }
1028
- }
1029
- return { mode: 'filter', keys: providerKeys };
1030
- }