@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.
- package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
- package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
- package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
- package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
- package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
- package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
- package/dist/conversion/hub/pipeline/target-utils.js +9 -5
- package/dist/conversion/hub/process/chat-process.js +256 -16
- package/dist/conversion/hub/response/provider-response.d.ts +8 -0
- package/dist/conversion/hub/response/provider-response.js +85 -27
- package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
- package/dist/conversion/hub/response/response-mappers.js +30 -6
- package/dist/conversion/hub/response/response-runtime.js +4 -38
- package/dist/conversion/hub/snapshot-recorder.js +5 -1
- package/dist/conversion/hub/standardized-bridge.js +23 -15
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
- package/dist/conversion/responses/responses-openai-bridge.js +20 -4
- package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
- package/dist/conversion/shared/gemini-tool-utils.js +580 -108
- package/dist/conversion/shared/jsonish.js +1 -1
- package/dist/conversion/shared/mcp-injection.js +67 -33
- package/dist/conversion/shared/openai-finalizer.js +2 -1
- package/dist/conversion/shared/openai-message-normalize.js +76 -21
- package/dist/conversion/shared/responses-output-builder.js +6 -0
- package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
- package/dist/conversion/shared/runtime-metadata.js +23 -0
- package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer.js +284 -4
- package/dist/conversion/shared/tool-canonicalizer.js +2 -1
- package/dist/conversion/shared/tool-governor.js +3 -3
- package/dist/filters/engine.js +5 -5
- package/dist/filters/special/request-tool-list-filter.js +194 -60
- package/dist/filters/special/request-tools-normalize.js +1 -1
- package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
- package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
- package/dist/filters/special/tool-filter-hooks.js +58 -62
- package/dist/guidance/index.js +5 -1
- package/dist/http/sse-response.js +6 -6
- package/dist/router/virtual-router/bootstrap.js +65 -5
- package/dist/router/virtual-router/context-advisor.d.ts +4 -0
- package/dist/router/virtual-router/context-advisor.js +3 -0
- package/dist/router/virtual-router/context-weighted.d.ts +31 -0
- package/dist/router/virtual-router/context-weighted.js +54 -0
- package/dist/router/virtual-router/engine-health.d.ts +1 -1
- package/dist/router/virtual-router/engine-health.js +11 -110
- package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
- package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
- package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
- package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
- package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
- package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
- package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
- package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
- package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
- package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
- package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
- package/dist/router/virtual-router/engine-selection.d.ts +4 -30
- package/dist/router/virtual-router/engine-selection.js +10 -815
- package/dist/router/virtual-router/engine.d.ts +1 -0
- package/dist/router/virtual-router/engine.js +55 -10
- package/dist/router/virtual-router/routing-instructions.js +6 -1
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
- package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
- package/dist/router/virtual-router/types.d.ts +53 -1
- package/dist/servertool/clock/config.d.ts +8 -0
- package/dist/servertool/clock/config.js +22 -0
- package/dist/servertool/clock/log.d.ts +3 -0
- package/dist/servertool/clock/log.js +13 -0
- package/dist/servertool/clock/task-store.d.ts +1 -1
- package/dist/servertool/clock/task-store.js +1 -1
- package/dist/servertool/clock/tasks.js +1 -1
- package/dist/servertool/engine.js +146 -21
- package/dist/servertool/handlers/clock-auto.js +11 -6
- package/dist/servertool/handlers/clock.js +36 -10
- package/dist/servertool/handlers/followup-request-builder.js +8 -2
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
- package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
- package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
- package/dist/servertool/handlers/stop-message-auto.js +100 -10
- package/dist/servertool/handlers/vision.js +4 -1
- package/dist/servertool/handlers/web-search.js +3 -1
- package/dist/servertool/pending-session.d.ts +19 -0
- package/dist/servertool/pending-session.js +97 -0
- package/dist/servertool/reenter-backend.js +5 -3
- package/dist/servertool/server-side-tools.js +235 -6
- package/dist/servertool/types.d.ts +13 -0
- package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
- package/dist/sse/shared/chat-serializer.js +2 -2
- package/dist/sse/shared/constants.js +1 -1
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
- package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
- package/dist/tools/apply-patch/execution-capturer.js +1 -1
- package/dist/tools/exec-command/normalize.js +4 -0
- package/dist/tools/exec-command/regression-capturer.js +1 -1
- package/package.json +10 -5
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 =
|
|
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
|
-
}
|