@jsonstudio/llms 0.6.1643 → 0.6.1739
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/harvest-tool-calls-from-text.d.ts +10 -0
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
- package/dist/conversion/compat/antigravity-session-signature.d.ts +1 -1
- package/dist/conversion/compat/antigravity-session-signature.js +5 -4
- package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
- package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
- package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +19 -2
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +101 -5
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
- package/dist/conversion/hub/pipeline/target-utils.js +3 -0
- package/dist/conversion/hub/response/provider-response.js +27 -1
- package/dist/conversion/responses/responses-openai-bridge.js +32 -6
- package/dist/conversion/shared/anthropic-message-utils.js +20 -5
- package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
- package/dist/conversion/shared/bridge-id-utils.js +52 -15
- package/dist/conversion/shared/responses-conversation-store.js +40 -5
- package/dist/conversion/shared/responses-output-builder.js +23 -7
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
- package/dist/conversion/shared/responses-tool-utils.js +30 -13
- package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
- package/dist/conversion/shared/text-markup-normalizer.js +269 -1
- package/dist/router/virtual-router/bootstrap.js +31 -7
- package/dist/router/virtual-router/classifier.js +1 -1
- package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
- package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
- package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
- package/dist/router/virtual-router/engine/health/index.js +720 -0
- package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
- package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
- package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
- package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
- package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
- package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
- package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
- package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
- package/dist/router/virtual-router/engine-health.d.ts +1 -23
- package/dist/router/virtual-router/engine-health.js +1 -720
- package/dist/router/virtual-router/engine-selection/route-utils.js +57 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +8 -48
- package/dist/router/virtual-router/engine-selection/tier-selection.js +34 -17
- package/dist/router/virtual-router/engine-selection.d.ts +1 -13
- package/dist/router/virtual-router/engine-selection.js +1 -225
- package/dist/router/virtual-router/engine.d.ts +2 -23
- package/dist/router/virtual-router/engine.js +130 -603
- package/dist/router/virtual-router/message-utils.js +15 -5
- package/dist/servertool/engine.js +4 -4
- package/dist/servertool/handlers/followup-request-builder.js +46 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +48 -47
- package/dist/servertool/handlers/stop-message-auto.js +64 -7
- package/dist/servertool/handlers/vision.js +10 -0
- package/dist/servertool/types.d.ts +3 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
- package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
- package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
- package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
- package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
- package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
- package/dist/tools/apply-patch/structured/coercion.js +28 -4
- package/dist/tools/apply-patch/validator.js +7 -146
- package/package.json +1 -1
|
@@ -4,17 +4,18 @@ import { RouteLoadBalancer } from './load-balancer.js';
|
|
|
4
4
|
import { RoutingClassifier } from './classifier.js';
|
|
5
5
|
import { buildRoutingFeatures } from './features.js';
|
|
6
6
|
import { ContextAdvisor } from './context-advisor.js';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import os from 'node:os';
|
|
9
|
-
import path from 'node:path';
|
|
10
7
|
import { DEFAULT_ROUTE, ROUTE_PRIORITY, VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
11
8
|
import { getStatsCenter } from '../../telemetry/stats-center.js';
|
|
12
9
|
import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRoutingInstructions } from './routing-instructions.js';
|
|
13
10
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from './sticky-session-store.js';
|
|
14
11
|
import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
|
|
15
|
-
import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine-
|
|
16
|
-
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine
|
|
17
|
-
import {
|
|
12
|
+
import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine/routing-pools/index.js';
|
|
13
|
+
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine/health/index.js';
|
|
14
|
+
import { hydrateAntigravityAliasLeaseStoreIfNeeded, recordAntigravitySessionLease, resolveAntigravityAliasReuseCooldownMs } from './engine/antigravity/alias-lease.js';
|
|
15
|
+
import { buildMetadataInstructions, resolveRoutingMode } from './engine/routing-state/metadata.js';
|
|
16
|
+
import { getRoutingInstructionState, persistRoutingInstructionState, resolveStopMessageScope } from './engine/routing-state/store.js';
|
|
17
|
+
import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './engine/provider-key/parse.js';
|
|
18
|
+
import { resolveSessionScope as resolveSessionScopeImpl, resolveStickyKey as resolveStickyKeyImpl } from './engine/routing-state/keys.js';
|
|
18
19
|
export class VirtualRouterEngine {
|
|
19
20
|
routing = {};
|
|
20
21
|
providerRegistry = new ProviderRegistry();
|
|
@@ -122,8 +123,13 @@ export class VirtualRouterEngine {
|
|
|
122
123
|
this.providerCooldowns.clear();
|
|
123
124
|
this.restoreHealthFromStore();
|
|
124
125
|
this.loadBalancer = new RouteLoadBalancer(config.loadBalancing);
|
|
125
|
-
this.antigravityAliasReuseCooldownMs =
|
|
126
|
-
|
|
126
|
+
this.antigravityAliasReuseCooldownMs = resolveAntigravityAliasReuseCooldownMs(config);
|
|
127
|
+
hydrateAntigravityAliasLeaseStoreIfNeeded({
|
|
128
|
+
force: true,
|
|
129
|
+
leaseStore: this.antigravityAliasLeaseStore,
|
|
130
|
+
persistence: this.antigravityLeasePersistence,
|
|
131
|
+
aliasReuseCooldownMs: this.antigravityAliasReuseCooldownMs
|
|
132
|
+
});
|
|
127
133
|
this.classifier = new RoutingClassifier(config.classifier);
|
|
128
134
|
this.contextRouting = config.contextRouting ?? { warnRatio: 0.9, hardLimit: false };
|
|
129
135
|
this.contextAdvisor.configure(this.contextRouting);
|
|
@@ -136,13 +142,13 @@ export class VirtualRouterEngine {
|
|
|
136
142
|
route(request, metadata) {
|
|
137
143
|
const stickyKey = this.resolveStickyKey(metadata);
|
|
138
144
|
const sessionScope = this.resolveSessionScope(metadata);
|
|
139
|
-
const stopMessageScope =
|
|
145
|
+
const stopMessageScope = resolveStopMessageScope(metadata);
|
|
140
146
|
// Routing instructions should be session/conversation-scoped when available (including /v1/responses),
|
|
141
147
|
// while auto-sticky for Responses remains request-chain scoped via resolveStickyKey().
|
|
142
148
|
const stateKey = sessionScope || stickyKey || 'default';
|
|
143
|
-
const baseState =
|
|
149
|
+
const baseState = getRoutingInstructionState(stateKey, this.routingInstructionState, this.routingStateStore);
|
|
144
150
|
let routingState = baseState;
|
|
145
|
-
const metadataInstructions =
|
|
151
|
+
const metadataInstructions = buildMetadataInstructions(metadata);
|
|
146
152
|
if (metadataInstructions.length > 0) {
|
|
147
153
|
routingState = applyRoutingInstructions(metadataInstructions, routingState);
|
|
148
154
|
}
|
|
@@ -157,7 +163,7 @@ export class VirtualRouterEngine {
|
|
|
157
163
|
};
|
|
158
164
|
}
|
|
159
165
|
if (stopMessageScope) {
|
|
160
|
-
const sessionState =
|
|
166
|
+
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
161
167
|
if (typeof sessionState.stopMessageText === 'string' ||
|
|
162
168
|
typeof sessionState.stopMessageMaxRepeats === 'number') {
|
|
163
169
|
routingState = {
|
|
@@ -173,7 +179,7 @@ export class VirtualRouterEngine {
|
|
|
173
179
|
const parsedInstructions = parseRoutingInstructions(request.messages);
|
|
174
180
|
let instructions = parsedInstructions;
|
|
175
181
|
if (stopMessageScope && parsedInstructions.length > 0) {
|
|
176
|
-
const sessionState =
|
|
182
|
+
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
177
183
|
const hasStopMessageClear = parsedInstructions.some((entry) => entry.type === 'stopMessageClear');
|
|
178
184
|
const stopMessageSets = parsedInstructions.filter((entry) => entry.type === 'stopMessageSet');
|
|
179
185
|
if (!hasStopMessageClear && stopMessageSets.length > 0) {
|
|
@@ -213,7 +219,7 @@ export class VirtualRouterEngine {
|
|
|
213
219
|
if (instructions.length > 0) {
|
|
214
220
|
routingState = applyRoutingInstructions(instructions, routingState);
|
|
215
221
|
this.routingInstructionState.set(stateKey, routingState);
|
|
216
|
-
|
|
222
|
+
persistRoutingInstructionState(stateKey, routingState, this.routingStateStore);
|
|
217
223
|
// 对 stopMessage 指令补充一份基于 session/conversation 的持久化状态,
|
|
218
224
|
// 便于 server-side 工具通过 session:*/conversation:* scope 读取到相同配置。
|
|
219
225
|
// stopMessage is strictly session-scoped (sessionId only). Persist it under the session scope
|
|
@@ -222,7 +228,7 @@ export class VirtualRouterEngine {
|
|
|
222
228
|
const hasStopMessageSet = instructions.some((entry) => entry.type === 'stopMessageSet');
|
|
223
229
|
const hasStopMessageClear = instructions.some((entry) => entry.type === 'stopMessageClear');
|
|
224
230
|
if (hasStopMessageSet || hasStopMessageClear) {
|
|
225
|
-
const sessionState =
|
|
231
|
+
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
226
232
|
let nextSessionState = {
|
|
227
233
|
...sessionState
|
|
228
234
|
};
|
|
@@ -267,7 +273,7 @@ export class VirtualRouterEngine {
|
|
|
267
273
|
}
|
|
268
274
|
if (shouldPersistSessionState) {
|
|
269
275
|
this.routingInstructionState.set(stopMessageScope, nextSessionState);
|
|
270
|
-
|
|
276
|
+
persistRoutingInstructionState(stopMessageScope, nextSessionState, this.routingStateStore);
|
|
271
277
|
}
|
|
272
278
|
else {
|
|
273
279
|
nextSessionState = sessionState;
|
|
@@ -285,7 +291,7 @@ export class VirtualRouterEngine {
|
|
|
285
291
|
}
|
|
286
292
|
}
|
|
287
293
|
if (instructions.length === 0 && stopMessageScope) {
|
|
288
|
-
const sessionState =
|
|
294
|
+
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
289
295
|
if (typeof sessionState.stopMessageText === 'string' ||
|
|
290
296
|
typeof sessionState.stopMessageMaxRepeats === 'number') {
|
|
291
297
|
routingState.stopMessageText = sessionState.stopMessageText;
|
|
@@ -308,14 +314,14 @@ export class VirtualRouterEngine {
|
|
|
308
314
|
for (const key of pool.targets) {
|
|
309
315
|
if (typeof key !== 'string' || !key)
|
|
310
316
|
continue;
|
|
311
|
-
const providerId =
|
|
317
|
+
const providerId = extractProviderId(key);
|
|
312
318
|
if (providerId) {
|
|
313
319
|
providersInRouting.add(providerId);
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
|
-
const allowed = Array.from(routingState.allowedProviders);
|
|
324
|
+
const allowed = Array.from(routingState.allowedProviders).filter((provider) => typeof provider === 'string');
|
|
319
325
|
const hasIntersection = allowed.some((provider) => providersInRouting.has(provider));
|
|
320
326
|
if (!hasIntersection) {
|
|
321
327
|
routingState = {
|
|
@@ -323,7 +329,7 @@ export class VirtualRouterEngine {
|
|
|
323
329
|
allowedProviders: new Set()
|
|
324
330
|
};
|
|
325
331
|
this.routingInstructionState.set(stateKey, routingState);
|
|
326
|
-
|
|
332
|
+
persistRoutingInstructionState(stateKey, routingState, this.routingStateStore);
|
|
327
333
|
}
|
|
328
334
|
}
|
|
329
335
|
const features = buildRoutingFeatures(request, metadata);
|
|
@@ -492,7 +498,7 @@ export class VirtualRouterEngine {
|
|
|
492
498
|
preferTarget: undefined
|
|
493
499
|
};
|
|
494
500
|
this.routingInstructionState.set(stateKey, routingState);
|
|
495
|
-
|
|
501
|
+
persistRoutingInstructionState(stateKey, routingState, this.routingStateStore);
|
|
496
502
|
}
|
|
497
503
|
}
|
|
498
504
|
}
|
|
@@ -517,9 +523,20 @@ export class VirtualRouterEngine {
|
|
|
517
523
|
...(this.webSearchForce ? { forceWebSearch: true } : {}),
|
|
518
524
|
...(forceVision ? { forceVision: true } : {})
|
|
519
525
|
};
|
|
520
|
-
|
|
526
|
+
recordAntigravitySessionLease({
|
|
527
|
+
metadata: features.metadata,
|
|
528
|
+
providerKey: selection.providerKey,
|
|
529
|
+
sessionKey: this.resolveSessionScope(features.metadata),
|
|
530
|
+
providerRegistry: this.providerRegistry,
|
|
531
|
+
leaseStore: this.antigravityAliasLeaseStore,
|
|
532
|
+
sessionAliasStore: this.antigravitySessionAliasStore,
|
|
533
|
+
persistence: this.antigravityLeasePersistence,
|
|
534
|
+
aliasReuseCooldownMs: this.antigravityAliasReuseCooldownMs,
|
|
535
|
+
commitSessionBinding: false,
|
|
536
|
+
debug: this.debug
|
|
537
|
+
});
|
|
521
538
|
this.incrementRouteStat(selection.routeUsed, selection.providerKey);
|
|
522
|
-
const routingMode =
|
|
539
|
+
const routingMode = resolveRoutingMode([...metadataInstructions, ...instructions], routingState);
|
|
523
540
|
try {
|
|
524
541
|
this.statsCenter.recordVirtualRouterHit({
|
|
525
542
|
requestId: metadata.requestId,
|
|
@@ -569,9 +586,13 @@ export class VirtualRouterEngine {
|
|
|
569
586
|
}
|
|
570
587
|
getStopMessageState(metadata) {
|
|
571
588
|
const sessionScope = this.resolveSessionScope(metadata);
|
|
572
|
-
const sessionState = sessionScope
|
|
589
|
+
const sessionState = sessionScope
|
|
590
|
+
? getRoutingInstructionState(sessionScope, this.routingInstructionState, this.routingStateStore)
|
|
591
|
+
: null;
|
|
573
592
|
const stickyKey = this.resolveStickyKey(metadata);
|
|
574
|
-
const stickyState = stickyKey
|
|
593
|
+
const stickyState = stickyKey
|
|
594
|
+
? getRoutingInstructionState(stickyKey, this.routingInstructionState, this.routingStateStore)
|
|
595
|
+
: null;
|
|
575
596
|
const effectiveState = sessionState && typeof sessionState.stopMessageText === 'string' && sessionState.stopMessageText.trim()
|
|
576
597
|
? sessionState
|
|
577
598
|
: stickyState;
|
|
@@ -659,7 +680,18 @@ export class VirtualRouterEngine {
|
|
|
659
680
|
return;
|
|
660
681
|
}
|
|
661
682
|
if (providerKey.toLowerCase().startsWith('antigravity.')) {
|
|
662
|
-
|
|
683
|
+
recordAntigravitySessionLease({
|
|
684
|
+
metadata: metadata,
|
|
685
|
+
providerKey,
|
|
686
|
+
sessionKey: this.resolveSessionScope(metadata),
|
|
687
|
+
providerRegistry: this.providerRegistry,
|
|
688
|
+
leaseStore: this.antigravityAliasLeaseStore,
|
|
689
|
+
sessionAliasStore: this.antigravitySessionAliasStore,
|
|
690
|
+
persistence: this.antigravityLeasePersistence,
|
|
691
|
+
aliasReuseCooldownMs: this.antigravityAliasReuseCooldownMs,
|
|
692
|
+
commitSessionBinding: true,
|
|
693
|
+
debug: this.debug
|
|
694
|
+
});
|
|
663
695
|
}
|
|
664
696
|
}
|
|
665
697
|
getStatus() {
|
|
@@ -720,7 +752,8 @@ export class VirtualRouterEngine {
|
|
|
720
752
|
}
|
|
721
753
|
}
|
|
722
754
|
selectProvider(requestedRoute, metadata, classification, features, routingState) {
|
|
723
|
-
const activeState = routingState ||
|
|
755
|
+
const activeState = routingState ||
|
|
756
|
+
getRoutingInstructionState(this.resolveStickyKey(metadata), this.routingInstructionState, this.routingStateStore);
|
|
724
757
|
return selectProviderImpl(requestedRoute, metadata, classification, features, activeState, {
|
|
725
758
|
routing: this.routing,
|
|
726
759
|
providerRegistry: this.providerRegistry,
|
|
@@ -749,477 +782,10 @@ export class VirtualRouterEngine {
|
|
|
749
782
|
return this.healthManager.getConfig();
|
|
750
783
|
}
|
|
751
784
|
resolveStickyKey(metadata) {
|
|
752
|
-
|
|
753
|
-
// 对 Responses 协议的自动粘滞,仅在“单次会话链路”内生效:
|
|
754
|
-
// - Resume/submit 调用:stickyKey = previousRequestId(指向首轮请求);
|
|
755
|
-
// - 普通 /v1/responses 调用:stickyKey = 本次 requestId;
|
|
756
|
-
// 这样不会把 Responses 的自动粘滞扩散到整个 session,仅在需要 save/restore
|
|
757
|
-
// 的请求链路中复用 provider.key.model。
|
|
758
|
-
if (providerProtocol === 'openai-responses') {
|
|
759
|
-
const resume = metadata.responsesResume;
|
|
760
|
-
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
761
|
-
return resume.previousRequestId.trim();
|
|
762
|
-
}
|
|
763
|
-
return metadata.requestId;
|
|
764
|
-
}
|
|
765
|
-
// 其它协议沿用会话级 sticky 语义:sessionId / conversationId → requestId。
|
|
766
|
-
const sessionScope = this.resolveSessionScope(metadata);
|
|
767
|
-
if (sessionScope) {
|
|
768
|
-
return sessionScope;
|
|
769
|
-
}
|
|
770
|
-
return metadata.requestId;
|
|
785
|
+
return resolveStickyKeyImpl(metadata);
|
|
771
786
|
}
|
|
772
787
|
resolveSessionScope(metadata) {
|
|
773
|
-
|
|
774
|
-
if (sessionId) {
|
|
775
|
-
return `session:${sessionId}`;
|
|
776
|
-
}
|
|
777
|
-
const conversationId = typeof metadata.conversationId === 'string' ? metadata.conversationId.trim() : '';
|
|
778
|
-
if (conversationId) {
|
|
779
|
-
return `conversation:${conversationId}`;
|
|
780
|
-
}
|
|
781
|
-
return undefined;
|
|
782
|
-
}
|
|
783
|
-
resolveAntigravityAliasReuseCooldownMs(config) {
|
|
784
|
-
const cfg = config.loadBalancing?.aliasSelection?.sessionLeaseCooldownMs;
|
|
785
|
-
if (typeof cfg === 'number' && Number.isFinite(cfg)) {
|
|
786
|
-
return Math.max(0, Math.floor(cfg));
|
|
787
|
-
}
|
|
788
|
-
return 5 * 60_000;
|
|
789
|
-
}
|
|
790
|
-
resolveAntigravityLeaseScope(providerKey) {
|
|
791
|
-
try {
|
|
792
|
-
const profile = this.providerRegistry.get(providerKey);
|
|
793
|
-
const modelId = typeof profile?.modelId === 'string' ? profile.modelId.trim().toLowerCase() : '';
|
|
794
|
-
if (modelId.startsWith('gemini-')) {
|
|
795
|
-
return 'gemini';
|
|
796
|
-
}
|
|
797
|
-
return null;
|
|
798
|
-
}
|
|
799
|
-
catch {
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
buildScopedSessionKey(sessionKey) {
|
|
804
|
-
return `${sessionKey}::gemini`;
|
|
805
|
-
}
|
|
806
|
-
buildAntigravityLeaseRuntimeKey(runtimeKey) {
|
|
807
|
-
return `${runtimeKey}::gemini`;
|
|
808
|
-
}
|
|
809
|
-
extractRuntimeKey(providerKey) {
|
|
810
|
-
const value = typeof providerKey === 'string' ? providerKey.trim() : '';
|
|
811
|
-
if (!value)
|
|
812
|
-
return null;
|
|
813
|
-
const firstDot = value.indexOf('.');
|
|
814
|
-
if (firstDot <= 0 || firstDot === value.length - 1)
|
|
815
|
-
return null;
|
|
816
|
-
const secondDot = value.indexOf('.', firstDot + 1);
|
|
817
|
-
if (secondDot <= firstDot + 1)
|
|
818
|
-
return null;
|
|
819
|
-
const providerId = value.slice(0, firstDot);
|
|
820
|
-
const alias = value.slice(firstDot + 1, secondDot);
|
|
821
|
-
if (!providerId || !alias)
|
|
822
|
-
return null;
|
|
823
|
-
return `${providerId}.${alias}`;
|
|
824
|
-
}
|
|
825
|
-
recordAntigravitySessionLease(metadata, providerKey, opts) {
|
|
826
|
-
// Per-request runtime override: allow callers (e.g. servertool followups) to disable
|
|
827
|
-
// Antigravity gemini session binding/lease without changing global config.
|
|
828
|
-
try {
|
|
829
|
-
const rt = metadata?.__rt;
|
|
830
|
-
if (rt && typeof rt === 'object' && !Array.isArray(rt)) {
|
|
831
|
-
const disable = rt.disableAntigravitySessionBinding;
|
|
832
|
-
const mode = rt.antigravitySessionBinding;
|
|
833
|
-
if (disable === true || mode === false) {
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
if (typeof mode === 'string' && ['0', 'false', 'off', 'disabled', 'none'].includes(mode.trim().toLowerCase())) {
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
catch {
|
|
842
|
-
// ignore metadata parse errors
|
|
843
|
-
}
|
|
844
|
-
const sessionKey = this.resolveSessionScope(metadata);
|
|
845
|
-
if (!sessionKey) {
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
const runtimeKeyBase = this.extractRuntimeKey(providerKey);
|
|
849
|
-
if (!runtimeKeyBase || !runtimeKeyBase.startsWith('antigravity.')) {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const commitSessionBinding = opts?.commitSessionBinding === true;
|
|
853
|
-
const scope = this.resolveAntigravityLeaseScope(providerKey);
|
|
854
|
-
// Policy: antigravity alias leasing/binding applies only to Gemini models.
|
|
855
|
-
if (scope !== 'gemini') {
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
const scopedSessionKey = this.buildScopedSessionKey(sessionKey);
|
|
859
|
-
const runtimeKey = this.buildAntigravityLeaseRuntimeKey(runtimeKeyBase);
|
|
860
|
-
const now = Date.now();
|
|
861
|
-
const cooldownMs = this.antigravityAliasReuseCooldownMs;
|
|
862
|
-
const existing = this.antigravityAliasLeaseStore.get(runtimeKey);
|
|
863
|
-
// Do not steal a lease from another active session. This prevents a "forced fallback" route
|
|
864
|
-
// (e.g. default route bypass) from evicting the original session and causing churn.
|
|
865
|
-
if (existing && existing.sessionKey !== scopedSessionKey && cooldownMs > 0 && now - existing.lastSeenAt < cooldownMs) {
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
this.antigravityAliasLeaseStore.set(runtimeKey, { sessionKey: scopedSessionKey, lastSeenAt: now });
|
|
869
|
-
if (commitSessionBinding) {
|
|
870
|
-
const previous = this.antigravitySessionAliasStore.get(scopedSessionKey);
|
|
871
|
-
this.antigravitySessionAliasStore.set(scopedSessionKey, runtimeKey);
|
|
872
|
-
try {
|
|
873
|
-
const raw = String(process.env.ROUTECODEX_STAGE_LOG || process.env.RCC_STAGE_LOG || '').trim().toLowerCase();
|
|
874
|
-
const enabled = raw !== '' && raw !== '0' && raw !== 'false' && raw !== 'no';
|
|
875
|
-
if (enabled && previous !== runtimeKey) {
|
|
876
|
-
this.debug?.log?.('[virtual-router][antigravity-session-binding] commit', JSON.stringify({ sessionKey: scopedSessionKey, runtimeKey, prev: previous ?? null }));
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
catch {
|
|
880
|
-
// ignore logging errors
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
this.scheduleAntigravityAliasLeaseStoreFlush();
|
|
884
|
-
// Opportunistic cleanup: keep only active leases within cooldown window.
|
|
885
|
-
if (cooldownMs <= 0) {
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
for (const [key, entry] of this.antigravityAliasLeaseStore.entries()) {
|
|
889
|
-
if (now - entry.lastSeenAt >= cooldownMs) {
|
|
890
|
-
this.antigravityAliasLeaseStore.delete(key);
|
|
891
|
-
// Clear session mapping if it still points at the expired runtimeKey.
|
|
892
|
-
if (this.antigravitySessionAliasStore.get(entry.sessionKey) === key) {
|
|
893
|
-
this.antigravitySessionAliasStore.delete(entry.sessionKey);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
resolveAntigravityAliasLeasePersistPath() {
|
|
899
|
-
try {
|
|
900
|
-
const enabledRaw = process.env.ROUTECODEX_ANTIGRAVITY_ALIAS_LEASE_PERSIST;
|
|
901
|
-
const enabled = typeof enabledRaw === 'string' &&
|
|
902
|
-
['1', 'true', 'yes', 'on'].includes(enabledRaw.trim().toLowerCase());
|
|
903
|
-
if (!enabled) {
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
const home = os.homedir();
|
|
907
|
-
if (!home)
|
|
908
|
-
return null;
|
|
909
|
-
return path.join(home, '.routecodex', 'state', 'antigravity-alias-leases.json');
|
|
910
|
-
}
|
|
911
|
-
catch {
|
|
912
|
-
return null;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
hydrateAntigravityAliasLeaseStoreIfNeeded(force = false) {
|
|
916
|
-
const filePath = this.resolveAntigravityAliasLeasePersistPath();
|
|
917
|
-
if (!filePath) {
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
let stat = null;
|
|
921
|
-
try {
|
|
922
|
-
stat = fs.statSync(filePath);
|
|
923
|
-
}
|
|
924
|
-
catch {
|
|
925
|
-
this.antigravityLeasePersistence.loadedOnce = true;
|
|
926
|
-
this.antigravityLeasePersistence.loadedMtimeMs = null;
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
const mtimeMs = typeof stat.mtimeMs === 'number' && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : stat.mtime.getTime();
|
|
930
|
-
if (!force && this.antigravityLeasePersistence.loadedOnce && this.antigravityLeasePersistence.loadedMtimeMs === mtimeMs) {
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
let parsed;
|
|
934
|
-
try {
|
|
935
|
-
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
936
|
-
}
|
|
937
|
-
catch {
|
|
938
|
-
this.antigravityLeasePersistence.loadedOnce = true;
|
|
939
|
-
this.antigravityLeasePersistence.loadedMtimeMs = mtimeMs;
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
943
|
-
this.antigravityLeasePersistence.loadedOnce = true;
|
|
944
|
-
this.antigravityLeasePersistence.loadedMtimeMs = mtimeMs;
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
const leasesRaw = parsed.leases;
|
|
948
|
-
if (!leasesRaw || typeof leasesRaw !== 'object' || Array.isArray(leasesRaw)) {
|
|
949
|
-
this.antigravityLeasePersistence.loadedOnce = true;
|
|
950
|
-
this.antigravityLeasePersistence.loadedMtimeMs = mtimeMs;
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
const now = Date.now();
|
|
954
|
-
const cooldownMs = this.antigravityAliasReuseCooldownMs;
|
|
955
|
-
const nextLeaseStore = new Map();
|
|
956
|
-
for (const [runtimeKeyRaw, entryRaw] of Object.entries(leasesRaw)) {
|
|
957
|
-
const runtimeKeyBase = typeof runtimeKeyRaw === 'string' ? runtimeKeyRaw.trim() : '';
|
|
958
|
-
if (!runtimeKeyBase || !runtimeKeyBase.startsWith('antigravity.'))
|
|
959
|
-
continue;
|
|
960
|
-
if (!entryRaw || typeof entryRaw !== 'object' || Array.isArray(entryRaw))
|
|
961
|
-
continue;
|
|
962
|
-
const rawSessionKey = typeof entryRaw.sessionKey === 'string' ? String(entryRaw.sessionKey).trim() : '';
|
|
963
|
-
const lastSeenAt = typeof entryRaw.lastSeenAt === 'number' && Number.isFinite(entryRaw.lastSeenAt)
|
|
964
|
-
? Math.floor(entryRaw.lastSeenAt)
|
|
965
|
-
: 0;
|
|
966
|
-
if (!rawSessionKey || !lastSeenAt)
|
|
967
|
-
continue;
|
|
968
|
-
if (cooldownMs > 0 && now - lastSeenAt >= cooldownMs)
|
|
969
|
-
continue;
|
|
970
|
-
// Backwards compatible: legacy lease keys were unscoped ("antigravity.<alias>").
|
|
971
|
-
// Policy: only keep Gemini-scoped leases. Drop any non-gemini scoped entries.
|
|
972
|
-
const hasExplicitScope = runtimeKeyBase.includes('::');
|
|
973
|
-
if (hasExplicitScope && !runtimeKeyBase.endsWith('::gemini'))
|
|
974
|
-
continue;
|
|
975
|
-
const runtimeKey = hasExplicitScope ? runtimeKeyBase : this.buildAntigravityLeaseRuntimeKey(runtimeKeyBase);
|
|
976
|
-
if (rawSessionKey.includes('::') && !rawSessionKey.endsWith('::gemini'))
|
|
977
|
-
continue;
|
|
978
|
-
const sessionKey = rawSessionKey.includes('::') ? rawSessionKey : this.buildScopedSessionKey(rawSessionKey);
|
|
979
|
-
nextLeaseStore.set(runtimeKey, { sessionKey, lastSeenAt });
|
|
980
|
-
}
|
|
981
|
-
this.antigravityAliasLeaseStore.clear();
|
|
982
|
-
for (const [key, val] of nextLeaseStore.entries()) {
|
|
983
|
-
this.antigravityAliasLeaseStore.set(key, val);
|
|
984
|
-
}
|
|
985
|
-
this.antigravityLeasePersistence.loadedOnce = true;
|
|
986
|
-
this.antigravityLeasePersistence.loadedMtimeMs = mtimeMs;
|
|
987
|
-
}
|
|
988
|
-
scheduleAntigravityAliasLeaseStoreFlush() {
|
|
989
|
-
if (this.antigravityLeasePersistence.flushTimer) {
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
this.antigravityLeasePersistence.flushTimer = setTimeout(() => {
|
|
993
|
-
this.antigravityLeasePersistence.flushTimer = null;
|
|
994
|
-
this.flushAntigravityAliasLeaseStoreSync();
|
|
995
|
-
}, 250);
|
|
996
|
-
this.antigravityLeasePersistence.flushTimer.unref?.();
|
|
997
|
-
}
|
|
998
|
-
flushAntigravityAliasLeaseStoreSync() {
|
|
999
|
-
const filePath = this.resolveAntigravityAliasLeasePersistPath();
|
|
1000
|
-
if (!filePath) {
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
try {
|
|
1004
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1005
|
-
}
|
|
1006
|
-
catch {
|
|
1007
|
-
// ignore
|
|
1008
|
-
}
|
|
1009
|
-
const now = Date.now();
|
|
1010
|
-
const cooldownMs = this.antigravityAliasReuseCooldownMs;
|
|
1011
|
-
const leases = {};
|
|
1012
|
-
for (const [runtimeKey, entry] of this.antigravityAliasLeaseStore.entries()) {
|
|
1013
|
-
if (!runtimeKey || !entry)
|
|
1014
|
-
continue;
|
|
1015
|
-
if (cooldownMs > 0 && now - entry.lastSeenAt >= cooldownMs)
|
|
1016
|
-
continue;
|
|
1017
|
-
if (!entry.sessionKey || !entry.lastSeenAt)
|
|
1018
|
-
continue;
|
|
1019
|
-
leases[runtimeKey] = { sessionKey: entry.sessionKey, lastSeenAt: entry.lastSeenAt };
|
|
1020
|
-
}
|
|
1021
|
-
try {
|
|
1022
|
-
const tmpPath = `${filePath}.tmp.${process.pid}.${now}`;
|
|
1023
|
-
fs.writeFileSync(tmpPath, JSON.stringify({ version: 1, updatedAt: now, leases }, null, 2), 'utf8');
|
|
1024
|
-
fs.renameSync(tmpPath, filePath);
|
|
1025
|
-
try {
|
|
1026
|
-
const stat = fs.statSync(filePath);
|
|
1027
|
-
const mtimeMs = typeof stat.mtimeMs === 'number' && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : stat.mtime.getTime();
|
|
1028
|
-
this.antigravityLeasePersistence.loadedOnce = true;
|
|
1029
|
-
this.antigravityLeasePersistence.loadedMtimeMs = mtimeMs;
|
|
1030
|
-
}
|
|
1031
|
-
catch {
|
|
1032
|
-
// ignore
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
catch {
|
|
1036
|
-
// best-effort persistence: must not affect routing
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
resolveStopMessageScope(metadata) {
|
|
1040
|
-
const sessionId = typeof metadata.sessionId === 'string' ? metadata.sessionId.trim() : '';
|
|
1041
|
-
if (sessionId) {
|
|
1042
|
-
return `session:${sessionId}`;
|
|
1043
|
-
}
|
|
1044
|
-
return undefined;
|
|
1045
|
-
}
|
|
1046
|
-
getRoutingInstructionState(stickyKey) {
|
|
1047
|
-
const key = stickyKey || 'default';
|
|
1048
|
-
const existing = this.routingInstructionState.get(key);
|
|
1049
|
-
// 对 session:/conversation: 作用域,在每次读取时尝试从磁盘刷新 stopMessage 相关字段,
|
|
1050
|
-
// 确保 servertool(如 stop_message_auto)通过 sticky-session-store 更新的使用次数
|
|
1051
|
-
// 能在 VirtualRouter 日志中实时反映出来。
|
|
1052
|
-
if (existing && (key.startsWith('session:') || key.startsWith('conversation:'))) {
|
|
1053
|
-
try {
|
|
1054
|
-
const persisted = this.routingStateStore.loadSync(key);
|
|
1055
|
-
const merged = mergeStopMessageFromPersisted(existing, persisted);
|
|
1056
|
-
existing.stopMessageSource = merged.stopMessageSource;
|
|
1057
|
-
existing.stopMessageText = merged.stopMessageText;
|
|
1058
|
-
existing.stopMessageMaxRepeats = merged.stopMessageMaxRepeats;
|
|
1059
|
-
existing.stopMessageUsed = merged.stopMessageUsed;
|
|
1060
|
-
existing.stopMessageUpdatedAt = merged.stopMessageUpdatedAt;
|
|
1061
|
-
existing.stopMessageLastUsedAt = merged.stopMessageLastUsedAt;
|
|
1062
|
-
}
|
|
1063
|
-
catch {
|
|
1064
|
-
// 刷新失败不影响原有内存状态
|
|
1065
|
-
}
|
|
1066
|
-
return existing;
|
|
1067
|
-
}
|
|
1068
|
-
let initial = null;
|
|
1069
|
-
// 仅对 session:/conversation: 作用域的 key 尝试从磁盘恢复持久化状态
|
|
1070
|
-
if (key.startsWith('session:') || key.startsWith('conversation:')) {
|
|
1071
|
-
initial = this.routingStateStore.loadSync(key);
|
|
1072
|
-
}
|
|
1073
|
-
if (!initial) {
|
|
1074
|
-
initial = {
|
|
1075
|
-
forcedTarget: undefined,
|
|
1076
|
-
stickyTarget: undefined,
|
|
1077
|
-
allowedProviders: new Set(),
|
|
1078
|
-
disabledProviders: new Set(),
|
|
1079
|
-
disabledKeys: new Map(),
|
|
1080
|
-
disabledModels: new Map(),
|
|
1081
|
-
stopMessageSource: undefined,
|
|
1082
|
-
stopMessageText: undefined,
|
|
1083
|
-
stopMessageMaxRepeats: undefined,
|
|
1084
|
-
stopMessageUsed: undefined,
|
|
1085
|
-
stopMessageUpdatedAt: undefined,
|
|
1086
|
-
stopMessageLastUsedAt: undefined
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
this.routingInstructionState.set(key, initial);
|
|
1090
|
-
return initial;
|
|
1091
|
-
}
|
|
1092
|
-
buildMetadataInstructions(metadata) {
|
|
1093
|
-
const instructions = [];
|
|
1094
|
-
const forcedProviderKeyRaw = metadata
|
|
1095
|
-
.__shadowCompareForcedProviderKey;
|
|
1096
|
-
const forcedProviderKey = this.parseMetadataForceProviderKey(forcedProviderKeyRaw);
|
|
1097
|
-
if (forcedProviderKey) {
|
|
1098
|
-
instructions.push({ type: 'force', ...forcedProviderKey });
|
|
1099
|
-
}
|
|
1100
|
-
if (Array.isArray(metadata.disabledProviderKeyAliases)) {
|
|
1101
|
-
for (const entry of metadata.disabledProviderKeyAliases) {
|
|
1102
|
-
const parsed = this.parseMetadataDisableDescriptor(entry);
|
|
1103
|
-
if (parsed) {
|
|
1104
|
-
instructions.push({ type: 'disable', ...parsed });
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return instructions;
|
|
1109
|
-
}
|
|
1110
|
-
parseMetadataDisableDescriptor(entry) {
|
|
1111
|
-
if (typeof entry !== 'string') {
|
|
1112
|
-
return null;
|
|
1113
|
-
}
|
|
1114
|
-
const trimmed = entry.trim();
|
|
1115
|
-
if (!trimmed) {
|
|
1116
|
-
return null;
|
|
1117
|
-
}
|
|
1118
|
-
const parts = trimmed.split('.');
|
|
1119
|
-
if (parts.length < 2) {
|
|
1120
|
-
return null;
|
|
1121
|
-
}
|
|
1122
|
-
const provider = parts[0];
|
|
1123
|
-
const alias = parts[1];
|
|
1124
|
-
if (!provider || !alias) {
|
|
1125
|
-
return null;
|
|
1126
|
-
}
|
|
1127
|
-
if (/^\d+$/.test(alias)) {
|
|
1128
|
-
return { provider, keyIndex: Number.parseInt(alias, 10) };
|
|
1129
|
-
}
|
|
1130
|
-
return { provider, keyAlias: alias };
|
|
1131
|
-
}
|
|
1132
|
-
parseMetadataForceProviderKey(entry) {
|
|
1133
|
-
if (typeof entry !== 'string') {
|
|
1134
|
-
return null;
|
|
1135
|
-
}
|
|
1136
|
-
const trimmed = entry.trim();
|
|
1137
|
-
if (!trimmed) {
|
|
1138
|
-
return null;
|
|
1139
|
-
}
|
|
1140
|
-
// Accept the bracket notation used in virtual-router-hit logs: provider[alias].model
|
|
1141
|
-
// - provider[].model means provider.model across all aliases
|
|
1142
|
-
const bracketMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\[([a-zA-Z0-9_-]*)\](?:\.(.+))?$/);
|
|
1143
|
-
if (bracketMatch) {
|
|
1144
|
-
const provider = bracketMatch[1]?.trim() || '';
|
|
1145
|
-
const keyAlias = bracketMatch[2]?.trim() || '';
|
|
1146
|
-
const model = typeof bracketMatch[3] === 'string' ? bracketMatch[3].trim() : '';
|
|
1147
|
-
if (!provider) {
|
|
1148
|
-
return null;
|
|
1149
|
-
}
|
|
1150
|
-
if (keyAlias) {
|
|
1151
|
-
return {
|
|
1152
|
-
provider,
|
|
1153
|
-
keyAlias,
|
|
1154
|
-
...(model ? { model } : {}),
|
|
1155
|
-
pathLength: 3
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
if (model) {
|
|
1159
|
-
return {
|
|
1160
|
-
provider,
|
|
1161
|
-
model,
|
|
1162
|
-
pathLength: 2
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
return { provider, pathLength: 1 };
|
|
1166
|
-
}
|
|
1167
|
-
// Accept provider.keyAlias.model and provider.model (model may contain dots when keyAlias is explicit).
|
|
1168
|
-
const parts = trimmed.split('.').map((part) => part.trim()).filter(Boolean);
|
|
1169
|
-
if (parts.length === 0) {
|
|
1170
|
-
return null;
|
|
1171
|
-
}
|
|
1172
|
-
const provider = parts[0] || '';
|
|
1173
|
-
if (!provider) {
|
|
1174
|
-
return null;
|
|
1175
|
-
}
|
|
1176
|
-
if (parts.length === 1) {
|
|
1177
|
-
return { provider, pathLength: 1 };
|
|
1178
|
-
}
|
|
1179
|
-
if (parts.length === 2) {
|
|
1180
|
-
const second = parts[1] || '';
|
|
1181
|
-
if (!second) {
|
|
1182
|
-
return null;
|
|
1183
|
-
}
|
|
1184
|
-
if (/^\d+$/.test(second)) {
|
|
1185
|
-
const keyIndex = Number.parseInt(second, 10);
|
|
1186
|
-
return Number.isFinite(keyIndex) && keyIndex > 0 ? { provider, keyIndex, pathLength: 2 } : null;
|
|
1187
|
-
}
|
|
1188
|
-
return { provider, model: second, pathLength: 2 };
|
|
1189
|
-
}
|
|
1190
|
-
const keyAlias = parts[1] || '';
|
|
1191
|
-
const model = parts.slice(2).join('.').trim();
|
|
1192
|
-
if (!keyAlias) {
|
|
1193
|
-
return null;
|
|
1194
|
-
}
|
|
1195
|
-
return {
|
|
1196
|
-
provider,
|
|
1197
|
-
keyAlias,
|
|
1198
|
-
...(model ? { model } : {}),
|
|
1199
|
-
pathLength: 3
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
resolveRoutingMode(instructions, state) {
|
|
1203
|
-
const hasForce = instructions.some((inst) => inst.type === 'force');
|
|
1204
|
-
const hasAllow = instructions.some((inst) => inst.type === 'allow');
|
|
1205
|
-
const hasClear = instructions.some((inst) => inst.type === 'clear');
|
|
1206
|
-
const hasPrefer = instructions.some((inst) => inst.type === 'prefer');
|
|
1207
|
-
if (hasClear) {
|
|
1208
|
-
return 'none';
|
|
1209
|
-
}
|
|
1210
|
-
if (hasAllow || state.allowedProviders.size > 0) {
|
|
1211
|
-
return 'sticky';
|
|
1212
|
-
}
|
|
1213
|
-
if (hasForce || state.forcedTarget) {
|
|
1214
|
-
return 'force';
|
|
1215
|
-
}
|
|
1216
|
-
if (hasPrefer || state.preferTarget) {
|
|
1217
|
-
return 'sticky';
|
|
1218
|
-
}
|
|
1219
|
-
if (state.stickyTarget) {
|
|
1220
|
-
return 'sticky';
|
|
1221
|
-
}
|
|
1222
|
-
return 'none';
|
|
788
|
+
return resolveSessionScopeImpl(metadata);
|
|
1223
789
|
}
|
|
1224
790
|
resolveInstructionTarget(target) {
|
|
1225
791
|
if (!target || !target.provider) {
|
|
@@ -1247,7 +813,7 @@ export class VirtualRouterEngine {
|
|
|
1247
813
|
if (target.model && target.model.trim()) {
|
|
1248
814
|
const normalizedModel = target.model.trim();
|
|
1249
815
|
const matchingKeys = providerKeys.filter((key) => {
|
|
1250
|
-
const modelId =
|
|
816
|
+
const modelId = getProviderModelId(key, this.providerRegistry);
|
|
1251
817
|
return modelId === normalizedModel;
|
|
1252
818
|
});
|
|
1253
819
|
if (matchingKeys.length > 0) {
|
|
@@ -1282,7 +848,7 @@ export class VirtualRouterEngine {
|
|
|
1282
848
|
continue;
|
|
1283
849
|
}
|
|
1284
850
|
for (const providerKey of pool.targets) {
|
|
1285
|
-
const providerId =
|
|
851
|
+
const providerId = extractProviderId(providerKey);
|
|
1286
852
|
// console.log('[filter] checking', providerKey, 'id=', providerId);
|
|
1287
853
|
if (!providerId)
|
|
1288
854
|
continue;
|
|
@@ -1295,8 +861,8 @@ export class VirtualRouterEngine {
|
|
|
1295
861
|
}
|
|
1296
862
|
const disabledKeys = state.disabledKeys.get(providerId);
|
|
1297
863
|
if (disabledKeys && disabledKeys.size > 0) {
|
|
1298
|
-
const keyAlias =
|
|
1299
|
-
const keyIndex =
|
|
864
|
+
const keyAlias = extractKeyAlias(providerKey);
|
|
865
|
+
const keyIndex = extractKeyIndex(providerKey);
|
|
1300
866
|
if (keyAlias && disabledKeys.has(keyAlias)) {
|
|
1301
867
|
continue;
|
|
1302
868
|
}
|
|
@@ -1306,7 +872,7 @@ export class VirtualRouterEngine {
|
|
|
1306
872
|
}
|
|
1307
873
|
const disabledModels = state.disabledModels.get(providerId);
|
|
1308
874
|
if (disabledModels && disabledModels.size > 0) {
|
|
1309
|
-
const modelId =
|
|
875
|
+
const modelId = getProviderModelId(providerKey, this.providerRegistry);
|
|
1310
876
|
if (modelId && disabledModels.has(modelId)) {
|
|
1311
877
|
continue;
|
|
1312
878
|
}
|
|
@@ -1331,12 +897,6 @@ export class VirtualRouterEngine {
|
|
|
1331
897
|
aliasQueueStore: this.aliasQueueStore
|
|
1332
898
|
}, { routingState: state });
|
|
1333
899
|
}
|
|
1334
|
-
extractProviderId(providerKey) {
|
|
1335
|
-
const firstDot = providerKey.indexOf('.');
|
|
1336
|
-
if (firstDot <= 0)
|
|
1337
|
-
return null;
|
|
1338
|
-
return providerKey.substring(0, firstDot);
|
|
1339
|
-
}
|
|
1340
900
|
/**
|
|
1341
901
|
* 在已有候选路由集合上,筛选出真正挂载了 sticky 池内 providerKey 的路由,
|
|
1342
902
|
* 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
|
|
@@ -1410,27 +970,27 @@ export class VirtualRouterEngine {
|
|
|
1410
970
|
// 应用 provider 白名单 / 黑名单
|
|
1411
971
|
if (allowedProviders.size > 0) {
|
|
1412
972
|
candidates = candidates.filter((key) => {
|
|
1413
|
-
const providerId =
|
|
973
|
+
const providerId = extractProviderId(key);
|
|
1414
974
|
return providerId && allowedProviders.has(providerId);
|
|
1415
975
|
});
|
|
1416
976
|
}
|
|
1417
977
|
if (disabledProviders.size > 0) {
|
|
1418
978
|
candidates = candidates.filter((key) => {
|
|
1419
|
-
const providerId =
|
|
979
|
+
const providerId = extractProviderId(key);
|
|
1420
980
|
return providerId && !disabledProviders.has(providerId);
|
|
1421
981
|
});
|
|
1422
982
|
}
|
|
1423
983
|
// 应用 key / model 级别黑名单
|
|
1424
984
|
if (disabledKeysMap.size > 0 || disabledModels.size > 0) {
|
|
1425
985
|
candidates = candidates.filter((key) => {
|
|
1426
|
-
const providerId =
|
|
986
|
+
const providerId = extractProviderId(key);
|
|
1427
987
|
if (!providerId) {
|
|
1428
988
|
return true;
|
|
1429
989
|
}
|
|
1430
990
|
const disabledKeys = disabledKeysMap.get(providerId);
|
|
1431
991
|
if (disabledKeys && disabledKeys.size > 0) {
|
|
1432
|
-
const keyAlias =
|
|
1433
|
-
const keyIndex =
|
|
992
|
+
const keyAlias = extractKeyAlias(key);
|
|
993
|
+
const keyIndex = extractKeyIndex(key);
|
|
1434
994
|
if (keyAlias && disabledKeys.has(keyAlias)) {
|
|
1435
995
|
return false;
|
|
1436
996
|
}
|
|
@@ -1440,7 +1000,7 @@ export class VirtualRouterEngine {
|
|
|
1440
1000
|
}
|
|
1441
1001
|
const disabledModelSet = disabledModels.get(providerId);
|
|
1442
1002
|
if (disabledModelSet && disabledModelSet.size > 0) {
|
|
1443
|
-
const modelId =
|
|
1003
|
+
const modelId = getProviderModelId(key, this.providerRegistry);
|
|
1444
1004
|
if (modelId && disabledModelSet.has(modelId)) {
|
|
1445
1005
|
return false;
|
|
1446
1006
|
}
|
|
@@ -1458,43 +1018,6 @@ export class VirtualRouterEngine {
|
|
|
1458
1018
|
// delegate to selection module
|
|
1459
1019
|
return null;
|
|
1460
1020
|
}
|
|
1461
|
-
extractKeyAlias(providerKey) {
|
|
1462
|
-
const parts = providerKey.split('.');
|
|
1463
|
-
if (parts.length === 3) {
|
|
1464
|
-
return this.normalizeAliasDescriptor(parts[1]);
|
|
1465
|
-
}
|
|
1466
|
-
return null;
|
|
1467
|
-
}
|
|
1468
|
-
normalizeAliasDescriptor(alias) {
|
|
1469
|
-
if (/^\d+-/.test(alias)) {
|
|
1470
|
-
return alias.replace(/^\d+-/, '');
|
|
1471
|
-
}
|
|
1472
|
-
return alias;
|
|
1473
|
-
}
|
|
1474
|
-
extractKeyIndex(providerKey) {
|
|
1475
|
-
const parts = providerKey.split('.');
|
|
1476
|
-
if (parts.length === 2) {
|
|
1477
|
-
const index = parseInt(parts[1], 10);
|
|
1478
|
-
if (!isNaN(index) && index > 0) {
|
|
1479
|
-
return index;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
return undefined;
|
|
1483
|
-
}
|
|
1484
|
-
getProviderModelId(providerKey) {
|
|
1485
|
-
const profile = this.providerRegistry.get(providerKey);
|
|
1486
|
-
if (profile.modelId) {
|
|
1487
|
-
return profile.modelId;
|
|
1488
|
-
}
|
|
1489
|
-
const parts = providerKey.split('.');
|
|
1490
|
-
if (parts.length === 2) {
|
|
1491
|
-
return parts[1] || null;
|
|
1492
|
-
}
|
|
1493
|
-
if (parts.length === 3) {
|
|
1494
|
-
return parts[2] || null;
|
|
1495
|
-
}
|
|
1496
|
-
return null;
|
|
1497
|
-
}
|
|
1498
1021
|
// mapProviderError/applySeriesCooldown moved to engine-health.ts
|
|
1499
1022
|
extractExcludedProviderKeySet(metadata) {
|
|
1500
1023
|
if (!metadata) {
|
|
@@ -1531,10 +1054,27 @@ export class VirtualRouterEngine {
|
|
|
1531
1054
|
}
|
|
1532
1055
|
}
|
|
1533
1056
|
}
|
|
1057
|
+
if (features.hasImageAttachment) {
|
|
1058
|
+
const allRouteNames = Object.keys(this.routing);
|
|
1059
|
+
for (const routeName of allRouteNames) {
|
|
1060
|
+
if (!this.routeHasTargets(this.routing[routeName])) {
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (!this.routeSupportsModel(routeName, 'kimi-k2.5')) {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (!baseList.includes(routeName)) {
|
|
1067
|
+
baseList.push(routeName);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1534
1071
|
let ordered = this.sortByPriority(baseList);
|
|
1535
1072
|
if (features.hasImageAttachment && !forceVision) {
|
|
1536
1073
|
ordered = this.reorderForInlineVision(ordered);
|
|
1537
1074
|
}
|
|
1075
|
+
if (features.hasImageAttachment) {
|
|
1076
|
+
ordered = this.reorderForPreferredModel(ordered, 'kimi-k2.5');
|
|
1077
|
+
}
|
|
1538
1078
|
const deduped = [];
|
|
1539
1079
|
for (const routeName of ordered) {
|
|
1540
1080
|
if (routeName && !deduped.includes(routeName)) {
|
|
@@ -1574,6 +1114,46 @@ export class VirtualRouterEngine {
|
|
|
1574
1114
|
}
|
|
1575
1115
|
return [...inlinePreferred, ...remaining];
|
|
1576
1116
|
}
|
|
1117
|
+
reorderForPreferredModel(routeNames, modelId) {
|
|
1118
|
+
const unique = Array.from(new Set(routeNames.filter(Boolean)));
|
|
1119
|
+
if (!unique.length) {
|
|
1120
|
+
return unique;
|
|
1121
|
+
}
|
|
1122
|
+
const preferred = unique.filter((routeName) => this.routeSupportsModel(routeName, modelId));
|
|
1123
|
+
if (!preferred.length) {
|
|
1124
|
+
return unique;
|
|
1125
|
+
}
|
|
1126
|
+
const remaining = unique.filter((routeName) => !preferred.includes(routeName));
|
|
1127
|
+
return [...preferred, ...remaining];
|
|
1128
|
+
}
|
|
1129
|
+
routeSupportsModel(routeName, modelId) {
|
|
1130
|
+
const normalizedModel = modelId.trim().toLowerCase();
|
|
1131
|
+
if (!normalizedModel) {
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
const pools = this.routing[routeName];
|
|
1135
|
+
if (!Array.isArray(pools)) {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
for (const pool of pools) {
|
|
1139
|
+
if (!Array.isArray(pool.targets)) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
for (const providerKey of pool.targets) {
|
|
1143
|
+
try {
|
|
1144
|
+
const profile = this.providerRegistry.get(providerKey);
|
|
1145
|
+
const candidate = typeof profile.modelId === 'string' ? profile.modelId.trim().toLowerCase() : '';
|
|
1146
|
+
if (candidate === normalizedModel) {
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
// ignore unknown provider keys during capability probing
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1577
1157
|
routeSupportsInlineVision(routeName) {
|
|
1578
1158
|
const pools = this.routing[routeName];
|
|
1579
1159
|
if (!Array.isArray(pools)) {
|
|
@@ -1657,59 +1237,6 @@ export class VirtualRouterEngine {
|
|
|
1657
1237
|
}
|
|
1658
1238
|
return flattened;
|
|
1659
1239
|
}
|
|
1660
|
-
isRoutingStateEmpty(state) {
|
|
1661
|
-
if (!state) {
|
|
1662
|
-
return true;
|
|
1663
|
-
}
|
|
1664
|
-
const noForced = !state.forcedTarget;
|
|
1665
|
-
const noSticky = !state.stickyTarget;
|
|
1666
|
-
const noPrefer = !state.preferTarget;
|
|
1667
|
-
const noAllowed = state.allowedProviders.size === 0;
|
|
1668
|
-
const noDisabledProviders = state.disabledProviders.size === 0;
|
|
1669
|
-
const noDisabledKeys = state.disabledKeys.size === 0;
|
|
1670
|
-
const noDisabledModels = state.disabledModels.size === 0;
|
|
1671
|
-
const noStopMessage = (!state.stopMessageText || !state.stopMessageText.trim()) &&
|
|
1672
|
-
(typeof state.stopMessageMaxRepeats !== 'number' || !Number.isFinite(state.stopMessageMaxRepeats)) &&
|
|
1673
|
-
(typeof state.stopMessageUsed !== 'number' || !Number.isFinite(state.stopMessageUsed)) &&
|
|
1674
|
-
(typeof state.stopMessageUpdatedAt !== 'number' || !Number.isFinite(state.stopMessageUpdatedAt)) &&
|
|
1675
|
-
(typeof state.stopMessageLastUsedAt !== 'number' || !Number.isFinite(state.stopMessageLastUsedAt));
|
|
1676
|
-
return (noForced &&
|
|
1677
|
-
noSticky &&
|
|
1678
|
-
noPrefer &&
|
|
1679
|
-
noAllowed &&
|
|
1680
|
-
noDisabledProviders &&
|
|
1681
|
-
noDisabledKeys &&
|
|
1682
|
-
noDisabledModels &&
|
|
1683
|
-
noStopMessage);
|
|
1684
|
-
}
|
|
1685
|
-
persistRoutingInstructionState(key, state) {
|
|
1686
|
-
if (!key || (!key.startsWith('session:') && !key.startsWith('conversation:'))) {
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
const supportsSync = typeof this.routingStateStore.saveSync === 'function';
|
|
1690
|
-
const prefersSync = supportsSync &&
|
|
1691
|
-
key.startsWith('session:') &&
|
|
1692
|
-
(Boolean(state.stopMessageText && state.stopMessageText.trim()) ||
|
|
1693
|
-
(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)) ||
|
|
1694
|
-
(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)) ||
|
|
1695
|
-
(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)) ||
|
|
1696
|
-
(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)));
|
|
1697
|
-
if (this.isRoutingStateEmpty(state)) {
|
|
1698
|
-
if (prefersSync) {
|
|
1699
|
-
this.routingStateStore.saveSync(key, null);
|
|
1700
|
-
}
|
|
1701
|
-
else {
|
|
1702
|
-
this.routingStateStore.saveAsync(key, null);
|
|
1703
|
-
}
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
if (prefersSync) {
|
|
1707
|
-
this.routingStateStore.saveSync(key, state);
|
|
1708
|
-
}
|
|
1709
|
-
else {
|
|
1710
|
-
this.routingStateStore.saveAsync(key, state);
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
1240
|
markProviderCooldown(providerKey, cooldownMs) {
|
|
1714
1241
|
if (!providerKey) {
|
|
1715
1242
|
return;
|