@jsonstudio/llms 0.6.802 → 0.6.954
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/bridge/routecodex-adapter.d.ts +74 -0
- package/dist/config-unified/enhanced-path-resolver.d.ts +5 -0
- package/dist/config-unified/unified-config.d.ts +26 -0
- package/dist/conversion/codec-registry.d.ts +10 -0
- package/dist/conversion/codecs/gemini-openai-codec.d.ts +16 -0
- package/dist/conversion/codecs/openai-openai-codec.d.ts +12 -0
- package/dist/conversion/codecs/responses-openai-codec.d.ts +12 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +12 -0
- package/dist/conversion/config/config-manager.d.ts +212 -0
- package/dist/conversion/hub/config/types.d.ts +26 -0
- package/dist/conversion/hub/core/detour-registry.d.ts +9 -0
- package/dist/conversion/hub/core/hub-context.d.ts +21 -0
- package/dist/conversion/hub/core/index.d.ts +3 -0
- package/dist/conversion/hub/core/stage-driver.d.ts +30 -0
- package/dist/conversion/hub/format-adapters/anthropic-format-adapter.d.ts +16 -0
- package/dist/conversion/hub/format-adapters/chat-format-adapter.d.ts +17 -0
- package/dist/conversion/hub/format-adapters/gemini-format-adapter.d.ts +16 -0
- package/dist/conversion/hub/format-adapters/index.d.ts +21 -0
- package/dist/conversion/hub/hub-feature.d.ts +1 -0
- package/dist/conversion/hub/node-support.d.ts +19 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +11 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +7 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +113 -17
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +6 -3
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +4 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +23 -1
- package/dist/conversion/hub/pipelines/inbound.d.ts +22 -0
- package/dist/conversion/hub/pipelines/outbound.d.ts +22 -0
- package/dist/conversion/hub/policy/policy-engine.d.ts +46 -0
- package/dist/conversion/hub/policy/policy-engine.js +176 -0
- package/dist/conversion/hub/policy/protocol-spec.d.ts +50 -0
- package/dist/conversion/hub/policy/protocol-spec.js +105 -0
- package/dist/conversion/hub/process/chat-process.d.ts +32 -0
- package/dist/conversion/hub/registry.d.ts +28 -0
- package/dist/conversion/hub/response/chat-response-utils.d.ts +6 -0
- package/dist/conversion/hub/response/provider-response.js +31 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +32 -1
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +7 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +96 -1
- package/dist/conversion/hub/semantic-mappers/index.d.ts +4 -0
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +21 -0
- package/dist/conversion/hub/standardized-bridge.d.ts +12 -0
- package/dist/conversion/hub/types/chat-schema.d.ts +112 -0
- package/dist/conversion/hub/types/errors.d.ts +5 -0
- package/dist/conversion/hub/types/format-envelope.d.ts +7 -0
- package/dist/conversion/hub/types/index.d.ts +6 -0
- package/dist/conversion/hub/types/json.d.ts +9 -0
- package/dist/conversion/hub/types/node.d.ts +31 -0
- package/dist/conversion/responses/responses-openai-bridge.js +263 -10
- package/dist/conversion/schema-validator.d.ts +7 -0
- package/dist/conversion/shared/args-mapping.d.ts +18 -0
- package/dist/conversion/shared/chat-request-filters.d.ts +9 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/gemini-tool-utils.js +105 -1
- package/dist/conversion/shared/jsonish.d.ts +3 -0
- package/dist/conversion/shared/mcp-injection.d.ts +2 -0
- package/dist/conversion/shared/media.d.ts +1 -0
- package/dist/conversion/shared/openai-message-normalize.d.ts +1 -0
- package/dist/conversion/shared/payload-budget.d.ts +13 -0
- package/dist/conversion/shared/reasoning-mapping.d.ts +5 -0
- package/dist/conversion/shared/responses-request-adapter.d.ts +1 -28
- package/dist/conversion/shared/responses-request-adapter.js +1 -430
- package/dist/conversion/shared/snapshot-hooks.js +58 -3
- package/dist/conversion/shared/tool-governor.js +8 -2
- package/dist/conversion/shared/tool-harvester.d.ts +31 -0
- package/dist/conversion/shared/tool-mapping.js +10 -29
- package/dist/conversion/types.d.ts +33 -0
- package/dist/filters/builtin/add-fields-filter.d.ts +8 -0
- package/dist/filters/builtin/blacklist-filter.d.ts +8 -0
- package/dist/filters/builtin/whitelist-filter.d.ts +8 -0
- package/dist/filters/engine.d.ts +16 -0
- package/dist/filters/special/request-tool-choice-policy.d.ts +11 -0
- package/dist/filters/special/response-finish-invariants.d.ts +11 -0
- package/dist/filters/special/response-openai-to-responses-bridge.d.ts +13 -0
- package/dist/filters/special/response-tool-arguments-blacklist.d.ts +12 -0
- package/dist/filters/special/response-tool-arguments-schema-converge.d.ts +13 -0
- package/dist/filters/special/response-tool-arguments-stringify.d.ts +9 -0
- package/dist/filters/special/response-tool-arguments-whitelist.d.ts +11 -0
- package/dist/filters/special/tool-filter-hooks.d.ts +19 -0
- package/dist/filters/special/tool-post-constraints.d.ts +31 -0
- package/dist/filters/types.d.ts +68 -0
- package/dist/filters/utils/fieldmap-loader.d.ts +2 -0
- package/dist/filters/utils/snapshot-writer.d.ts +10 -0
- package/dist/guidance/index.d.ts +3 -0
- package/dist/guidance/index.js +78 -83
- package/dist/http/sse-response.d.ts +22 -0
- package/dist/router/virtual-router/bootstrap.d.ts +6 -0
- package/dist/router/virtual-router/bootstrap.js +49 -5
- package/dist/router/virtual-router/classifier.d.ts +10 -0
- package/dist/router/virtual-router/engine-selection.js +98 -11
- package/dist/router/virtual-router/engine.js +177 -31
- package/dist/router/virtual-router/error-center.d.ts +10 -0
- package/dist/router/virtual-router/features.d.ts +3 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +23 -1
- package/dist/router/virtual-router/routing-instructions.js +120 -30
- package/dist/router/virtual-router/types.d.ts +11 -0
- package/dist/servertool/engine.js +192 -17
- package/dist/servertool/handlers/apply-patch-guard.js +269 -0
- package/dist/servertool/handlers/exec-command-guard.js +558 -0
- package/dist/servertool/handlers/followup-message-trimmer.d.ts +16 -0
- package/dist/servertool/handlers/followup-message-trimmer.js +198 -0
- package/dist/servertool/handlers/followup-request-builder.d.ts +17 -0
- package/dist/servertool/handlers/followup-request-builder.js +122 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +252 -51
- package/dist/servertool/handlers/iflow-model-error-retry.js +12 -22
- package/dist/servertool/handlers/stop-message-auto.js +237 -75
- package/dist/servertool/handlers/vision.js +15 -27
- package/dist/servertool/handlers/web-search.js +17 -43
- package/dist/servertool/server-side-tools.d.ts +3 -0
- package/dist/servertool/server-side-tools.js +3 -0
- package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +2 -1
- package/dist/sse/json-to-sse/chat-json-to-sse-converter.d.ts +80 -0
- package/dist/sse/json-to-sse/event-generators/chat.d.ts +55 -0
- package/dist/sse/json-to-sse/event-generators/responses.d.ts +99 -0
- package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +2 -1
- package/dist/sse/json-to-sse/responses-json-to-sse-converter.d.ts +80 -0
- package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +1 -1
- package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +2 -2
- package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +1 -1
- package/dist/sse/json-to-sse/sequencers/responses-sequencer.d.ts +40 -0
- package/dist/sse/shared/chat-serializer.d.ts +4 -0
- package/dist/sse/shared/constants.d.ts +272 -0
- package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +1 -1
- package/dist/sse/shared/serializers/base-serializer.d.ts +158 -0
- package/dist/sse/shared/serializers/chat-event-serializer.d.ts +82 -0
- package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +1 -1
- package/dist/sse/shared/serializers/index.d.ts +2 -1
- package/dist/sse/shared/serializers/responses-event-serializer.d.ts +123 -0
- package/dist/sse/shared/serializers/types.d.ts +51 -0
- package/dist/sse/shared/utils.d.ts +254 -0
- package/dist/sse/shared/writer.d.ts +2 -2
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -1
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +1 -1
- package/dist/sse/sse-to-json/builders/response-builder.d.ts +1 -1
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +2 -1
- package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +2 -1
- package/dist/sse/sse-to-json/parsers/sse-parser.d.ts +73 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
- package/dist/sse/types/chat-types.d.ts +1 -1
- package/dist/sse/types/responses-types.d.ts +1 -1
- package/dist/tools/apply-patch/execution-capturer.d.ts +13 -0
- package/dist/tools/apply-patch/execution-capturer.js +158 -0
- package/dist/tools/apply-patch/regression-capturer.d.ts +1 -0
- package/dist/tools/apply-patch/regression-capturer.js +5 -4
- package/dist/tools/apply-patch/structured.js +109 -13
- package/dist/tools/apply-patch/validator.js +112 -18
- package/dist/tools/tool-registry.d.ts +8 -0
- package/dist/tools/tool-registry.js +2 -1
- package/package.json +4 -4
- package/dist/conversion/compat/actions/apply-patch-format-fixer.js +0 -233
- package/dist/conversion/config/compat-profiles.json +0 -38
- package/dist/conversion/hub/pipeline/context-limit.d.ts +0 -13
- package/dist/conversion/hub/pipeline/context-limit.js +0 -55
- package/dist/conversion/hub/response/server-side-tools.d.ts +0 -26
- package/dist/conversion/hub/response/server-side-tools.js +0 -383
- package/dist/conversion/shared/bridge-conversation-store.d.ts +0 -41
- package/dist/conversion/shared/bridge-conversation-store.js +0 -279
- package/dist/conversion/shared/bridge-request-adapter.d.ts +0 -28
- package/dist/conversion/shared/bridge-request-adapter.js +0 -430
- package/dist/conversion/shared/responses-id-utils.js +0 -42
- package/dist/conversion/shared/responses-instructions.js +0 -113
- package/dist/conversion/shared/responses-message-utils.d.ts +0 -15
- package/dist/conversion/shared/responses-message-utils.js +0 -206
- package/dist/conversion/shared/responses-metadata.js +0 -1
- package/dist/conversion/shared/responses-output-utils.d.ts +0 -7
- package/dist/conversion/shared/responses-output-utils.js +0 -108
- package/dist/conversion/shared/responses-types.d.ts +0 -33
- package/dist/conversion/shared/tool-normalizers.d.ts +0 -4
- package/dist/conversion/shared/tool-normalizers.js +0 -84
- package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +0 -13
- package/dist/filters/special/request-streaming-to-nonstreaming.js +0 -39
- package/dist/filters/special/response-apply-patch-toon-decode.d.ts +0 -23
- package/dist/filters/special/response-apply-patch-toon-decode.js +0 -460
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +0 -10
- package/dist/filters/special/response-tool-arguments-toon-decode.js +0 -154
- package/dist/servertool/flow-types.d.ts +0 -40
- package/dist/servertool/flow-types.js +0 -1
- package/dist/servertool/orchestration-types.d.ts +0 -33
- package/dist/servertool/orchestration-types.js +0 -1
- package/dist/servertool/vision-tool.d.ts +0 -2
- package/dist/servertool/vision-tool.js +0 -185
- package/dist/tools/patch-args-normalizer.d.ts +0 -15
- package/dist/tools/patch-args-normalizer.js +0 -472
- package/dist/utils/toon.d.ts +0 -4
- package/dist/utils/toon.js +0 -75
- /package/dist/{conversion/compat/actions/apply-patch-format-fixer.d.ts → servertool/handlers/apply-patch-guard.d.ts} +0 -0
- /package/dist/{conversion/shared/responses-types.js → servertool/handlers/exec-command-guard.d.ts} +0 -0
|
@@ -76,13 +76,41 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
if (forcedKeySet.size > 0) {
|
|
79
|
-
const candidates = buildRouteCandidates(requestedRoute, classification.candidates, features, deps.routing, deps.providerRegistry);
|
|
79
|
+
const candidates = extendRouteCandidatesForState(buildRouteCandidates(requestedRoute, classification.candidates, features, deps.routing, deps.providerRegistry), state, deps.routing);
|
|
80
80
|
const filteredCandidates = filterCandidatesByRoutingState(candidates, state, deps.routing, deps.providerRegistry);
|
|
81
81
|
if (filteredCandidates.length === 0) {
|
|
82
|
-
|
|
82
|
+
const allowedProviders = Array.from(state.allowedProviders);
|
|
83
|
+
const disabledProviders = Array.from(state.disabledProviders);
|
|
84
|
+
const providersInRouting = new Set();
|
|
85
|
+
for (const pools of Object.values(deps.routing)) {
|
|
86
|
+
if (!Array.isArray(pools))
|
|
87
|
+
continue;
|
|
88
|
+
for (const pool of pools) {
|
|
89
|
+
if (!pool || !Array.isArray(pool.targets))
|
|
90
|
+
continue;
|
|
91
|
+
for (const key of pool.targets) {
|
|
92
|
+
if (typeof key !== 'string' || !key)
|
|
93
|
+
continue;
|
|
94
|
+
const providerId = extractProviderId(key);
|
|
95
|
+
if (providerId) {
|
|
96
|
+
providersInRouting.add(providerId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const missingAllowedProviders = allowedProviders.length > 0 ? allowedProviders.filter((provider) => !providersInRouting.has(provider)) : [];
|
|
102
|
+
const hint = (() => {
|
|
103
|
+
if (missingAllowedProviders.length > 0) {
|
|
104
|
+
return `Allowed providers not present in routing pools: ${missingAllowedProviders.join(', ')}`;
|
|
105
|
+
}
|
|
106
|
+
return 'Routing instructions excluded all route candidates';
|
|
107
|
+
})();
|
|
108
|
+
throw new VirtualRouterError(`No available providers after applying routing instructions (${hint}). ` +
|
|
109
|
+
`Tip: remove/adjust <**...**> routing instructions (or use <**clear**>), or add providers/models to routing.`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, {
|
|
83
110
|
requestedRoute,
|
|
84
|
-
allowedProviders
|
|
85
|
-
disabledProviders
|
|
111
|
+
allowedProviders,
|
|
112
|
+
disabledProviders,
|
|
113
|
+
missingAllowedProviders
|
|
86
114
|
});
|
|
87
115
|
}
|
|
88
116
|
return selectFromCandidates(filteredCandidates, metadata, classification, features, state, deps, {
|
|
@@ -98,18 +126,65 @@ export function selectProviderImpl(requestedRoute, metadata, classification, fea
|
|
|
98
126
|
}
|
|
99
127
|
}
|
|
100
128
|
const candidates = buildRouteCandidates(requestedRoute, classification.candidates, features, deps.routing, deps.providerRegistry);
|
|
101
|
-
const
|
|
129
|
+
const expandedCandidates = extendRouteCandidatesForState(candidates, state, deps.routing);
|
|
130
|
+
const filteredCandidates = filterCandidatesByRoutingState(expandedCandidates, state, deps.routing, deps.providerRegistry);
|
|
102
131
|
if (filteredCandidates.length === 0) {
|
|
103
|
-
|
|
132
|
+
const allowedProviders = Array.from(state.allowedProviders);
|
|
133
|
+
const disabledProviders = Array.from(state.disabledProviders);
|
|
134
|
+
const providersInRouting = new Set();
|
|
135
|
+
for (const pools of Object.values(deps.routing)) {
|
|
136
|
+
if (!Array.isArray(pools))
|
|
137
|
+
continue;
|
|
138
|
+
for (const pool of pools) {
|
|
139
|
+
if (!pool || !Array.isArray(pool.targets))
|
|
140
|
+
continue;
|
|
141
|
+
for (const key of pool.targets) {
|
|
142
|
+
if (typeof key !== 'string' || !key)
|
|
143
|
+
continue;
|
|
144
|
+
const providerId = extractProviderId(key);
|
|
145
|
+
if (providerId) {
|
|
146
|
+
providersInRouting.add(providerId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const missingAllowedProviders = allowedProviders.length > 0 ? allowedProviders.filter((provider) => !providersInRouting.has(provider)) : [];
|
|
152
|
+
const hint = (() => {
|
|
153
|
+
if (missingAllowedProviders.length > 0) {
|
|
154
|
+
return `Allowed providers not present in routing pools: ${missingAllowedProviders.join(', ')}`;
|
|
155
|
+
}
|
|
156
|
+
return 'Routing instructions excluded all route candidates';
|
|
157
|
+
})();
|
|
158
|
+
throw new VirtualRouterError(`No available providers after applying routing instructions (${hint}). ` +
|
|
159
|
+
`Tip: remove/adjust <**...**> routing instructions (or use <**clear**>), or add providers/models to routing.`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, {
|
|
104
160
|
requestedRoute,
|
|
105
|
-
allowedProviders
|
|
106
|
-
disabledProviders
|
|
161
|
+
allowedProviders,
|
|
162
|
+
disabledProviders,
|
|
163
|
+
missingAllowedProviders
|
|
107
164
|
});
|
|
108
165
|
}
|
|
109
166
|
return selectFromCandidates(filteredCandidates, metadata, classification, features, state, deps, {
|
|
110
167
|
allowAliasRotation
|
|
111
168
|
});
|
|
112
169
|
}
|
|
170
|
+
function extendRouteCandidatesForState(candidates, state, routing) {
|
|
171
|
+
// When provider allowlists are active (e.g. "<**!glm**>"), routing should not be bounded by
|
|
172
|
+
// classifier candidates only. Otherwise, a perfectly valid provider that exists in config
|
|
173
|
+
// (e.g. in a backup/default pool) can become unreachable and cause PROVIDER_NOT_AVAILABLE.
|
|
174
|
+
//
|
|
175
|
+
// We keep original ordering, then append all known routes (by priority) as a fallback search space.
|
|
176
|
+
if (!state.allowedProviders || state.allowedProviders.size === 0) {
|
|
177
|
+
return candidates;
|
|
178
|
+
}
|
|
179
|
+
const allRoutes = sortByPriority(Object.keys(routing).filter((routeName) => routeName && routeHasTargets(routing[routeName])));
|
|
180
|
+
const expanded = Array.isArray(candidates) ? [...candidates] : [];
|
|
181
|
+
for (const routeName of allRoutes) {
|
|
182
|
+
if (!expanded.includes(routeName)) {
|
|
183
|
+
expanded.push(routeName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return expanded;
|
|
187
|
+
}
|
|
113
188
|
export function selectDirectProviderModel(providerId, modelId, metadata, features, activeState, deps) {
|
|
114
189
|
const normalizedProvider = typeof providerId === 'string' ? providerId.trim() : '';
|
|
115
190
|
const normalizedModel = typeof modelId === 'string' ? modelId.trim() : '';
|
|
@@ -822,9 +897,21 @@ function resolveInstructionTarget(target, providerRegistry) {
|
|
|
822
897
|
const alias = typeof target.keyAlias === 'string' ? target.keyAlias.trim() : '';
|
|
823
898
|
const aliasExplicit = alias.length > 0 && target.pathLength === 3;
|
|
824
899
|
if (aliasExplicit) {
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
900
|
+
const prefix = `${providerId}.${alias}.`;
|
|
901
|
+
const aliasKeys = providerKeys.filter((key) => key.startsWith(prefix));
|
|
902
|
+
if (aliasKeys.length > 0) {
|
|
903
|
+
if (target.model && target.model.trim()) {
|
|
904
|
+
const normalizedModel = target.model.trim();
|
|
905
|
+
const matching = aliasKeys.filter((key) => getProviderModelId(key, providerRegistry) === normalizedModel);
|
|
906
|
+
if (matching.length > 0) {
|
|
907
|
+
// Prefer exact to keep sticky pool deterministic when only one key matches.
|
|
908
|
+
if (matching.length === 1) {
|
|
909
|
+
return { mode: 'exact', keys: [matching[0]] };
|
|
910
|
+
}
|
|
911
|
+
return { mode: 'filter', keys: matching };
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return { mode: 'filter', keys: aliasKeys };
|
|
828
915
|
}
|
|
829
916
|
}
|
|
830
917
|
if (typeof target.keyIndex === 'number' && target.keyIndex > 0) {
|
|
@@ -9,7 +9,7 @@ import { getStatsCenter } from '../../telemetry/stats-center.js';
|
|
|
9
9
|
import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRoutingInstructions } from './routing-instructions.js';
|
|
10
10
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from './sticky-session-store.js';
|
|
11
11
|
import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
|
|
12
|
-
import { selectDirectProviderModel, selectProviderImpl } from './engine-selection.js';
|
|
12
|
+
import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine-selection.js';
|
|
13
13
|
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
|
|
14
14
|
export class VirtualRouterEngine {
|
|
15
15
|
routing = {};
|
|
@@ -105,7 +105,11 @@ export class VirtualRouterEngine {
|
|
|
105
105
|
}
|
|
106
106
|
route(request, metadata) {
|
|
107
107
|
const stickyKey = this.resolveStickyKey(metadata);
|
|
108
|
-
const
|
|
108
|
+
const sessionScope = this.resolveSessionScope(metadata);
|
|
109
|
+
// Routing instructions should be session/conversation-scoped when available (including /v1/responses),
|
|
110
|
+
// while auto-sticky for Responses remains request-chain scoped via resolveStickyKey().
|
|
111
|
+
const stateKey = sessionScope || stickyKey || 'default';
|
|
112
|
+
const baseState = this.getRoutingInstructionState(stateKey);
|
|
109
113
|
let routingState = baseState;
|
|
110
114
|
const metadataInstructions = this.buildMetadataInstructions(metadata);
|
|
111
115
|
if (metadataInstructions.length > 0) {
|
|
@@ -114,13 +118,13 @@ export class VirtualRouterEngine {
|
|
|
114
118
|
const disableStickyRoutes = metadata &&
|
|
115
119
|
typeof metadata === 'object' &&
|
|
116
120
|
metadata.disableStickyRoutes === true;
|
|
117
|
-
if (disableStickyRoutes && routingState.stickyTarget) {
|
|
121
|
+
if (disableStickyRoutes && (routingState.stickyTarget || routingState.preferTarget)) {
|
|
118
122
|
routingState = {
|
|
119
123
|
...routingState,
|
|
120
|
-
stickyTarget: undefined
|
|
124
|
+
stickyTarget: undefined,
|
|
125
|
+
preferTarget: undefined
|
|
121
126
|
};
|
|
122
127
|
}
|
|
123
|
-
const sessionScope = this.resolveSessionScope(metadata);
|
|
124
128
|
if (sessionScope) {
|
|
125
129
|
const sessionState = this.getRoutingInstructionState(sessionScope);
|
|
126
130
|
if (typeof sessionState.stopMessageText === 'string' ||
|
|
@@ -163,9 +167,8 @@ export class VirtualRouterEngine {
|
|
|
163
167
|
}
|
|
164
168
|
if (instructions.length > 0) {
|
|
165
169
|
routingState = applyRoutingInstructions(instructions, routingState);
|
|
166
|
-
|
|
167
|
-
this.
|
|
168
|
-
this.persistRoutingInstructionState(effectiveKey, routingState);
|
|
170
|
+
this.routingInstructionState.set(stateKey, routingState);
|
|
171
|
+
this.persistRoutingInstructionState(stateKey, routingState);
|
|
169
172
|
// 对 stopMessage 指令补充一份基于 session/conversation 的持久化状态,
|
|
170
173
|
// 便于 server-side 工具通过 session:*/conversation:* scope 读取到相同配置。
|
|
171
174
|
if (sessionScope) {
|
|
@@ -236,12 +239,52 @@ export class VirtualRouterEngine {
|
|
|
236
239
|
routingState.stopMessageLastUsedAt = sessionState.stopMessageLastUsedAt;
|
|
237
240
|
}
|
|
238
241
|
}
|
|
239
|
-
|
|
242
|
+
// Guardrail: if a session is restricted to providers that do not exist in any routing pools,
|
|
243
|
+
// we must not hard-fail the request loop. Auto-clear the allowlist and fall back to normal routing.
|
|
244
|
+
if (routingState.allowedProviders.size > 0) {
|
|
245
|
+
const providersInRouting = new Set();
|
|
246
|
+
for (const pools of Object.values(this.routing)) {
|
|
247
|
+
if (!Array.isArray(pools))
|
|
248
|
+
continue;
|
|
249
|
+
for (const pool of pools) {
|
|
250
|
+
if (!pool || !Array.isArray(pool.targets))
|
|
251
|
+
continue;
|
|
252
|
+
for (const key of pool.targets) {
|
|
253
|
+
if (typeof key !== 'string' || !key)
|
|
254
|
+
continue;
|
|
255
|
+
const providerId = this.extractProviderId(key);
|
|
256
|
+
if (providerId) {
|
|
257
|
+
providersInRouting.add(providerId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const allowed = Array.from(routingState.allowedProviders);
|
|
263
|
+
const hasIntersection = allowed.some((provider) => providersInRouting.has(provider));
|
|
264
|
+
if (!hasIntersection) {
|
|
265
|
+
routingState = {
|
|
266
|
+
...routingState,
|
|
267
|
+
allowedProviders: new Set()
|
|
268
|
+
};
|
|
269
|
+
this.routingInstructionState.set(stateKey, routingState);
|
|
270
|
+
this.persistRoutingInstructionState(stateKey, routingState);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
240
273
|
const features = buildRoutingFeatures(request, metadata);
|
|
241
274
|
const directProviderModel = this.parseDirectProviderModel(request?.model);
|
|
242
275
|
let classification;
|
|
243
276
|
let requestedRoute;
|
|
244
277
|
let selection;
|
|
278
|
+
const selectionDeps = {
|
|
279
|
+
routing: this.routing,
|
|
280
|
+
providerRegistry: this.providerRegistry,
|
|
281
|
+
healthManager: this.healthManager,
|
|
282
|
+
contextAdvisor: this.contextAdvisor,
|
|
283
|
+
loadBalancer: this.loadBalancer,
|
|
284
|
+
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
285
|
+
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
286
|
+
quotaView: this.quotaView
|
|
287
|
+
};
|
|
245
288
|
if (directProviderModel) {
|
|
246
289
|
const providerKeys = this.providerRegistry.listProviderKeys(directProviderModel.providerId);
|
|
247
290
|
let hasModel = false;
|
|
@@ -268,33 +311,129 @@ export class VirtualRouterEngine {
|
|
|
268
311
|
candidates: ['direct']
|
|
269
312
|
};
|
|
270
313
|
requestedRoute = 'direct';
|
|
271
|
-
const directSelection = selectDirectProviderModel(directProviderModel.providerId, directProviderModel.modelId, metadata, features, routingState,
|
|
272
|
-
routing: this.routing,
|
|
273
|
-
providerRegistry: this.providerRegistry,
|
|
274
|
-
healthManager: this.healthManager,
|
|
275
|
-
contextAdvisor: this.contextAdvisor,
|
|
276
|
-
loadBalancer: this.loadBalancer,
|
|
277
|
-
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
278
|
-
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
279
|
-
quotaView: this.quotaView
|
|
280
|
-
});
|
|
314
|
+
const directSelection = selectDirectProviderModel(directProviderModel.providerId, directProviderModel.modelId, metadata, features, routingState, selectionDeps);
|
|
281
315
|
if (!directSelection) {
|
|
282
316
|
throw new VirtualRouterError(`All providers unavailable for model ${directProviderModel.providerId}.${directProviderModel.modelId}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
|
|
283
317
|
}
|
|
284
318
|
selection = directSelection;
|
|
285
319
|
}
|
|
286
320
|
else {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
321
|
+
// Prefer target (from "<**!provider.model**>") is evaluated before routing classification.
|
|
322
|
+
const preferTarget = routingState.preferTarget;
|
|
323
|
+
if (preferTarget && typeof preferTarget.provider === 'string' && preferTarget.provider.trim()) {
|
|
324
|
+
const providerId = preferTarget.provider.trim();
|
|
325
|
+
const keyAlias = typeof preferTarget.keyAlias === 'string' ? preferTarget.keyAlias.trim() : '';
|
|
326
|
+
const modelId = typeof preferTarget.model === 'string' ? preferTarget.model.trim() : '';
|
|
327
|
+
const keyIndex = typeof preferTarget.keyIndex === 'number' && Number.isFinite(preferTarget.keyIndex)
|
|
328
|
+
? Math.floor(preferTarget.keyIndex)
|
|
329
|
+
: undefined;
|
|
330
|
+
const candidateKeys = [];
|
|
331
|
+
if (keyIndex !== undefined && keyIndex > 0) {
|
|
332
|
+
const runtimeKey = this.providerRegistry.resolveRuntimeKeyByIndex(providerId, keyIndex);
|
|
333
|
+
if (runtimeKey) {
|
|
334
|
+
candidateKeys.push(runtimeKey);
|
|
335
|
+
}
|
|
294
336
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
337
|
+
else if (modelId) {
|
|
338
|
+
const allKeys = this.providerRegistry.listProviderKeys(providerId);
|
|
339
|
+
for (const key of allKeys) {
|
|
340
|
+
if (keyAlias) {
|
|
341
|
+
const prefix = `${providerId}.${keyAlias}.`;
|
|
342
|
+
if (!key.startsWith(prefix)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const profile = this.providerRegistry.get(key);
|
|
348
|
+
if (profile?.modelId === modelId) {
|
|
349
|
+
candidateKeys.push(key);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const allowAliasRotation = !keyAlias && keyIndex === undefined;
|
|
358
|
+
const eligibleKeys = (() => {
|
|
359
|
+
if (candidateKeys.length === 0) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
const quotaView = selectionDeps.quotaView;
|
|
363
|
+
const now = quotaView ? Date.now() : 0;
|
|
364
|
+
return candidateKeys.filter((key) => {
|
|
365
|
+
if (this.isProviderCoolingDown(key)) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (!this.healthManager.isAvailable(key)) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
if (!quotaView) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const entry = quotaView(key);
|
|
375
|
+
if (!entry) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (!entry.inPool) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
if (entry.cooldownUntil && entry.cooldownUntil > now) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (entry.blacklistUntil && entry.blacklistUntil > now) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
});
|
|
389
|
+
})();
|
|
390
|
+
const preferSelection = eligibleKeys.length > 0
|
|
391
|
+
? selectFromStickyPool(new Set(eligibleKeys), metadata, features, routingState, selectionDeps, {
|
|
392
|
+
allowAliasRotation
|
|
393
|
+
})
|
|
394
|
+
: null;
|
|
395
|
+
if (preferSelection) {
|
|
396
|
+
classification = {
|
|
397
|
+
routeName: 'prefer',
|
|
398
|
+
confidence: 1,
|
|
399
|
+
reasoning: keyIndex !== undefined ? `prefer_key:${providerId}.${keyIndex}` : `prefer_model:${providerId}.${modelId}`,
|
|
400
|
+
fallback: false,
|
|
401
|
+
candidates: ['prefer']
|
|
402
|
+
};
|
|
403
|
+
requestedRoute = 'prefer';
|
|
404
|
+
selection = {
|
|
405
|
+
...preferSelection,
|
|
406
|
+
routeUsed: 'prefer',
|
|
407
|
+
poolId: 'prefer-primary'
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
else if (routingState.preferTarget) {
|
|
411
|
+
// Auto-clear only when the target becomes invalid or blocked by explicit routing instructions.
|
|
412
|
+
// Do NOT clear for temporary unavailability (e.g. 429 cooldown, quota cooldown, transient health).
|
|
413
|
+
const shouldAutoClear = candidateKeys.length === 0 || eligibleKeys.length > 0;
|
|
414
|
+
if (shouldAutoClear) {
|
|
415
|
+
routingState = {
|
|
416
|
+
...routingState,
|
|
417
|
+
preferTarget: undefined
|
|
418
|
+
};
|
|
419
|
+
this.routingInstructionState.set(stateKey, routingState);
|
|
420
|
+
this.persistRoutingInstructionState(stateKey, routingState);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!selection) {
|
|
425
|
+
classification = metadata.routeHint && metadata.routeHint.trim()
|
|
426
|
+
? {
|
|
427
|
+
routeName: metadata.routeHint.trim(),
|
|
428
|
+
confidence: 1,
|
|
429
|
+
reasoning: `route_hint:${metadata.routeHint.trim()}`,
|
|
430
|
+
fallback: false,
|
|
431
|
+
candidates: [metadata.routeHint.trim()]
|
|
432
|
+
}
|
|
433
|
+
: this.classifier.classify(features);
|
|
434
|
+
requestedRoute = this.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
|
|
435
|
+
selection = this.selectProvider(requestedRoute, metadata, classification, features, routingState);
|
|
436
|
+
}
|
|
298
437
|
}
|
|
299
438
|
const baseTarget = this.providerRegistry.buildTarget(selection.providerKey);
|
|
300
439
|
const forceVision = this.routeHasForceFlag('vision');
|
|
@@ -304,6 +443,7 @@ export class VirtualRouterEngine {
|
|
|
304
443
|
...(forceVision ? { forceVision: true } : {})
|
|
305
444
|
};
|
|
306
445
|
this.incrementRouteStat(selection.routeUsed, selection.providerKey);
|
|
446
|
+
const routingMode = this.resolveRoutingMode([...metadataInstructions, ...instructions], routingState);
|
|
307
447
|
try {
|
|
308
448
|
this.statsCenter.recordVirtualRouterHit({
|
|
309
449
|
requestId: metadata.requestId,
|
|
@@ -577,7 +717,7 @@ export class VirtualRouterEngine {
|
|
|
577
717
|
// 能在 VirtualRouter 日志中实时反映出来。
|
|
578
718
|
if (existing && (key.startsWith('session:') || key.startsWith('conversation:'))) {
|
|
579
719
|
try {
|
|
580
|
-
const persisted =
|
|
720
|
+
const persisted = this.routingStateStore.loadSync(key);
|
|
581
721
|
if (persisted) {
|
|
582
722
|
// 以持久化状态为准(包括清空后的 undefined),避免 stopMessage 状态“卡死”在内存中。
|
|
583
723
|
existing.stopMessageText = persisted.stopMessageText;
|
|
@@ -603,7 +743,7 @@ export class VirtualRouterEngine {
|
|
|
603
743
|
let initial = null;
|
|
604
744
|
// 仅对 session:/conversation: 作用域的 key 尝试从磁盘恢复持久化状态
|
|
605
745
|
if (key.startsWith('session:') || key.startsWith('conversation:')) {
|
|
606
|
-
initial =
|
|
746
|
+
initial = this.routingStateStore.loadSync(key);
|
|
607
747
|
}
|
|
608
748
|
if (!initial) {
|
|
609
749
|
initial = {
|
|
@@ -661,6 +801,7 @@ export class VirtualRouterEngine {
|
|
|
661
801
|
const hasForce = instructions.some((inst) => inst.type === 'force');
|
|
662
802
|
const hasAllow = instructions.some((inst) => inst.type === 'allow');
|
|
663
803
|
const hasClear = instructions.some((inst) => inst.type === 'clear');
|
|
804
|
+
const hasPrefer = instructions.some((inst) => inst.type === 'prefer');
|
|
664
805
|
if (hasClear) {
|
|
665
806
|
return 'none';
|
|
666
807
|
}
|
|
@@ -670,6 +811,9 @@ export class VirtualRouterEngine {
|
|
|
670
811
|
if (hasForce || state.forcedTarget) {
|
|
671
812
|
return 'force';
|
|
672
813
|
}
|
|
814
|
+
if (hasPrefer || state.preferTarget) {
|
|
815
|
+
return 'sticky';
|
|
816
|
+
}
|
|
673
817
|
if (state.stickyTarget) {
|
|
674
818
|
return 'sticky';
|
|
675
819
|
}
|
|
@@ -1114,6 +1258,7 @@ export class VirtualRouterEngine {
|
|
|
1114
1258
|
}
|
|
1115
1259
|
const noForced = !state.forcedTarget;
|
|
1116
1260
|
const noSticky = !state.stickyTarget;
|
|
1261
|
+
const noPrefer = !state.preferTarget;
|
|
1117
1262
|
const noAllowed = state.allowedProviders.size === 0;
|
|
1118
1263
|
const noDisabledProviders = state.disabledProviders.size === 0;
|
|
1119
1264
|
const noDisabledKeys = state.disabledKeys.size === 0;
|
|
@@ -1125,6 +1270,7 @@ export class VirtualRouterEngine {
|
|
|
1125
1270
|
(typeof state.stopMessageLastUsedAt !== 'number' || !Number.isFinite(state.stopMessageLastUsedAt));
|
|
1126
1271
|
return (noForced &&
|
|
1127
1272
|
noSticky &&
|
|
1273
|
+
noPrefer &&
|
|
1128
1274
|
noAllowed &&
|
|
1129
1275
|
noDisabledProviders &&
|
|
1130
1276
|
noDisabledKeys &&
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProviderErrorEvent } from './types.js';
|
|
2
|
+
type ProviderErrorListener = (event: ProviderErrorEvent) => void;
|
|
3
|
+
export declare class ProviderErrorCenter {
|
|
4
|
+
private readonly listeners;
|
|
5
|
+
subscribe(listener: ProviderErrorListener): () => void;
|
|
6
|
+
emit(event: ProviderErrorEvent): ProviderErrorEvent;
|
|
7
|
+
private normalize;
|
|
8
|
+
}
|
|
9
|
+
export declare const providerErrorCenter: ProviderErrorCenter;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProcessedRequest, StandardizedRequest } from '../../conversion/hub/types/standardized.js';
|
|
2
|
+
import type { RouterMetadataInput, RoutingFeatures } from './types.js';
|
|
3
|
+
export declare function buildRoutingFeatures(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): RoutingFeatures;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StandardizedMessage } from '../../conversion/hub/types/standardized.js';
|
|
2
2
|
export interface RoutingInstruction {
|
|
3
|
-
type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow' | 'stopMessageSet' | 'stopMessageClear';
|
|
3
|
+
type: 'force' | 'sticky' | 'prefer' | 'disable' | 'enable' | 'clear' | 'allow' | 'stopMessageSet' | 'stopMessageClear';
|
|
4
4
|
provider?: string;
|
|
5
5
|
keyAlias?: string;
|
|
6
6
|
keyIndex?: number;
|
|
@@ -24,6 +24,13 @@ export interface RoutingInstructionState {
|
|
|
24
24
|
model?: string;
|
|
25
25
|
pathLength?: number;
|
|
26
26
|
};
|
|
27
|
+
preferTarget?: {
|
|
28
|
+
provider?: string;
|
|
29
|
+
keyAlias?: string;
|
|
30
|
+
keyIndex?: number;
|
|
31
|
+
model?: string;
|
|
32
|
+
pathLength?: number;
|
|
33
|
+
};
|
|
27
34
|
allowedProviders: Set<string>;
|
|
28
35
|
disabledProviders: Set<string>;
|
|
29
36
|
disabledKeys: Map<string, Set<string | number>>;
|
|
@@ -41,6 +48,21 @@ export interface RoutingInstructionState {
|
|
|
41
48
|
stopMessageLastUsedAt?: number;
|
|
42
49
|
}
|
|
43
50
|
export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
|
|
51
|
+
/**
|
|
52
|
+
* 解析并预处理路由指令,优先处理 clear 指令,确保新指令能够覆盖旧状态。
|
|
53
|
+
* 返回清理后的指令列表,移除冗余的 stopMessageSet 指令。
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseAndPreprocessRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
|
|
56
|
+
/**
|
|
57
|
+
* 提取 clear 指令(如果存在)。用于在路由选择前优先执行清理操作。
|
|
58
|
+
* @returns 是否存在 clear 指令
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractClearInstruction(messages: StandardizedMessage[]): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* 提取 stopMessageClear 指令(如果存在)。
|
|
63
|
+
* @returns 是否存在 stopMessageClear 指令
|
|
64
|
+
*/
|
|
65
|
+
export declare function extractStopMessageClearInstruction(messages: StandardizedMessage[]): boolean;
|
|
44
66
|
export declare function applyRoutingInstructions(instructions: RoutingInstruction[], currentState: RoutingInstructionState): RoutingInstructionState;
|
|
45
67
|
export declare function cleanMessagesFromRoutingInstructions(messages: StandardizedMessage[]): StandardizedMessage[];
|
|
46
68
|
export declare function serializeRoutingInstructionState(state: RoutingInstructionState): Record<string, unknown>;
|