@jsonstudio/llms 0.6.1892 → 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-request.js +16 -2
- package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
- package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
- package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
- package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
- package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
- package/dist/conversion/compat/antigravity-session-signature.js +15 -0
- package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
- package/dist/conversion/compat/profiles/chat-glm.json +22 -0
- package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
- package/dist/conversion/hub/process/chat-process.js +85 -18
- package/dist/conversion/hub/response/provider-response.js +21 -50
- package/dist/conversion/hub/response/response-runtime.js +71 -10
- package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
- package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
- package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
- package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
- package/dist/conversion/responses/responses-openai-bridge.js +193 -504
- package/dist/conversion/shared/anthropic-message-utils.js +82 -2
- package/dist/conversion/shared/bridge-message-utils.js +92 -39
- package/dist/conversion/shared/snapshot-hooks.js +8 -13
- package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
- package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
- package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
- package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
- package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
- package/dist/conversion/shared/tool-governor.js +136 -10
- package/dist/filters/utils/snapshot-writer.js +3 -3
- package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
- package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
- package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
- package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
- package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
- package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
- package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
- package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
- package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
- package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
- package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
- package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
- package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
- package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
- package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
- package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
- package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
- package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
- package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
- package/dist/router/virtual-router/bootstrap/utils.js +41 -0
- package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
- package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
- package/dist/router/virtual-router/bootstrap.d.ts +0 -4
- package/dist/router/virtual-router/bootstrap.js +31 -1275
- package/dist/router/virtual-router/classifier.js +32 -14
- package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
- package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
- package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
- package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
- package/dist/router/virtual-router/engine/route-analytics.js +44 -0
- package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
- package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
- package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
- package/dist/router/virtual-router/engine-logging.d.ts +42 -1
- package/dist/router/virtual-router/engine-logging.js +82 -15
- 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 +31 -1
- package/dist/router/virtual-router/engine.d.ts +21 -7
- package/dist/router/virtual-router/engine.js +198 -194
- 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/pre-command-file-resolver.js +40 -2
- package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -2
- package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
- package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
- package/dist/router/virtual-router/token-counter.js +51 -10
- package/dist/router/virtual-router/tool-signals.js +4 -0
- package/dist/router/virtual-router/types.d.ts +15 -0
- package/dist/servertool/clock/session-scope.d.ts +3 -0
- package/dist/servertool/clock/session-scope.js +52 -0
- package/dist/servertool/clock/state.js +9 -0
- package/dist/servertool/clock/tasks.js +12 -1
- package/dist/servertool/clock/types.d.ts +3 -0
- package/dist/servertool/engine.js +177 -31
- package/dist/servertool/handlers/clock-auto.js +2 -8
- package/dist/servertool/handlers/clock.js +6 -9
- package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
- 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 -1
- package/dist/servertool/handlers/stop-message-auto.js +80 -556
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
- package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
- package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
- package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
- package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
- package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
- package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
- package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
- package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
- package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
- package/dist/servertool/handlers/web-search.js +117 -0
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +4 -3
- package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
- package/dist/telemetry/stats-center.d.ts +9 -0
- package/dist/telemetry/stats-center.js +29 -1
- package/dist/tools/apply-patch/structured/coercion.js +3 -11
- package/dist/tools/exec-command/validator.d.ts +1 -0
- package/dist/tools/exec-command/validator.js +132 -0
- package/dist/tools/tool-registry.d.ts +1 -0
- package/dist/tools/tool-registry.js +1 -1
- package/package.json +1 -1
|
@@ -9,15 +9,20 @@ import { getStatsCenter } from '../../telemetry/stats-center.js';
|
|
|
9
9
|
import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRoutingInstructions } from './routing-instructions.js';
|
|
10
10
|
import { extractMessageText } from './message-utils.js';
|
|
11
11
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from './sticky-session-store.js';
|
|
12
|
-
import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
|
|
12
|
+
import { buildHitReason, createVirtualRouterHitRecord, formatVirtualRouterHit, toVirtualRouterHitEvent } from './engine-logging.js';
|
|
13
13
|
import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine/routing-pools/index.js';
|
|
14
14
|
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine/health/index.js';
|
|
15
15
|
import { hydrateAntigravityAliasLeaseStoreIfNeeded, recordAntigravitySessionLease, resolveAntigravityAliasReuseCooldownMs } from './engine/antigravity/alias-lease.js';
|
|
16
16
|
import { buildMetadataInstructions, resolveRoutingMode } from './engine/routing-state/metadata.js';
|
|
17
17
|
import { getRoutingInstructionState, persistRoutingInstructionState, resolveStopMessageScope } from './engine/routing-state/store.js';
|
|
18
|
+
import { ensureStopMessageModeMaxRepeats } from './routing-stop-message-state-codec.js';
|
|
18
19
|
import { validateStopMessageStageTemplatesCompleteness } from './stop-message-stage-template-files.js';
|
|
19
20
|
import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './engine/provider-key/parse.js';
|
|
20
21
|
import { resolveSessionScope as resolveSessionScopeImpl, resolveStickyKey as resolveStickyKeyImpl } from './engine/routing-state/keys.js';
|
|
22
|
+
import { RouteAnalytics } from './engine/route-analytics.js';
|
|
23
|
+
import { StickySessionManager } from './engine/sticky-session-manager.js';
|
|
24
|
+
import { CooldownManager } from './engine/cooldown-manager.js';
|
|
25
|
+
const DEFAULT_STOP_MESSAGE_MAX_REPEATS = 10;
|
|
21
26
|
function normalizeStopMessageStageMode(value) {
|
|
22
27
|
if (typeof value !== 'string') {
|
|
23
28
|
return undefined;
|
|
@@ -72,9 +77,12 @@ function hasRoutingInstructionMarker(messages) {
|
|
|
72
77
|
function hasLatestUserRoutingInstructionMarker(messages) {
|
|
73
78
|
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
74
79
|
const message = messages[idx];
|
|
75
|
-
if (!message
|
|
80
|
+
if (!message) {
|
|
76
81
|
continue;
|
|
77
82
|
}
|
|
83
|
+
if (message.role !== 'user') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
78
86
|
const content = extractMessageText(message);
|
|
79
87
|
if (!content) {
|
|
80
88
|
return false;
|
|
@@ -83,25 +91,30 @@ function hasLatestUserRoutingInstructionMarker(messages) {
|
|
|
83
91
|
}
|
|
84
92
|
return false;
|
|
85
93
|
}
|
|
94
|
+
function isServerToolFollowupRequest(metadata) {
|
|
95
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const rt = metadata.__rt;
|
|
99
|
+
if (!rt || typeof rt !== 'object' || Array.isArray(rt)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const flag = rt.serverToolFollowup;
|
|
103
|
+
return (flag === true ||
|
|
104
|
+
(typeof flag === 'string' && flag.trim().toLowerCase() === 'true'));
|
|
105
|
+
}
|
|
86
106
|
export class VirtualRouterEngine {
|
|
87
107
|
routing = {};
|
|
88
108
|
providerRegistry = new ProviderRegistry();
|
|
89
109
|
healthManager = new ProviderHealthManager();
|
|
90
|
-
providerCooldowns
|
|
110
|
+
get providerCooldowns() { return this.cooldownManager.getCooldownMap(); }
|
|
91
111
|
loadBalancer = new RouteLoadBalancer();
|
|
92
112
|
classifier = new RoutingClassifier({});
|
|
93
113
|
contextAdvisor = new ContextAdvisor();
|
|
94
114
|
contextRouting;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
aliasQueueStore = new Map();
|
|
99
|
-
// Antigravity alias session lease store:
|
|
100
|
-
// - Enforces that one auth alias is not shared across different sessions within a cooldown window.
|
|
101
|
-
// - Tracks lastSeenAt per alias runtimeKey (providerId.keyAlias).
|
|
102
|
-
antigravityAliasLeaseStore = new Map();
|
|
103
|
-
antigravitySessionAliasStore = new Map();
|
|
104
|
-
antigravityAliasReuseCooldownMs = 5 * 60_000;
|
|
115
|
+
routeAnalytics = new RouteAnalytics();
|
|
116
|
+
stickySessionManager = new StickySessionManager();
|
|
117
|
+
cooldownManager;
|
|
105
118
|
antigravityLeasePersistence = { loadedOnce: false, loadedMtimeMs: null, flushTimer: null };
|
|
106
119
|
debug = console; // thin hook; host may monkey-patch for colored logging
|
|
107
120
|
healthConfig = null;
|
|
@@ -116,7 +129,19 @@ export class VirtualRouterEngine {
|
|
|
116
129
|
};
|
|
117
130
|
routingInstructionState = new Map();
|
|
118
131
|
quotaView;
|
|
132
|
+
/**
|
|
133
|
+
* Backward-compatible test/debug surface used by existing regression scripts.
|
|
134
|
+
* Keep this as a read-only view over StickySessionManager storage.
|
|
135
|
+
*/
|
|
136
|
+
get antigravitySessionAliasStore() {
|
|
137
|
+
return this.stickySessionManager.getAllStores().sessionAliasStore;
|
|
138
|
+
}
|
|
119
139
|
constructor(deps) {
|
|
140
|
+
this.cooldownManager = new CooldownManager({
|
|
141
|
+
healthStore: deps?.healthStore,
|
|
142
|
+
healthConfig: this.healthConfig,
|
|
143
|
+
quotaView: deps?.quotaView
|
|
144
|
+
});
|
|
120
145
|
if (deps?.healthStore) {
|
|
121
146
|
this.healthStore = deps.healthStore;
|
|
122
147
|
}
|
|
@@ -133,6 +158,7 @@ export class VirtualRouterEngine {
|
|
|
133
158
|
}
|
|
134
159
|
if ('healthStore' in deps) {
|
|
135
160
|
this.healthStore = deps.healthStore ?? undefined;
|
|
161
|
+
this.cooldownManager.updateDeps({ healthStore: this.healthStore });
|
|
136
162
|
}
|
|
137
163
|
if ('routingStateStore' in deps) {
|
|
138
164
|
this.routingStateStore =
|
|
@@ -148,15 +174,16 @@ export class VirtualRouterEngine {
|
|
|
148
174
|
if ('quotaView' in deps) {
|
|
149
175
|
const prevQuotaEnabled = Boolean(this.quotaView);
|
|
150
176
|
this.quotaView = deps.quotaView ?? undefined;
|
|
177
|
+
this.cooldownManager.updateDeps({ quotaView: this.quotaView });
|
|
151
178
|
const nextQuotaEnabled = Boolean(this.quotaView);
|
|
152
179
|
// When quotaView is enabled, health/cooldown decisions must be driven by quotaView only.
|
|
153
180
|
// - Enabling quotaView: clear any legacy router-local cooldown TTLs immediately.
|
|
154
181
|
// - Disabling quotaView: reload legacy cooldown state from health snapshots.
|
|
155
182
|
if (!prevQuotaEnabled && nextQuotaEnabled) {
|
|
156
|
-
this.
|
|
183
|
+
this.cooldownManager.clearAllCooldowns();
|
|
157
184
|
}
|
|
158
185
|
else if (prevQuotaEnabled && !nextQuotaEnabled) {
|
|
159
|
-
this.
|
|
186
|
+
this.cooldownManager.clearAllCooldowns();
|
|
160
187
|
this.restoreHealthFromStore();
|
|
161
188
|
}
|
|
162
189
|
}
|
|
@@ -180,6 +207,24 @@ export class VirtualRouterEngine {
|
|
|
180
207
|
}
|
|
181
208
|
return { providerId, modelId };
|
|
182
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
|
+
}
|
|
183
228
|
initialize(config) {
|
|
184
229
|
this.validateConfig(config);
|
|
185
230
|
this.routing = config.routing;
|
|
@@ -187,23 +232,24 @@ export class VirtualRouterEngine {
|
|
|
187
232
|
this.healthManager.configure(config.health);
|
|
188
233
|
this.healthConfig = config.health ?? null;
|
|
189
234
|
this.healthManager.registerProviders(Object.keys(config.providers));
|
|
190
|
-
this.
|
|
235
|
+
this.cooldownManager.clearAllCooldowns();
|
|
191
236
|
this.restoreHealthFromStore();
|
|
192
237
|
this.loadBalancer = new RouteLoadBalancer(config.loadBalancing);
|
|
193
|
-
|
|
238
|
+
const aliasReuseCooldownMs = resolveAntigravityAliasReuseCooldownMs(config);
|
|
239
|
+
this.stickySessionManager = new StickySessionManager(aliasReuseCooldownMs);
|
|
194
240
|
hydrateAntigravityAliasLeaseStoreIfNeeded({
|
|
195
241
|
force: true,
|
|
196
|
-
leaseStore: this.
|
|
242
|
+
leaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
197
243
|
persistence: this.antigravityLeasePersistence,
|
|
198
|
-
aliasReuseCooldownMs: this.
|
|
244
|
+
aliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs()
|
|
199
245
|
});
|
|
200
246
|
this.classifier = new RoutingClassifier(config.classifier);
|
|
201
247
|
this.contextRouting = config.contextRouting ?? { warnRatio: 0.9, hardLimit: false };
|
|
202
248
|
this.contextAdvisor.configure(this.contextRouting);
|
|
203
249
|
this.webSearchForce = config.webSearch?.force === true;
|
|
204
|
-
this.
|
|
250
|
+
this.routeAnalytics.getAllRouteStats().clear();
|
|
205
251
|
for (const routeName of Object.keys(this.routing)) {
|
|
206
|
-
this.
|
|
252
|
+
this.routeAnalytics.getRouteStats(routeName) || this.routeAnalytics.incrementRouteStat(routeName, '', { timestampMs: Date.now(), stopMessage: { active: false } });
|
|
207
253
|
}
|
|
208
254
|
}
|
|
209
255
|
route(request, metadata) {
|
|
@@ -231,6 +277,10 @@ export class VirtualRouterEngine {
|
|
|
231
277
|
}
|
|
232
278
|
if (stopMessageScope) {
|
|
233
279
|
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
280
|
+
if (ensureStopMessageModeMaxRepeats(sessionState)) {
|
|
281
|
+
this.routingInstructionState.set(stopMessageScope, sessionState);
|
|
282
|
+
persistRoutingInstructionState(stopMessageScope, sessionState, this.routingStateStore);
|
|
283
|
+
}
|
|
234
284
|
if (typeof sessionState.stopMessageText === 'string' ||
|
|
235
285
|
typeof sessionState.stopMessageMaxRepeats === 'number' ||
|
|
236
286
|
typeof sessionState.stopMessageStageMode === 'string') {
|
|
@@ -258,8 +308,16 @@ export class VirtualRouterEngine {
|
|
|
258
308
|
}
|
|
259
309
|
}
|
|
260
310
|
const parsedInstructions = parseRoutingInstructions(request.messages);
|
|
311
|
+
const serverToolFollowup = isServerToolFollowupRequest(metadata);
|
|
261
312
|
const latestUserHasMarker = hasLatestUserRoutingInstructionMarker(request.messages);
|
|
262
313
|
let instructions = parsedInstructions;
|
|
314
|
+
if (serverToolFollowup && instructions.length > 0) {
|
|
315
|
+
instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet' &&
|
|
316
|
+
entry.type !== 'stopMessageMode' &&
|
|
317
|
+
entry.type !== 'stopMessageClear' &&
|
|
318
|
+
entry.type !== 'preCommandSet' &&
|
|
319
|
+
entry.type !== 'preCommandClear');
|
|
320
|
+
}
|
|
263
321
|
if (stopMessageScope && parsedInstructions.length > 0) {
|
|
264
322
|
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
265
323
|
const hasStaleStopMessageInstruction = !latestUserHasMarker &&
|
|
@@ -278,7 +336,8 @@ export class VirtualRouterEngine {
|
|
|
278
336
|
entry.type !== 'stopMessageClear');
|
|
279
337
|
}
|
|
280
338
|
}
|
|
281
|
-
const
|
|
339
|
+
const hasGlobalClear = instructions.some((entry) => entry.type === 'clear');
|
|
340
|
+
const hasStopMessageClear = hasGlobalClear || instructions.some((entry) => entry.type === 'stopMessageClear');
|
|
282
341
|
const stopMessageSets = instructions.filter((entry) => entry.type === 'stopMessageSet');
|
|
283
342
|
if (!hasStopMessageClear && stopMessageSets.length > 0) {
|
|
284
343
|
const sessionText = typeof sessionState.stopMessageText === 'string' ? sessionState.stopMessageText.trim() : '';
|
|
@@ -308,8 +367,8 @@ export class VirtualRouterEngine {
|
|
|
308
367
|
}
|
|
309
368
|
// stopMessage must be session-scoped: require explicit sessionId in metadata.
|
|
310
369
|
// This prevents global/default persistence and ensures the trigger matches the setting sessionId.
|
|
311
|
-
if (
|
|
312
|
-
const hasSessionScopedInstruction =
|
|
370
|
+
if (instructions.length > 0) {
|
|
371
|
+
const hasSessionScopedInstruction = instructions.some((entry) => entry.type === 'stopMessageSet' ||
|
|
313
372
|
entry.type === 'stopMessageMode' ||
|
|
314
373
|
entry.type === 'stopMessageClear' ||
|
|
315
374
|
entry.type === 'preCommandSet' ||
|
|
@@ -318,7 +377,7 @@ export class VirtualRouterEngine {
|
|
|
318
377
|
throw new VirtualRouterError('[stopMessage/precommand] requires sessionId (e.g. set x-session-id header or metadata.sessionId).', VirtualRouterErrorCode.CONFIG_ERROR, { requestId: metadata.requestId, entryEndpoint: metadata.entryEndpoint });
|
|
319
378
|
}
|
|
320
379
|
}
|
|
321
|
-
if (stopMessageScope &&
|
|
380
|
+
if (stopMessageScope && instructions.length > 0) {
|
|
322
381
|
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
323
382
|
const nextStopMessageMode = resolveStopMessageStageModeAfterInstructions(instructions, sessionState.stopMessageStageMode);
|
|
324
383
|
if (nextStopMessageMode === 'on') {
|
|
@@ -345,7 +404,8 @@ export class VirtualRouterEngine {
|
|
|
345
404
|
if (stopMessageScope) {
|
|
346
405
|
const hasStopMessageSet = instructions.some((entry) => entry.type === 'stopMessageSet');
|
|
347
406
|
const hasStopMessageMode = instructions.some((entry) => entry.type === 'stopMessageMode');
|
|
348
|
-
const
|
|
407
|
+
const hasGlobalClear = instructions.some((entry) => entry.type === 'clear');
|
|
408
|
+
const hasStopMessageClear = hasGlobalClear || instructions.some((entry) => entry.type === 'stopMessageClear');
|
|
349
409
|
if (hasStopMessageSet || hasStopMessageMode || hasStopMessageClear) {
|
|
350
410
|
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
351
411
|
let nextSessionState = {
|
|
@@ -436,10 +496,27 @@ export class VirtualRouterEngine {
|
|
|
436
496
|
nextSessionState.stopMessageStageMode = mode;
|
|
437
497
|
shouldPersistSessionState = true;
|
|
438
498
|
}
|
|
439
|
-
|
|
499
|
+
const fallbackMaxRepeats = mode === 'on' || mode === 'auto'
|
|
500
|
+
? DEFAULT_STOP_MESSAGE_MAX_REPEATS
|
|
501
|
+
: 0;
|
|
502
|
+
const normalizedMaxRepeats = modeMaxRepeats > 0 ? modeMaxRepeats : fallbackMaxRepeats;
|
|
503
|
+
if (normalizedMaxRepeats > 0 &&
|
|
440
504
|
(typeof nextSessionState.stopMessageMaxRepeats !== 'number' ||
|
|
441
|
-
Math.floor(nextSessionState.stopMessageMaxRepeats) !==
|
|
442
|
-
nextSessionState.stopMessageMaxRepeats =
|
|
505
|
+
Math.floor(nextSessionState.stopMessageMaxRepeats) !== normalizedMaxRepeats)) {
|
|
506
|
+
nextSessionState.stopMessageMaxRepeats = normalizedMaxRepeats;
|
|
507
|
+
shouldPersistSessionState = true;
|
|
508
|
+
}
|
|
509
|
+
// Explicit mode marker from latest user turn should re-arm lifecycle counters,
|
|
510
|
+
// even when mode/max are unchanged (e.g. <**stopMessage:on,10**>).
|
|
511
|
+
if (mode === 'on' || mode === 'auto') {
|
|
512
|
+
nextSessionState.stopMessageUsed = 0;
|
|
513
|
+
nextSessionState.stopMessageUpdatedAt = Date.now();
|
|
514
|
+
nextSessionState.stopMessageLastUsedAt = undefined;
|
|
515
|
+
nextSessionState.stopMessageStage = undefined;
|
|
516
|
+
nextSessionState.stopMessageSource = 'explicit';
|
|
517
|
+
nextSessionState.stopMessageObservationHash = undefined;
|
|
518
|
+
nextSessionState.stopMessageObservationStableCount = 0;
|
|
519
|
+
nextSessionState.stopMessageBdWorkState = undefined;
|
|
443
520
|
shouldPersistSessionState = true;
|
|
444
521
|
}
|
|
445
522
|
}
|
|
@@ -579,6 +656,7 @@ export class VirtualRouterEngine {
|
|
|
579
656
|
quotaView: this.quotaView
|
|
580
657
|
};
|
|
581
658
|
if (directProviderModel) {
|
|
659
|
+
const forceMediaFallback = this.shouldFallbackDirectModelForMedia(directProviderModel, features);
|
|
582
660
|
const providerKeys = this.providerRegistry.listProviderKeys(directProviderModel.providerId);
|
|
583
661
|
let hasModel = false;
|
|
584
662
|
for (const key of providerKeys) {
|
|
@@ -596,19 +674,26 @@ export class VirtualRouterEngine {
|
|
|
596
674
|
if (!hasModel) {
|
|
597
675
|
throw new VirtualRouterError(`Unknown model ${directProviderModel.modelId} for provider ${directProviderModel.providerId}`, VirtualRouterErrorCode.CONFIG_ERROR, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
|
|
598
676
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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);
|
|
610
696
|
}
|
|
611
|
-
selection = directSelection;
|
|
612
697
|
}
|
|
613
698
|
else {
|
|
614
699
|
// Prefer target (from "<**!provider.model**>") is evaluated before routing classification.
|
|
@@ -763,38 +848,44 @@ export class VirtualRouterEngine {
|
|
|
763
848
|
providerKey: selection.providerKey,
|
|
764
849
|
sessionKey: this.resolveSessionScope(features.metadata),
|
|
765
850
|
providerRegistry: this.providerRegistry,
|
|
766
|
-
leaseStore: this.
|
|
767
|
-
sessionAliasStore: this.
|
|
851
|
+
leaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
852
|
+
sessionAliasStore: this.stickySessionManager.getAllStores().sessionAliasStore,
|
|
768
853
|
persistence: this.antigravityLeasePersistence,
|
|
769
|
-
aliasReuseCooldownMs: this.
|
|
854
|
+
aliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs(),
|
|
770
855
|
commitSessionBinding: false,
|
|
771
856
|
debug: this.debug
|
|
772
857
|
});
|
|
773
|
-
this.incrementRouteStat(selection.routeUsed, selection.providerKey);
|
|
774
858
|
const routingMode = resolveRoutingMode([...metadataInstructions, ...instructions], routingState);
|
|
859
|
+
const hitReason = buildHitReason(selection.routeUsed, selection.providerKey, classification, features, routingMode, { providerRegistry: this.providerRegistry, contextRouting: this.contextRouting });
|
|
860
|
+
const stickyScope = routingMode !== 'none' ? this.resolveSessionScope(metadata) : undefined;
|
|
861
|
+
const routeForLog = routingMode === 'sticky' ? 'sticky' : selection.routeUsed;
|
|
862
|
+
const hitRecord = createVirtualRouterHitRecord({
|
|
863
|
+
routeName: routeForLog,
|
|
864
|
+
poolId: selection.poolId,
|
|
865
|
+
providerKey: selection.providerKey,
|
|
866
|
+
modelId: target.modelId || undefined,
|
|
867
|
+
hitReason,
|
|
868
|
+
stickyScope,
|
|
869
|
+
routingState,
|
|
870
|
+
requestTokens: features.estimatedTokens,
|
|
871
|
+
selectionPenalty: this.resolveSelectionPenalty(selection.providerKey)
|
|
872
|
+
});
|
|
873
|
+
this.routeAnalytics.incrementRouteStat(selection.routeUsed, selection.providerKey, hitRecord);
|
|
775
874
|
try {
|
|
776
|
-
this.statsCenter.recordVirtualRouterHit({
|
|
875
|
+
this.statsCenter.recordVirtualRouterHit(toVirtualRouterHitEvent(hitRecord, {
|
|
777
876
|
requestId: metadata.requestId,
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
routeName: selection.routeUsed,
|
|
781
|
-
pool: selection.poolId || selection.routeUsed,
|
|
782
|
-
providerKey: selection.providerKey,
|
|
783
|
-
modelId: target.modelId || undefined
|
|
784
|
-
});
|
|
877
|
+
entryEndpoint: metadata.entryEndpoint || '/v1/chat/completions'
|
|
878
|
+
}));
|
|
785
879
|
}
|
|
786
880
|
catch {
|
|
787
881
|
// stats must never break routing
|
|
788
882
|
}
|
|
789
|
-
const
|
|
790
|
-
const stickyScope = routingMode !== 'none' ? this.resolveSessionScope(metadata) : undefined;
|
|
791
|
-
const routeForLog = routingMode === 'sticky' ? 'sticky' : selection.routeUsed;
|
|
792
|
-
const formatted = formatVirtualRouterHit(routeForLog, selection.poolId, selection.providerKey, target.modelId || '', hitReason, stickyScope, routingState);
|
|
883
|
+
const formatted = formatVirtualRouterHit(hitRecord);
|
|
793
884
|
if (formatted) {
|
|
794
885
|
this.debug?.log?.(formatted);
|
|
795
886
|
}
|
|
796
887
|
else {
|
|
797
|
-
this.debug?.log?.(
|
|
888
|
+
this.debug?.log?.(formatVirtualRouterHit(hitRecord));
|
|
798
889
|
}
|
|
799
890
|
const didFallback = selection.routeUsed !== requestedRoute;
|
|
800
891
|
return {
|
|
@@ -986,10 +1077,10 @@ export class VirtualRouterEngine {
|
|
|
986
1077
|
providerKey,
|
|
987
1078
|
sessionKey: this.resolveSessionScope(metadata),
|
|
988
1079
|
providerRegistry: this.providerRegistry,
|
|
989
|
-
leaseStore: this.
|
|
990
|
-
sessionAliasStore: this.
|
|
1080
|
+
leaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
1081
|
+
sessionAliasStore: this.stickySessionManager.getAllStores().sessionAliasStore,
|
|
991
1082
|
persistence: this.antigravityLeasePersistence,
|
|
992
|
-
aliasReuseCooldownMs: this.
|
|
1083
|
+
aliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs(),
|
|
993
1084
|
commitSessionBinding: true,
|
|
994
1085
|
debug: this.debug
|
|
995
1086
|
});
|
|
@@ -998,11 +1089,12 @@ export class VirtualRouterEngine {
|
|
|
998
1089
|
getStatus() {
|
|
999
1090
|
const routes = {};
|
|
1000
1091
|
for (const [route, pools] of Object.entries(this.routing)) {
|
|
1001
|
-
const stats = this.
|
|
1092
|
+
const stats = this.routeAnalytics.getRouteStats(route);
|
|
1002
1093
|
routes[route] = {
|
|
1003
1094
|
providers: this.flattenPoolTargets(pools),
|
|
1004
|
-
hits: stats
|
|
1005
|
-
lastUsedProvider: stats
|
|
1095
|
+
hits: stats?.hits ?? 0,
|
|
1096
|
+
lastUsedProvider: stats?.lastProvider,
|
|
1097
|
+
...(stats?.lastHit ? { lastHit: { ...stats.lastHit } } : {})
|
|
1006
1098
|
};
|
|
1007
1099
|
}
|
|
1008
1100
|
return {
|
|
@@ -1062,22 +1154,30 @@ export class VirtualRouterEngine {
|
|
|
1062
1154
|
contextAdvisor: this.contextAdvisor,
|
|
1063
1155
|
loadBalancer: this.loadBalancer,
|
|
1064
1156
|
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1157
|
+
getProviderCooldownRemainingMs: (key) => this.getProviderCooldownRemainingMs(key),
|
|
1065
1158
|
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1066
1159
|
quotaView: this.quotaView,
|
|
1067
|
-
aliasQueueStore: this.aliasQueueStore,
|
|
1068
|
-
antigravityAliasLeaseStore: this.
|
|
1069
|
-
antigravitySessionAliasStore: this.
|
|
1070
|
-
antigravityAliasReuseCooldownMs: this.
|
|
1160
|
+
aliasQueueStore: this.stickySessionManager.getAllStores().aliasQueueStore,
|
|
1161
|
+
antigravityAliasLeaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
1162
|
+
antigravitySessionAliasStore: this.stickySessionManager.getAllStores().sessionAliasStore,
|
|
1163
|
+
antigravityAliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs()
|
|
1071
1164
|
}, { routingState });
|
|
1072
1165
|
}
|
|
1073
|
-
|
|
1074
|
-
if (!this.
|
|
1075
|
-
|
|
1076
|
-
|
|
1166
|
+
resolveSelectionPenalty(providerKey) {
|
|
1167
|
+
if (!this.quotaView) {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
const entry = this.quotaView(providerKey);
|
|
1172
|
+
const raw = entry?.selectionPenalty;
|
|
1173
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) {
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
return Math.floor(raw);
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
return undefined;
|
|
1077
1180
|
}
|
|
1078
|
-
const stats = this.routeStats.get(routeName);
|
|
1079
|
-
stats.hits += 1;
|
|
1080
|
-
stats.lastProvider = providerKey;
|
|
1081
1181
|
}
|
|
1082
1182
|
providerHealthConfig() {
|
|
1083
1183
|
return this.healthManager.getConfig();
|
|
@@ -1214,9 +1314,10 @@ export class VirtualRouterEngine {
|
|
|
1214
1314
|
contextAdvisor: this.contextAdvisor,
|
|
1215
1315
|
loadBalancer: this.loadBalancer,
|
|
1216
1316
|
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1317
|
+
getProviderCooldownRemainingMs: (key) => this.getProviderCooldownRemainingMs(key),
|
|
1217
1318
|
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1218
1319
|
quotaView: this.quotaView,
|
|
1219
|
-
aliasQueueStore: this.aliasQueueStore
|
|
1320
|
+
aliasQueueStore: this.stickySessionManager.getAllStores().aliasQueueStore
|
|
1220
1321
|
}, { routingState: state });
|
|
1221
1322
|
}
|
|
1222
1323
|
/**
|
|
@@ -1342,22 +1443,16 @@ export class VirtualRouterEngine {
|
|
|
1342
1443
|
}
|
|
1343
1444
|
// mapProviderError/applySeriesCooldown moved to engine-health.ts
|
|
1344
1445
|
extractExcludedProviderKeySet(metadata) {
|
|
1345
|
-
|
|
1346
|
-
return new Set();
|
|
1347
|
-
}
|
|
1348
|
-
const raw = metadata.excludedProviderKeys;
|
|
1349
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
1350
|
-
return new Set();
|
|
1351
|
-
}
|
|
1352
|
-
const normalized = raw
|
|
1353
|
-
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
1354
|
-
.filter((value) => Boolean(value));
|
|
1355
|
-
return new Set(normalized);
|
|
1446
|
+
return this.routeAnalytics.extractExcludedProviderKeySet(metadata);
|
|
1356
1447
|
}
|
|
1357
1448
|
buildRouteCandidates(requestedRoute, classificationCandidates, features) {
|
|
1358
1449
|
const forceVision = this.routeHasForceFlag('vision');
|
|
1359
1450
|
const hasMultimodalTargets = this.routeHasTargets(this.routing.multimodal);
|
|
1360
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
|
+
}
|
|
1361
1456
|
const normalized = this.normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
|
|
1362
1457
|
const baseList = [];
|
|
1363
1458
|
if (classificationCandidates && classificationCandidates.length) {
|
|
@@ -1374,9 +1469,9 @@ export class VirtualRouterEngine {
|
|
|
1374
1469
|
baseList.unshift('multimodal');
|
|
1375
1470
|
}
|
|
1376
1471
|
}
|
|
1377
|
-
|
|
1472
|
+
if (hasVisionTargets) {
|
|
1378
1473
|
if (!baseList.includes('vision')) {
|
|
1379
|
-
baseList.
|
|
1474
|
+
baseList.push('vision');
|
|
1380
1475
|
}
|
|
1381
1476
|
}
|
|
1382
1477
|
if (!forceVision && hasMultimodalTargets) {
|
|
@@ -1558,125 +1653,34 @@ export class VirtualRouterEngine {
|
|
|
1558
1653
|
return flattened;
|
|
1559
1654
|
}
|
|
1560
1655
|
markProviderCooldown(providerKey, cooldownMs) {
|
|
1561
|
-
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
const ttl = typeof cooldownMs === 'number' ? Math.round(cooldownMs) : Number.NaN;
|
|
1565
|
-
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
this.providerCooldowns.set(providerKey, Date.now() + ttl);
|
|
1569
|
-
this.persistHealthSnapshot();
|
|
1656
|
+
this.cooldownManager.markProviderCooldown(providerKey, cooldownMs);
|
|
1570
1657
|
}
|
|
1571
1658
|
clearProviderCooldown(providerKey) {
|
|
1572
|
-
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
if (this.providerCooldowns.delete(providerKey)) {
|
|
1576
|
-
this.persistHealthSnapshot();
|
|
1577
|
-
}
|
|
1659
|
+
this.cooldownManager.clearProviderCooldown(providerKey);
|
|
1578
1660
|
}
|
|
1579
1661
|
isProviderCoolingDown(providerKey) {
|
|
1662
|
+
return this.cooldownManager.isProviderCoolingDown(providerKey);
|
|
1663
|
+
}
|
|
1664
|
+
getProviderCooldownRemainingMs(providerKey) {
|
|
1580
1665
|
if (!providerKey) {
|
|
1581
|
-
return
|
|
1666
|
+
return 0;
|
|
1582
1667
|
}
|
|
1583
1668
|
const expiry = this.providerCooldowns.get(providerKey);
|
|
1584
|
-
if (!expiry) {
|
|
1585
|
-
return
|
|
1669
|
+
if (!expiry || !Number.isFinite(expiry)) {
|
|
1670
|
+
return 0;
|
|
1586
1671
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
return false;
|
|
1590
|
-
}
|
|
1591
|
-
return true;
|
|
1672
|
+
const remaining = Math.floor(expiry - Date.now());
|
|
1673
|
+
return remaining > 0 ? remaining : 0;
|
|
1592
1674
|
}
|
|
1593
1675
|
restoreHealthFromStore() {
|
|
1594
|
-
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
|
-
// When quotaView is enabled, health/cooldown must be driven by quotaView only.
|
|
1598
|
-
// Do not restore legacy router-local cooldown TTLs from health snapshots.
|
|
1599
|
-
if (this.quotaView) {
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
let snapshot = null;
|
|
1603
|
-
try {
|
|
1604
|
-
snapshot = this.healthStore.loadInitialSnapshot();
|
|
1605
|
-
}
|
|
1606
|
-
catch {
|
|
1607
|
-
snapshot = null;
|
|
1608
|
-
}
|
|
1609
|
-
if (!snapshot) {
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
const now = Date.now();
|
|
1613
|
-
const providerKeys = new Set();
|
|
1614
|
-
for (const pools of Object.values(this.routing)) {
|
|
1615
|
-
for (const pool of pools) {
|
|
1616
|
-
for (const key of pool.targets) {
|
|
1617
|
-
if (typeof key === 'string' && key) {
|
|
1618
|
-
providerKeys.add(key);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
const byKey = new Map();
|
|
1624
|
-
for (const entry of snapshot.cooldowns || []) {
|
|
1625
|
-
if (!entry || !entry.providerKey) {
|
|
1626
|
-
continue;
|
|
1627
|
-
}
|
|
1628
|
-
if (!providerKeys.has(entry.providerKey)) {
|
|
1629
|
-
continue;
|
|
1630
|
-
}
|
|
1631
|
-
if (!Number.isFinite(entry.cooldownExpiresAt) || entry.cooldownExpiresAt <= now) {
|
|
1632
|
-
continue;
|
|
1633
|
-
}
|
|
1634
|
-
byKey.set(entry.providerKey, entry);
|
|
1635
|
-
this.providerCooldowns.set(entry.providerKey, entry.cooldownExpiresAt);
|
|
1636
|
-
}
|
|
1637
|
-
for (const state of snapshot.providers || []) {
|
|
1638
|
-
if (!state || !state.providerKey) {
|
|
1639
|
-
continue;
|
|
1640
|
-
}
|
|
1641
|
-
if (!providerKeys.has(state.providerKey)) {
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
|
-
if (state.cooldownExpiresAt && state.cooldownExpiresAt > now) {
|
|
1645
|
-
const ttl = state.cooldownExpiresAt - now;
|
|
1646
|
-
if (ttl > 0) {
|
|
1647
|
-
this.healthManager.tripProvider(state.providerKey, state.reason, ttl);
|
|
1648
|
-
if (!byKey.has(state.providerKey)) {
|
|
1649
|
-
this.providerCooldowns.set(state.providerKey, state.cooldownExpiresAt);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1676
|
+
this.cooldownManager.restoreHealthFromStore();
|
|
1654
1677
|
}
|
|
1655
1678
|
buildHealthSnapshot() {
|
|
1656
1679
|
const providers = this.healthManager.getSnapshot();
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
for (const [providerKey, expiry] of this.providerCooldowns.entries()) {
|
|
1660
|
-
if (!expiry || expiry <= now) {
|
|
1661
|
-
continue;
|
|
1662
|
-
}
|
|
1663
|
-
cooldowns.push({
|
|
1664
|
-
providerKey,
|
|
1665
|
-
cooldownExpiresAt: expiry
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
return { providers, cooldowns };
|
|
1680
|
+
const cooldownSnapshot = this.cooldownManager.buildHealthSnapshot();
|
|
1681
|
+
return { providers, cooldowns: cooldownSnapshot.cooldowns };
|
|
1669
1682
|
}
|
|
1670
1683
|
persistHealthSnapshot() {
|
|
1671
|
-
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
try {
|
|
1675
|
-
const snapshot = this.buildHealthSnapshot();
|
|
1676
|
-
this.healthStore.persistSnapshot(snapshot);
|
|
1677
|
-
}
|
|
1678
|
-
catch {
|
|
1679
|
-
// 持久化失败不影响路由主流程
|
|
1680
|
-
}
|
|
1684
|
+
this.cooldownManager.persistHealthSnapshot();
|
|
1681
1685
|
}
|
|
1682
1686
|
}
|