@jsonstudio/llms 0.6.2125 → 0.6.2172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversion/compat/actions/deepseek-web-response.js +27 -3
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +9 -3
- package/dist/conversion/hub/process/chat-process.js +15 -18
- package/dist/conversion/responses/responses-openai-bridge.js +13 -12
- package/dist/conversion/shared/bridge-message-utils.js +92 -39
- package/dist/router/virtual-router/classifier.js +29 -5
- package/dist/router/virtual-router/engine/routing-pools/index.js +111 -5
- package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -0
- package/dist/router/virtual-router/engine.d.ts +2 -0
- package/dist/router/virtual-router/engine.js +57 -14
- package/dist/router/virtual-router/features.js +12 -4
- package/dist/router/virtual-router/message-utils.d.ts +8 -0
- package/dist/router/virtual-router/message-utils.js +170 -45
- package/dist/router/virtual-router/token-counter.js +51 -10
- package/dist/router/virtual-router/types.d.ts +3 -0
- package/dist/servertool/clock/session-scope.d.ts +3 -0
- package/dist/servertool/clock/session-scope.js +52 -0
- package/dist/servertool/engine.js +68 -8
- package/dist/servertool/handlers/clock-auto.js +2 -8
- package/dist/servertool/handlers/clock.js +3 -9
- package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
- package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
- package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
- package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
- package/dist/servertool/handlers/stop-message-auto.d.ts +1 -7
- package/dist/servertool/handlers/stop-message-auto.js +69 -971
- package/dist/servertool/handlers/web-search.js +117 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_ROUTE, VirtualRouterError, VirtualRouterErrorCode } from '../../types.js';
|
|
2
|
-
import { extractExcludedProviderKeySet, extractProviderId } from '../../engine-selection/key-parsing.js';
|
|
2
|
+
import { extractExcludedProviderKeySet, extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from '../../engine-selection/key-parsing.js';
|
|
3
|
+
import { providerSupportsMultimodalRequest } from '../../engine-selection/multimodal-capability.js';
|
|
3
4
|
import { trySelectFromTier } from '../../engine-selection/tier-selection.js';
|
|
4
5
|
import { resolveInstructionTarget } from '../../engine-selection/instruction-target.js';
|
|
5
6
|
import { filterCandidatesByRoutingState } from '../../engine-selection/routing-state-filter.js';
|
|
@@ -49,7 +50,7 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
|
|
|
49
50
|
stickyResolution = resolveInstructionTarget(state.stickyTarget, deps.providerRegistry);
|
|
50
51
|
if (stickyResolution && stickyResolution.mode === 'exact') {
|
|
51
52
|
const stickyKey = stickyResolution.keys[0];
|
|
52
|
-
if (stickyProviderMatchesRequestCapabilities(stickyKey, requestedRoute, classification, features, deps.routing) &&
|
|
53
|
+
if (stickyProviderMatchesRequestCapabilities(stickyKey, requestedRoute, classification, features, deps.routing, deps.providerRegistry) &&
|
|
53
54
|
(deps.quotaView ? true : deps.healthManager.isAvailable(stickyKey)) &&
|
|
54
55
|
!excludedProviderKeys.has(stickyKey) &&
|
|
55
56
|
!deps.isProviderCoolingDown(stickyKey) &&
|
|
@@ -63,7 +64,7 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
if (stickyResolution && stickyResolution.mode === 'filter' && stickyResolution.keys.length > 0) {
|
|
66
|
-
const liveKeys = stickyResolution.keys.filter((key) => stickyProviderMatchesRequestCapabilities(key, requestedRoute, classification, features, deps.routing) &&
|
|
67
|
+
const liveKeys = stickyResolution.keys.filter((key) => stickyProviderMatchesRequestCapabilities(key, requestedRoute, classification, features, deps.routing, deps.providerRegistry) &&
|
|
67
68
|
(deps.quotaView ? true : deps.healthManager.isAvailable(key)) &&
|
|
68
69
|
!excludedProviderKeys.has(key) &&
|
|
69
70
|
!deps.isProviderCoolingDown(key) &&
|
|
@@ -177,7 +178,7 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
|
|
|
177
178
|
allowAliasRotation
|
|
178
179
|
});
|
|
179
180
|
}
|
|
180
|
-
function stickyProviderMatchesRequestCapabilities(providerKey, requestedRoute, classification, features, routing) {
|
|
181
|
+
function stickyProviderMatchesRequestCapabilities(providerKey, requestedRoute, classification, features, routing, providerRegistry) {
|
|
181
182
|
if (!providerKey) {
|
|
182
183
|
return false;
|
|
183
184
|
}
|
|
@@ -187,6 +188,9 @@ function stickyProviderMatchesRequestCapabilities(providerKey, requestedRoute, c
|
|
|
187
188
|
if (!supportsImageRoute) {
|
|
188
189
|
return false;
|
|
189
190
|
}
|
|
191
|
+
if (!providerSupportsMultimodalRequest(providerKey, features, providerRegistry)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
190
194
|
}
|
|
191
195
|
if (requestRequiresSearchRoute(requestedRoute, classification, features)) {
|
|
192
196
|
const supportsSearchRoute = routeTargetsIncludeProvider(routing, 'web_search', providerKey) ||
|
|
@@ -230,9 +234,13 @@ function routeTargetsIncludeProvider(routing, routeName, providerKey) {
|
|
|
230
234
|
}
|
|
231
235
|
return false;
|
|
232
236
|
}
|
|
237
|
+
function isFinitePositiveNumber(value) {
|
|
238
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
239
|
+
}
|
|
233
240
|
function selectFromCandidates(routes, metadata, classification, features, state, deps, options) {
|
|
234
241
|
const allowedProviders = new Set(state.allowedProviders);
|
|
235
242
|
const disabledProviders = new Set(state.disabledProviders);
|
|
243
|
+
const excludedProviderKeys = extractExcludedProviderKeySet(features.metadata);
|
|
236
244
|
const disabledKeysMap = new Map(Array.from(state.disabledKeys.entries()).map(([provider, keys]) => [
|
|
237
245
|
provider,
|
|
238
246
|
new Set(Array.from(keys).map((k) => (typeof k === 'string' ? k : k + 1)))
|
|
@@ -242,6 +250,89 @@ function selectFromCandidates(routes, metadata, classification, features, state,
|
|
|
242
250
|
const attempted = [];
|
|
243
251
|
const visitedRoutes = new Set();
|
|
244
252
|
const routeQueue = initializeRouteQueue(routes);
|
|
253
|
+
const healthSnapshotByProviderKey = new Map(deps.healthManager.getSnapshot().map((entry) => [entry.providerKey, entry]));
|
|
254
|
+
let minRecoverableCooldownMs;
|
|
255
|
+
const recoverableCooldownHints = [];
|
|
256
|
+
const recordRecoverableCooldown = (providerKey, waitMsRaw, source) => {
|
|
257
|
+
const waitMs = Math.max(1, Math.floor(waitMsRaw));
|
|
258
|
+
if (!isFinitePositiveNumber(waitMs)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!Number.isFinite(minRecoverableCooldownMs) || waitMs < minRecoverableCooldownMs) {
|
|
262
|
+
minRecoverableCooldownMs = waitMs;
|
|
263
|
+
}
|
|
264
|
+
const existing = recoverableCooldownHints.find((item) => item.providerKey === providerKey && item.source === source);
|
|
265
|
+
if (!existing) {
|
|
266
|
+
recoverableCooldownHints.push({ providerKey, waitMs, source });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (waitMs < existing.waitMs) {
|
|
270
|
+
existing.waitMs = waitMs;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const collectRecoverableCooldownForKey = (providerKey) => {
|
|
274
|
+
const nowMs = Date.now();
|
|
275
|
+
if (deps.quotaView) {
|
|
276
|
+
const entry = deps.quotaView(providerKey);
|
|
277
|
+
if (!entry) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (isFinitePositiveNumber(entry.blacklistUntil) && entry.blacklistUntil > nowMs) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (isFinitePositiveNumber(entry.cooldownUntil) && entry.cooldownUntil > nowMs) {
|
|
284
|
+
recordRecoverableCooldown(providerKey, entry.cooldownUntil - nowMs, 'quota.cooldown');
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (typeof deps.getProviderCooldownRemainingMs === 'function') {
|
|
289
|
+
const localCooldownMs = deps.getProviderCooldownRemainingMs(providerKey);
|
|
290
|
+
if (isFinitePositiveNumber(localCooldownMs)) {
|
|
291
|
+
recordRecoverableCooldown(providerKey, localCooldownMs, 'router.cooldown');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const healthState = healthSnapshotByProviderKey.get(providerKey);
|
|
295
|
+
if (healthState && isFinitePositiveNumber(healthState.cooldownExpiresAt) && healthState.cooldownExpiresAt > nowMs) {
|
|
296
|
+
recordRecoverableCooldown(providerKey, healthState.cooldownExpiresAt - nowMs, 'health.cooldown');
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const isEligibleTargetForCurrentAttempt = (providerKey) => {
|
|
300
|
+
if (!providerKey || excludedProviderKeys.has(providerKey)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (options.requiredProviderKeys && options.requiredProviderKeys.size > 0 && !options.requiredProviderKeys.has(providerKey)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const providerId = extractProviderId(providerKey);
|
|
307
|
+
if (!providerId) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
if (allowedProviders.size > 0 && !allowedProviders.has(providerId)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (disabledProviders.has(providerId)) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const disabledKeys = disabledKeysMap.get(providerId);
|
|
317
|
+
if (disabledKeys && disabledKeys.size > 0) {
|
|
318
|
+
const keyAlias = extractKeyAlias(providerKey);
|
|
319
|
+
const keyIndex = extractKeyIndex(providerKey);
|
|
320
|
+
if (keyAlias && disabledKeys.has(keyAlias)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const disabledModelSet = disabledModels.get(providerId);
|
|
328
|
+
if (disabledModelSet && disabledModelSet.size > 0) {
|
|
329
|
+
const modelId = getProviderModelId(providerKey, deps.providerRegistry);
|
|
330
|
+
if (modelId && disabledModelSet.has(modelId)) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
};
|
|
245
336
|
const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
|
|
246
337
|
? Math.max(0, features.estimatedTokens)
|
|
247
338
|
: 0;
|
|
@@ -273,8 +364,23 @@ function selectFromCandidates(routes, metadata, classification, features, state,
|
|
|
273
364
|
if (failureHint) {
|
|
274
365
|
attempted.push(failureHint);
|
|
275
366
|
}
|
|
367
|
+
if (Array.isArray(poolTier.targets) && poolTier.targets.length > 0) {
|
|
368
|
+
for (const providerKey of poolTier.targets) {
|
|
369
|
+
if (!isEligibleTargetForCurrentAttempt(providerKey)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
collectRecoverableCooldownForKey(providerKey);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
276
375
|
}
|
|
277
376
|
}
|
|
278
377
|
const requestedRoute = normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
|
|
279
|
-
|
|
378
|
+
const details = { routeName: requestedRoute, attempted };
|
|
379
|
+
if (isFinitePositiveNumber(minRecoverableCooldownMs)) {
|
|
380
|
+
details.minRecoverableCooldownMs = Math.floor(minRecoverableCooldownMs);
|
|
381
|
+
details.recoverableCooldownHints = recoverableCooldownHints
|
|
382
|
+
.sort((a, b) => a.waitMs - b.waitMs)
|
|
383
|
+
.slice(0, 8);
|
|
384
|
+
}
|
|
385
|
+
throw new VirtualRouterError(`All providers unavailable for route ${requestedRoute}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, details);
|
|
280
386
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { extractProviderId, getProviderModelId } from './key-parsing.js';
|
|
2
|
+
function isQwen35PlusProvider(providerKey, providerRegistry) {
|
|
3
|
+
const providerId = (extractProviderId(providerKey) ?? '').trim().toLowerCase();
|
|
4
|
+
if (providerId !== 'qwen') {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const modelId = (getProviderModelId(providerKey, providerRegistry) ?? '').trim().toLowerCase();
|
|
8
|
+
if (!modelId) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return modelId === 'qwen3.5-plus' || modelId === 'qwen3-5-plus' || modelId === 'qwen3_5-plus';
|
|
12
|
+
}
|
|
13
|
+
export function providerSupportsMultimodalRequest(providerKey, features, providerRegistry) {
|
|
14
|
+
if (!features.hasImageAttachment) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (!isQwen35PlusProvider(providerKey, providerRegistry)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (features.hasVideoAttachment !== true) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const hasRemoteVideo = features.hasRemoteVideoAttachment === true;
|
|
24
|
+
const hasLocalVideo = features.hasLocalVideoAttachment === true;
|
|
25
|
+
return hasRemoteVideo && !hasLocalVideo;
|
|
26
|
+
}
|
|
@@ -43,6 +43,10 @@ export function buildRouteCandidates(requestedRoute, classificationCandidates, f
|
|
|
43
43
|
const forceVision = routeHasForceFlag('vision', routing);
|
|
44
44
|
const hasMultimodalTargets = routeHasTargets(routing.multimodal);
|
|
45
45
|
const hasVisionTargets = routeHasTargets(routing.vision);
|
|
46
|
+
const hasLocalVideoAttachment = features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true;
|
|
47
|
+
if (features.hasImageAttachment && hasLocalVideoAttachment && hasVisionTargets) {
|
|
48
|
+
return ['vision'];
|
|
49
|
+
}
|
|
46
50
|
const normalized = normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
|
|
47
51
|
const baseList = [];
|
|
48
52
|
if (classificationCandidates && classificationCandidates.length) {
|
|
@@ -59,9 +63,9 @@ export function buildRouteCandidates(requestedRoute, classificationCandidates, f
|
|
|
59
63
|
baseList.unshift('multimodal');
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
|
-
|
|
66
|
+
if (hasVisionTargets) {
|
|
63
67
|
if (!baseList.includes('vision')) {
|
|
64
|
-
baseList.
|
|
68
|
+
baseList.push('vision');
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
if (!forceVision && hasMultimodalTargets) {
|
|
@@ -10,6 +10,7 @@ export type SelectionDeps = {
|
|
|
10
10
|
contextAdvisor: ContextAdvisor;
|
|
11
11
|
loadBalancer: RouteLoadBalancer;
|
|
12
12
|
isProviderCoolingDown: (providerKey: string) => boolean;
|
|
13
|
+
getProviderCooldownRemainingMs?: (providerKey: string) => number;
|
|
13
14
|
resolveStickyKey: (metadata: RouterMetadataInput) => string | undefined;
|
|
14
15
|
quotaView?: ProviderQuotaView;
|
|
15
16
|
aliasQueueStore?: Map<string, string[]>;
|
|
@@ -3,6 +3,7 @@ import { resolveContextWeightedConfig } from '../context-weighted.js';
|
|
|
3
3
|
import { resolveHealthWeightedConfig } from '../health-weighted.js';
|
|
4
4
|
import { pinCandidatesByAliasQueue, resolveAliasSelectionStrategy } from './alias-selection.js';
|
|
5
5
|
import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './key-parsing.js';
|
|
6
|
+
import { providerSupportsMultimodalRequest } from './multimodal-capability.js';
|
|
6
7
|
import { selectProviderKeyFromCandidatePool } from './tier-selection-select.js';
|
|
7
8
|
import { lookupAntigravityPinnedAliasForSessionId, unpinAntigravitySessionAliasForSessionId } from '../../../conversion/compat/antigravity-session-signature.js';
|
|
8
9
|
const DEFAULT_ANTIGRAVITY_ALIAS_SESSION_COOLDOWN_MS = 5 * 60_000;
|
|
@@ -388,6 +389,7 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
|
|
|
388
389
|
targets = filtered;
|
|
389
390
|
}
|
|
390
391
|
if (features.hasImageAttachment && routeName === 'multimodal') {
|
|
392
|
+
targets = targets.filter((key) => providerSupportsMultimodalRequest(key, features, deps.providerRegistry));
|
|
391
393
|
const kimiTargets = targets.filter((key) => {
|
|
392
394
|
const modelId = getProviderModelId(key, deps.providerRegistry) ?? '';
|
|
393
395
|
return modelId.trim().toLowerCase() === 'kimi-k2.5';
|
|
@@ -44,6 +44,7 @@ export declare class VirtualRouterEngine {
|
|
|
44
44
|
quotaView?: ProviderQuotaView | null;
|
|
45
45
|
}): void;
|
|
46
46
|
private parseDirectProviderModel;
|
|
47
|
+
private shouldFallbackDirectModelForMedia;
|
|
47
48
|
initialize(config: VirtualRouterConfig): void;
|
|
48
49
|
route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
|
|
49
50
|
target: TargetMetadata;
|
|
@@ -117,6 +118,7 @@ export declare class VirtualRouterEngine {
|
|
|
117
118
|
private markProviderCooldown;
|
|
118
119
|
private clearProviderCooldown;
|
|
119
120
|
private isProviderCoolingDown;
|
|
121
|
+
private getProviderCooldownRemainingMs;
|
|
120
122
|
private restoreHealthFromStore;
|
|
121
123
|
private buildHealthSnapshot;
|
|
122
124
|
private persistHealthSnapshot;
|
|
@@ -207,6 +207,24 @@ export class VirtualRouterEngine {
|
|
|
207
207
|
}
|
|
208
208
|
return { providerId, modelId };
|
|
209
209
|
}
|
|
210
|
+
shouldFallbackDirectModelForMedia(direct, features) {
|
|
211
|
+
if (!features.hasImageAttachment) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const providerId = direct.providerId.trim().toLowerCase();
|
|
215
|
+
const modelId = direct.modelId.trim().toLowerCase();
|
|
216
|
+
if (providerId !== 'qwen') {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const isQwen35Plus = modelId === 'qwen3.5-plus' || modelId === 'qwen3-5-plus' || modelId === 'qwen3_5-plus';
|
|
220
|
+
if (!isQwen35Plus) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (!(features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return this.routeHasTargets(this.routing.vision);
|
|
227
|
+
}
|
|
210
228
|
initialize(config) {
|
|
211
229
|
this.validateConfig(config);
|
|
212
230
|
this.routing = config.routing;
|
|
@@ -638,6 +656,7 @@ export class VirtualRouterEngine {
|
|
|
638
656
|
quotaView: this.quotaView
|
|
639
657
|
};
|
|
640
658
|
if (directProviderModel) {
|
|
659
|
+
const forceMediaFallback = this.shouldFallbackDirectModelForMedia(directProviderModel, features);
|
|
641
660
|
const providerKeys = this.providerRegistry.listProviderKeys(directProviderModel.providerId);
|
|
642
661
|
let hasModel = false;
|
|
643
662
|
for (const key of providerKeys) {
|
|
@@ -655,19 +674,26 @@ export class VirtualRouterEngine {
|
|
|
655
674
|
if (!hasModel) {
|
|
656
675
|
throw new VirtualRouterError(`Unknown model ${directProviderModel.modelId} for provider ${directProviderModel.providerId}`, VirtualRouterErrorCode.CONFIG_ERROR, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
|
|
657
676
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
677
|
+
if (!forceMediaFallback) {
|
|
678
|
+
const directSelection = selectDirectProviderModel(directProviderModel.providerId, directProviderModel.modelId, metadata, features, routingState, selectionDeps);
|
|
679
|
+
if (!directSelection) {
|
|
680
|
+
throw new VirtualRouterError(`All providers unavailable for model ${directProviderModel.providerId}.${directProviderModel.modelId}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
|
|
681
|
+
}
|
|
682
|
+
classification = {
|
|
683
|
+
routeName: 'direct',
|
|
684
|
+
confidence: 1,
|
|
685
|
+
reasoning: `direct_model:${directProviderModel.providerId}.${directProviderModel.modelId}`,
|
|
686
|
+
fallback: false,
|
|
687
|
+
candidates: ['direct']
|
|
688
|
+
};
|
|
689
|
+
requestedRoute = 'direct';
|
|
690
|
+
selection = directSelection;
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
classification = this.classifier.classify(features);
|
|
694
|
+
requestedRoute = this.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
|
|
695
|
+
selection = this.selectProvider(requestedRoute, metadata, classification, features, routingState);
|
|
669
696
|
}
|
|
670
|
-
selection = directSelection;
|
|
671
697
|
}
|
|
672
698
|
else {
|
|
673
699
|
// Prefer target (from "<**!provider.model**>") is evaluated before routing classification.
|
|
@@ -1128,6 +1154,7 @@ export class VirtualRouterEngine {
|
|
|
1128
1154
|
contextAdvisor: this.contextAdvisor,
|
|
1129
1155
|
loadBalancer: this.loadBalancer,
|
|
1130
1156
|
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1157
|
+
getProviderCooldownRemainingMs: (key) => this.getProviderCooldownRemainingMs(key),
|
|
1131
1158
|
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1132
1159
|
quotaView: this.quotaView,
|
|
1133
1160
|
aliasQueueStore: this.stickySessionManager.getAllStores().aliasQueueStore,
|
|
@@ -1287,6 +1314,7 @@ export class VirtualRouterEngine {
|
|
|
1287
1314
|
contextAdvisor: this.contextAdvisor,
|
|
1288
1315
|
loadBalancer: this.loadBalancer,
|
|
1289
1316
|
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1317
|
+
getProviderCooldownRemainingMs: (key) => this.getProviderCooldownRemainingMs(key),
|
|
1290
1318
|
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1291
1319
|
quotaView: this.quotaView,
|
|
1292
1320
|
aliasQueueStore: this.stickySessionManager.getAllStores().aliasQueueStore
|
|
@@ -1421,6 +1449,10 @@ export class VirtualRouterEngine {
|
|
|
1421
1449
|
const forceVision = this.routeHasForceFlag('vision');
|
|
1422
1450
|
const hasMultimodalTargets = this.routeHasTargets(this.routing.multimodal);
|
|
1423
1451
|
const hasVisionTargets = this.routeHasTargets(this.routing.vision);
|
|
1452
|
+
const hasLocalVideoAttachment = features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true;
|
|
1453
|
+
if (features.hasImageAttachment && hasLocalVideoAttachment && hasVisionTargets) {
|
|
1454
|
+
return ['vision'];
|
|
1455
|
+
}
|
|
1424
1456
|
const normalized = this.normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
|
|
1425
1457
|
const baseList = [];
|
|
1426
1458
|
if (classificationCandidates && classificationCandidates.length) {
|
|
@@ -1437,9 +1469,9 @@ export class VirtualRouterEngine {
|
|
|
1437
1469
|
baseList.unshift('multimodal');
|
|
1438
1470
|
}
|
|
1439
1471
|
}
|
|
1440
|
-
|
|
1472
|
+
if (hasVisionTargets) {
|
|
1441
1473
|
if (!baseList.includes('vision')) {
|
|
1442
|
-
baseList.
|
|
1474
|
+
baseList.push('vision');
|
|
1443
1475
|
}
|
|
1444
1476
|
}
|
|
1445
1477
|
if (!forceVision && hasMultimodalTargets) {
|
|
@@ -1629,6 +1661,17 @@ export class VirtualRouterEngine {
|
|
|
1629
1661
|
isProviderCoolingDown(providerKey) {
|
|
1630
1662
|
return this.cooldownManager.isProviderCoolingDown(providerKey);
|
|
1631
1663
|
}
|
|
1664
|
+
getProviderCooldownRemainingMs(providerKey) {
|
|
1665
|
+
if (!providerKey) {
|
|
1666
|
+
return 0;
|
|
1667
|
+
}
|
|
1668
|
+
const expiry = this.providerCooldowns.get(providerKey);
|
|
1669
|
+
if (!expiry || !Number.isFinite(expiry)) {
|
|
1670
|
+
return 0;
|
|
1671
|
+
}
|
|
1672
|
+
const remaining = Math.floor(expiry - Date.now());
|
|
1673
|
+
return remaining > 0 ? remaining : 0;
|
|
1674
|
+
}
|
|
1632
1675
|
restoreHealthFromStore() {
|
|
1633
1676
|
this.cooldownManager.restoreHealthFromStore();
|
|
1634
1677
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { analyzeMediaAttachments, detectExtendedThinkingKeyword, detectKeyword, extractMessageText, getLatestMessageRole, } from './message-utils.js';
|
|
2
2
|
import { extractAntigravityGeminiSessionId } from '../../conversion/compat/antigravity-session-signature.js';
|
|
3
3
|
import { detectCodingTool, detectLastAssistantToolCategory, detectVisionTool, detectWebSearchToolDeclared, detectWebTool, extractMeaningfulDeclaredToolNames } from './tool-signals.js';
|
|
4
4
|
import { computeRequestTokens } from './token-estimator.js';
|
|
@@ -18,10 +18,14 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
})();
|
|
21
|
-
const latestUserMessage = getLatestUserMessage(request.messages);
|
|
22
21
|
const latestMessageRole = getLatestMessageRole(request.messages);
|
|
22
|
+
const latestMessage = Array.isArray(request.messages) && request.messages.length
|
|
23
|
+
? request.messages[request.messages.length - 1]
|
|
24
|
+
: undefined;
|
|
23
25
|
const assistantMessages = request.messages.filter((msg) => msg.role === 'assistant');
|
|
24
|
-
const latestUserText =
|
|
26
|
+
const latestUserText = latestMessageRole === 'user' && latestMessage
|
|
27
|
+
? extractMessageText(latestMessage)
|
|
28
|
+
: '';
|
|
25
29
|
const normalizedUserText = latestUserText.toLowerCase();
|
|
26
30
|
const meaningfulDeclaredTools = extractMeaningfulDeclaredToolNames(request.tools);
|
|
27
31
|
const hasTools = meaningfulDeclaredTools.length > 0;
|
|
@@ -31,7 +35,8 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
31
35
|
const hasVisionTool = detectVisionTool(request);
|
|
32
36
|
// Vision routing must only trigger for the current user turn (latest message),
|
|
33
37
|
// not for historical user messages carrying images during tool/assistant followups.
|
|
34
|
-
const
|
|
38
|
+
const mediaSignals = latestMessageRole === 'user' ? analyzeMediaAttachments(latestMessage) : analyzeMediaAttachments(undefined);
|
|
39
|
+
const hasImageAttachment = mediaSignals.hasAnyMedia;
|
|
35
40
|
const hasCodingTool = detectCodingTool(request);
|
|
36
41
|
const hasWebTool = detectWebTool(request);
|
|
37
42
|
const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
|
|
@@ -58,6 +63,9 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
58
63
|
hasToolCallResponses,
|
|
59
64
|
hasVisionTool,
|
|
60
65
|
hasImageAttachment,
|
|
66
|
+
hasVideoAttachment: mediaSignals.hasVideo,
|
|
67
|
+
hasRemoteVideoAttachment: mediaSignals.hasRemoteVideo,
|
|
68
|
+
hasLocalVideoAttachment: mediaSignals.hasLocalVideo,
|
|
61
69
|
hasWebTool,
|
|
62
70
|
hasWebSearchToolDeclared: detectWebSearchToolDeclared(request),
|
|
63
71
|
hasCodingTool,
|
|
@@ -4,4 +4,12 @@ export declare function getLatestMessageRole(messages: StandardizedMessage[]): s
|
|
|
4
4
|
export declare function extractMessageText(message: StandardizedMessage): string;
|
|
5
5
|
export declare function detectKeyword(text: string, keywords: string[]): boolean;
|
|
6
6
|
export declare function detectExtendedThinkingKeyword(text: string): boolean;
|
|
7
|
+
export interface MediaAttachmentSignals {
|
|
8
|
+
hasAnyMedia: boolean;
|
|
9
|
+
hasImage: boolean;
|
|
10
|
+
hasVideo: boolean;
|
|
11
|
+
hasRemoteVideo: boolean;
|
|
12
|
+
hasLocalVideo: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function analyzeMediaAttachments(message: StandardizedMessage | undefined): MediaAttachmentSignals;
|
|
7
15
|
export declare function detectImageAttachment(message: StandardizedMessage | undefined): boolean;
|