@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
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProviderRegistry } from '../../provider-registry.js';
|
|
2
|
+
export declare function extractProviderId(providerKey: string): string | null;
|
|
3
|
+
export declare function normalizeAliasDescriptor(alias: string): string;
|
|
4
|
+
export declare function extractKeyAlias(providerKey: string): string | null;
|
|
5
|
+
export declare function extractKeyIndex(providerKey: string): number | undefined;
|
|
6
|
+
export declare function getProviderModelId(providerKey: string, providerRegistry: ProviderRegistry): string | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function extractProviderId(providerKey) {
|
|
2
|
+
const firstDot = providerKey.indexOf('.');
|
|
3
|
+
if (firstDot <= 0)
|
|
4
|
+
return null;
|
|
5
|
+
return providerKey.substring(0, firstDot);
|
|
6
|
+
}
|
|
7
|
+
export function normalizeAliasDescriptor(alias) {
|
|
8
|
+
if (/^\d+-/.test(alias)) {
|
|
9
|
+
return alias.replace(/^\d+-/, '');
|
|
10
|
+
}
|
|
11
|
+
return alias;
|
|
12
|
+
}
|
|
13
|
+
export function extractKeyAlias(providerKey) {
|
|
14
|
+
const parts = providerKey.split('.');
|
|
15
|
+
if (parts.length === 3) {
|
|
16
|
+
return normalizeAliasDescriptor(parts[1]);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function extractKeyIndex(providerKey) {
|
|
21
|
+
const parts = providerKey.split('.');
|
|
22
|
+
if (parts.length === 2) {
|
|
23
|
+
const index = parseInt(parts[1], 10);
|
|
24
|
+
if (!isNaN(index) && index > 0) {
|
|
25
|
+
return index;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export function getProviderModelId(providerKey, providerRegistry) {
|
|
31
|
+
const profile = providerRegistry.get(providerKey);
|
|
32
|
+
if (profile.modelId) {
|
|
33
|
+
return profile.modelId;
|
|
34
|
+
}
|
|
35
|
+
const parts = providerKey.split('.');
|
|
36
|
+
if (parts.length === 2) {
|
|
37
|
+
return parts[1] || null;
|
|
38
|
+
}
|
|
39
|
+
if (parts.length === 3) {
|
|
40
|
+
return parts[2] || null;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClassificationResult, RouterMetadataInput, RoutingFeatures } from '../../types.js';
|
|
2
|
+
import type { RoutingInstructionState } from '../../routing-instructions.js';
|
|
3
|
+
import type { SelectionDeps } from '../../engine-selection/selection-deps.js';
|
|
4
|
+
export { selectDirectProviderModel } from '../../engine-selection/direct-provider-model.js';
|
|
5
|
+
export { selectFromStickyPool } from '../../engine-selection/sticky-pool.js';
|
|
6
|
+
export declare function selectProviderImpl(requestedRoute: string, metadata: RouterMetadataInput, classification: ClassificationResult, features: RoutingFeatures, activeState: RoutingInstructionState, deps: SelectionDeps, options?: {
|
|
7
|
+
routingState?: RoutingInstructionState;
|
|
8
|
+
}): {
|
|
9
|
+
providerKey: string;
|
|
10
|
+
routeUsed: string;
|
|
11
|
+
pool: string[];
|
|
12
|
+
poolId?: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { DEFAULT_ROUTE, VirtualRouterError, VirtualRouterErrorCode } from '../../types.js';
|
|
2
|
+
import { extractExcludedProviderKeySet, extractProviderId } from '../../engine-selection/key-parsing.js';
|
|
3
|
+
import { trySelectFromTier } from '../../engine-selection/tier-selection.js';
|
|
4
|
+
import { resolveInstructionTarget } from '../../engine-selection/instruction-target.js';
|
|
5
|
+
import { filterCandidatesByRoutingState } from '../../engine-selection/routing-state-filter.js';
|
|
6
|
+
import { selectFromStickyPool as selectFromStickyPoolImpl } from '../../engine-selection/sticky-pool.js';
|
|
7
|
+
export { selectDirectProviderModel } from '../../engine-selection/direct-provider-model.js';
|
|
8
|
+
export { selectFromStickyPool } from '../../engine-selection/sticky-pool.js';
|
|
9
|
+
import { buildRouteCandidates, extendRouteCandidatesForState, initializeRouteQueue, normalizeRouteAlias, routeHasTargets, sortRoutePools } from '../../engine-selection/route-utils.js';
|
|
10
|
+
export function selectProviderImpl(requestedRoute, metadata, classification, features, activeState, deps, options = {}) {
|
|
11
|
+
const state = options.routingState ?? activeState;
|
|
12
|
+
const quotaView = deps.quotaView;
|
|
13
|
+
const quotaNow = quotaView ? Date.now() : 0;
|
|
14
|
+
const isAllowedByQuota = (key) => {
|
|
15
|
+
if (!quotaView) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const entry = quotaView(key);
|
|
19
|
+
if (!entry) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (!entry.inPool) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (entry.cooldownUntil && entry.cooldownUntil > quotaNow) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (entry.blacklistUntil && entry.blacklistUntil > quotaNow) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
const excludedProviderKeys = extractExcludedProviderKeySet(features.metadata);
|
|
34
|
+
const forcedResolution = state.forcedTarget ? resolveInstructionTarget(state.forcedTarget, deps.providerRegistry) : null;
|
|
35
|
+
if (forcedResolution && forcedResolution.mode === 'exact') {
|
|
36
|
+
const forcedKey = forcedResolution.keys[0];
|
|
37
|
+
if (!excludedProviderKeys.has(forcedKey) && !deps.isProviderCoolingDown(forcedKey) && isAllowedByQuota(forcedKey)) {
|
|
38
|
+
return {
|
|
39
|
+
providerKey: forcedKey,
|
|
40
|
+
routeUsed: requestedRoute,
|
|
41
|
+
pool: [forcedKey],
|
|
42
|
+
poolId: 'forced'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let stickyResolution = null;
|
|
47
|
+
let stickyKeySet;
|
|
48
|
+
if (!forcedResolution && state.stickyTarget) {
|
|
49
|
+
stickyResolution = resolveInstructionTarget(state.stickyTarget, deps.providerRegistry);
|
|
50
|
+
if (stickyResolution && stickyResolution.mode === 'exact') {
|
|
51
|
+
const stickyKey = stickyResolution.keys[0];
|
|
52
|
+
if ((deps.quotaView ? true : deps.healthManager.isAvailable(stickyKey)) &&
|
|
53
|
+
!excludedProviderKeys.has(stickyKey) &&
|
|
54
|
+
!deps.isProviderCoolingDown(stickyKey) &&
|
|
55
|
+
isAllowedByQuota(stickyKey)) {
|
|
56
|
+
return {
|
|
57
|
+
providerKey: stickyKey,
|
|
58
|
+
routeUsed: requestedRoute,
|
|
59
|
+
pool: [stickyKey],
|
|
60
|
+
poolId: 'sticky'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (stickyResolution && stickyResolution.mode === 'filter' && stickyResolution.keys.length > 0) {
|
|
65
|
+
const liveKeys = stickyResolution.keys.filter((key) => (deps.quotaView ? true : deps.healthManager.isAvailable(key)) &&
|
|
66
|
+
!excludedProviderKeys.has(key) &&
|
|
67
|
+
!deps.isProviderCoolingDown(key) &&
|
|
68
|
+
isAllowedByQuota(key));
|
|
69
|
+
if (liveKeys.length > 0) {
|
|
70
|
+
stickyKeySet = new Set(liveKeys);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const allowAliasRotation = Boolean(state.stickyTarget) &&
|
|
75
|
+
!state.stickyTarget?.keyAlias &&
|
|
76
|
+
state.stickyTarget?.keyIndex === undefined;
|
|
77
|
+
if (forcedResolution && forcedResolution.mode === 'filter') {
|
|
78
|
+
const forcedKeySet = new Set(forcedResolution.keys);
|
|
79
|
+
if (forcedKeySet.size > 0) {
|
|
80
|
+
for (const key of Array.from(forcedKeySet)) {
|
|
81
|
+
if (excludedProviderKeys.has(key) || deps.isProviderCoolingDown(key)) {
|
|
82
|
+
forcedKeySet.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (forcedKeySet.size > 0) {
|
|
87
|
+
const candidates = extendRouteCandidatesForState(buildRouteCandidates(requestedRoute, classification.candidates, features, deps.routing, deps.providerRegistry), state, deps.routing);
|
|
88
|
+
const filteredCandidates = filterCandidatesByRoutingState(candidates, state, deps.routing, deps.providerRegistry);
|
|
89
|
+
if (filteredCandidates.length === 0) {
|
|
90
|
+
const allowedProviders = Array.from(state.allowedProviders);
|
|
91
|
+
const disabledProviders = Array.from(state.disabledProviders);
|
|
92
|
+
const providersInRouting = new Set();
|
|
93
|
+
for (const pools of Object.values(deps.routing)) {
|
|
94
|
+
if (!Array.isArray(pools))
|
|
95
|
+
continue;
|
|
96
|
+
for (const pool of pools) {
|
|
97
|
+
if (!pool || !Array.isArray(pool.targets))
|
|
98
|
+
continue;
|
|
99
|
+
for (const key of pool.targets) {
|
|
100
|
+
if (typeof key !== 'string' || !key)
|
|
101
|
+
continue;
|
|
102
|
+
const providerId = extractProviderId(key);
|
|
103
|
+
if (providerId) {
|
|
104
|
+
providersInRouting.add(providerId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const missingAllowedProviders = allowedProviders.length > 0 ? allowedProviders.filter((provider) => !providersInRouting.has(provider)) : [];
|
|
110
|
+
const hint = (() => {
|
|
111
|
+
if (missingAllowedProviders.length > 0) {
|
|
112
|
+
return `Allowed providers not present in routing pools: ${missingAllowedProviders.join(', ')}`;
|
|
113
|
+
}
|
|
114
|
+
return 'Routing instructions excluded all route candidates';
|
|
115
|
+
})();
|
|
116
|
+
throw new VirtualRouterError(`No available providers after applying routing instructions (${hint}). ` +
|
|
117
|
+
`Tip: remove/adjust <**...**> routing instructions (or use <**clear**>), or add providers/models to routing.`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, {
|
|
118
|
+
requestedRoute,
|
|
119
|
+
allowedProviders,
|
|
120
|
+
disabledProviders,
|
|
121
|
+
missingAllowedProviders
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return selectFromCandidates(filteredCandidates, metadata, classification, features, state, deps, {
|
|
125
|
+
requiredProviderKeys: forcedKeySet,
|
|
126
|
+
allowAliasRotation
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (stickyKeySet && stickyKeySet.size > 0) {
|
|
131
|
+
const stickySelection = selectFromStickyPoolImpl(stickyKeySet, metadata, features, state, deps, { allowAliasRotation });
|
|
132
|
+
if (stickySelection) {
|
|
133
|
+
return stickySelection;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const candidates = buildRouteCandidates(requestedRoute, classification.candidates, features, deps.routing, deps.providerRegistry);
|
|
137
|
+
const expandedCandidates = extendRouteCandidatesForState(candidates, state, deps.routing);
|
|
138
|
+
const filteredCandidates = filterCandidatesByRoutingState(expandedCandidates, state, deps.routing, deps.providerRegistry);
|
|
139
|
+
if (filteredCandidates.length === 0) {
|
|
140
|
+
const allowedProviders = Array.from(state.allowedProviders);
|
|
141
|
+
const disabledProviders = Array.from(state.disabledProviders);
|
|
142
|
+
const providersInRouting = new Set();
|
|
143
|
+
for (const pools of Object.values(deps.routing)) {
|
|
144
|
+
if (!Array.isArray(pools))
|
|
145
|
+
continue;
|
|
146
|
+
for (const pool of pools) {
|
|
147
|
+
if (!pool || !Array.isArray(pool.targets))
|
|
148
|
+
continue;
|
|
149
|
+
for (const key of pool.targets) {
|
|
150
|
+
if (typeof key !== 'string' || !key)
|
|
151
|
+
continue;
|
|
152
|
+
const providerId = extractProviderId(key);
|
|
153
|
+
if (providerId) {
|
|
154
|
+
providersInRouting.add(providerId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const missingAllowedProviders = allowedProviders.length > 0 ? allowedProviders.filter((provider) => !providersInRouting.has(provider)) : [];
|
|
160
|
+
const hint = (() => {
|
|
161
|
+
if (missingAllowedProviders.length > 0) {
|
|
162
|
+
return `Allowed providers not present in routing pools: ${missingAllowedProviders.join(', ')}`;
|
|
163
|
+
}
|
|
164
|
+
return 'Routing instructions excluded all route candidates';
|
|
165
|
+
})();
|
|
166
|
+
throw new VirtualRouterError(`No available providers after applying routing instructions (${hint}). ` +
|
|
167
|
+
`Tip: remove/adjust <**...**> routing instructions (or use <**clear**>), or add providers/models to routing.`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, {
|
|
168
|
+
requestedRoute,
|
|
169
|
+
allowedProviders,
|
|
170
|
+
disabledProviders,
|
|
171
|
+
missingAllowedProviders
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return selectFromCandidates(filteredCandidates, metadata, classification, features, state, deps, {
|
|
175
|
+
allowAliasRotation
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function selectFromCandidates(routes, metadata, classification, features, state, deps, options) {
|
|
179
|
+
const allowedProviders = new Set(state.allowedProviders);
|
|
180
|
+
const disabledProviders = new Set(state.disabledProviders);
|
|
181
|
+
const disabledKeysMap = new Map(Array.from(state.disabledKeys.entries()).map(([provider, keys]) => [
|
|
182
|
+
provider,
|
|
183
|
+
new Set(Array.from(keys).map((k) => (typeof k === 'string' ? k : k + 1)))
|
|
184
|
+
]));
|
|
185
|
+
const disabledModels = new Map(Array.from(state.disabledModels.entries()).map(([provider, models]) => [provider, new Set(models)]));
|
|
186
|
+
const stickyKey = options.allowAliasRotation ? undefined : deps.resolveStickyKey(metadata);
|
|
187
|
+
const attempted = [];
|
|
188
|
+
const visitedRoutes = new Set();
|
|
189
|
+
const routeQueue = initializeRouteQueue(routes);
|
|
190
|
+
const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
|
|
191
|
+
? Math.max(0, features.estimatedTokens)
|
|
192
|
+
: 0;
|
|
193
|
+
while (routeQueue.length) {
|
|
194
|
+
const routeName = routeQueue.shift();
|
|
195
|
+
if (visitedRoutes.has(routeName)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const routePools = deps.routing[routeName];
|
|
199
|
+
if (!routeHasTargets(routePools)) {
|
|
200
|
+
visitedRoutes.add(routeName);
|
|
201
|
+
attempted.push(`${routeName}:empty`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
visitedRoutes.add(routeName);
|
|
205
|
+
const orderedPools = sortRoutePools(routePools);
|
|
206
|
+
for (const poolTier of orderedPools) {
|
|
207
|
+
const { providerKey, poolTargets, tierId, failureHint } = trySelectFromTier(routeName, poolTier, stickyKey, estimatedTokens, features, deps, {
|
|
208
|
+
disabledProviders,
|
|
209
|
+
disabledKeysMap,
|
|
210
|
+
allowedProviders,
|
|
211
|
+
disabledModels,
|
|
212
|
+
requiredProviderKeys: options.requiredProviderKeys,
|
|
213
|
+
allowAliasRotation: options.allowAliasRotation
|
|
214
|
+
});
|
|
215
|
+
if (providerKey) {
|
|
216
|
+
return { providerKey, routeUsed: routeName, pool: poolTargets, poolId: tierId };
|
|
217
|
+
}
|
|
218
|
+
if (failureHint) {
|
|
219
|
+
attempted.push(failureHint);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const requestedRoute = normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
|
|
224
|
+
throw new VirtualRouterError(`All providers unavailable for route ${requestedRoute}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { routeName: requestedRoute, attempted });
|
|
225
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function resolveSessionScope(metadata) {
|
|
2
|
+
const sessionId = typeof metadata.sessionId === 'string' ? metadata.sessionId.trim() : '';
|
|
3
|
+
if (sessionId) {
|
|
4
|
+
return `session:${sessionId}`;
|
|
5
|
+
}
|
|
6
|
+
const conversationId = typeof metadata.conversationId === 'string' ? metadata.conversationId.trim() : '';
|
|
7
|
+
if (conversationId) {
|
|
8
|
+
return `conversation:${conversationId}`;
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
export function resolveStickyKey(metadata) {
|
|
13
|
+
const providerProtocol = metadata.providerProtocol;
|
|
14
|
+
// For Responses protocol, auto-sticky is request-chain scoped:
|
|
15
|
+
// - resume/submit: stickyKey = previousRequestId (points to chain root)
|
|
16
|
+
// - normal call: stickyKey = requestId
|
|
17
|
+
if (providerProtocol === 'openai-responses') {
|
|
18
|
+
const resume = metadata.responsesResume;
|
|
19
|
+
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
20
|
+
return resume.previousRequestId.trim();
|
|
21
|
+
}
|
|
22
|
+
return metadata.requestId;
|
|
23
|
+
}
|
|
24
|
+
// Other protocols: session/conversation scoped when available.
|
|
25
|
+
const sessionScope = resolveSessionScope(metadata);
|
|
26
|
+
if (sessionScope) {
|
|
27
|
+
return sessionScope;
|
|
28
|
+
}
|
|
29
|
+
return metadata.requestId;
|
|
30
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RouterMetadataInput, RoutingInstructionMode } from '../../types.js';
|
|
2
|
+
import type { RoutingInstruction, RoutingInstructionState } from '../../routing-instructions.js';
|
|
3
|
+
export declare function buildMetadataInstructions(metadata: RouterMetadataInput, options?: {
|
|
4
|
+
forcedProviderKeyField?: string;
|
|
5
|
+
}): RoutingInstruction[];
|
|
6
|
+
export declare function resolveRoutingMode(instructions: RoutingInstruction[], state: RoutingInstructionState): RoutingInstructionMode;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function buildMetadataInstructions(metadata, options) {
|
|
2
|
+
const instructions = [];
|
|
3
|
+
const forcedField = options?.forcedProviderKeyField || '__shadowCompareForcedProviderKey';
|
|
4
|
+
const forcedProviderKeyRaw = metadata[forcedField];
|
|
5
|
+
const forcedProviderKey = parseMetadataForceProviderKey(forcedProviderKeyRaw);
|
|
6
|
+
if (forcedProviderKey) {
|
|
7
|
+
instructions.push({ type: 'force', ...forcedProviderKey });
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(metadata.disabledProviderKeyAliases)) {
|
|
10
|
+
for (const entry of metadata.disabledProviderKeyAliases) {
|
|
11
|
+
const parsed = parseMetadataDisableDescriptor(entry);
|
|
12
|
+
if (parsed) {
|
|
13
|
+
instructions.push({ type: 'disable', ...parsed });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return instructions;
|
|
18
|
+
}
|
|
19
|
+
export function resolveRoutingMode(instructions, state) {
|
|
20
|
+
const hasForce = instructions.some((inst) => inst.type === 'force');
|
|
21
|
+
const hasAllow = instructions.some((inst) => inst.type === 'allow');
|
|
22
|
+
const hasClear = instructions.some((inst) => inst.type === 'clear');
|
|
23
|
+
const hasPrefer = instructions.some((inst) => inst.type === 'prefer');
|
|
24
|
+
if (hasClear) {
|
|
25
|
+
return 'none';
|
|
26
|
+
}
|
|
27
|
+
if (hasAllow || state.allowedProviders.size > 0) {
|
|
28
|
+
return 'sticky';
|
|
29
|
+
}
|
|
30
|
+
if (hasForce || state.forcedTarget) {
|
|
31
|
+
return 'force';
|
|
32
|
+
}
|
|
33
|
+
if (hasPrefer || state.preferTarget) {
|
|
34
|
+
return 'sticky';
|
|
35
|
+
}
|
|
36
|
+
if (state.stickyTarget) {
|
|
37
|
+
return 'sticky';
|
|
38
|
+
}
|
|
39
|
+
return 'none';
|
|
40
|
+
}
|
|
41
|
+
function parseMetadataDisableDescriptor(entry) {
|
|
42
|
+
if (typeof entry !== 'string') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const trimmed = entry.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const parts = trimmed.split('.');
|
|
50
|
+
if (parts.length < 2) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const provider = parts[0];
|
|
54
|
+
const alias = parts[1];
|
|
55
|
+
if (!provider || !alias) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (/^\d+$/.test(alias)) {
|
|
59
|
+
return { provider, keyIndex: Number.parseInt(alias, 10) };
|
|
60
|
+
}
|
|
61
|
+
return { provider, keyAlias: alias };
|
|
62
|
+
}
|
|
63
|
+
function parseMetadataForceProviderKey(entry) {
|
|
64
|
+
if (typeof entry !== 'string') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const trimmed = entry.trim();
|
|
68
|
+
if (!trimmed) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Accept the bracket notation used in virtual-router-hit logs: provider[alias].model
|
|
72
|
+
// - provider[].model means provider.model across all aliases
|
|
73
|
+
const bracketMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\[([a-zA-Z0-9_-]*)\](?:\.(.+))?$/);
|
|
74
|
+
if (bracketMatch) {
|
|
75
|
+
const provider = bracketMatch[1]?.trim() || '';
|
|
76
|
+
const keyAlias = bracketMatch[2]?.trim() || '';
|
|
77
|
+
const model = typeof bracketMatch[3] === 'string' ? bracketMatch[3].trim() : '';
|
|
78
|
+
if (!provider) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (keyAlias) {
|
|
82
|
+
return {
|
|
83
|
+
provider,
|
|
84
|
+
keyAlias,
|
|
85
|
+
...(model ? { model } : {}),
|
|
86
|
+
pathLength: 3
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (model) {
|
|
90
|
+
return {
|
|
91
|
+
provider,
|
|
92
|
+
model,
|
|
93
|
+
pathLength: 2
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { provider, pathLength: 1 };
|
|
97
|
+
}
|
|
98
|
+
// Accept provider.keyAlias.model and provider.model (model may contain dots when keyAlias is explicit).
|
|
99
|
+
const parts = trimmed.split('.').map((part) => part.trim()).filter(Boolean);
|
|
100
|
+
if (parts.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const provider = parts[0] || '';
|
|
104
|
+
if (!provider) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (parts.length === 1) {
|
|
108
|
+
return { provider, pathLength: 1 };
|
|
109
|
+
}
|
|
110
|
+
if (parts.length === 2) {
|
|
111
|
+
const second = parts[1] || '';
|
|
112
|
+
if (!second) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (/^\d+$/.test(second)) {
|
|
116
|
+
const keyIndex = Number.parseInt(second, 10);
|
|
117
|
+
return Number.isFinite(keyIndex) && keyIndex > 0 ? { provider, keyIndex, pathLength: 2 } : null;
|
|
118
|
+
}
|
|
119
|
+
return { provider, model: second, pathLength: 2 };
|
|
120
|
+
}
|
|
121
|
+
const keyAlias = parts[1] || '';
|
|
122
|
+
const model = parts.slice(2).join('.').trim();
|
|
123
|
+
if (!keyAlias) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
provider,
|
|
128
|
+
keyAlias,
|
|
129
|
+
...(model ? { model } : {}),
|
|
130
|
+
pathLength: 3
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RouterMetadataInput } from '../../types.js';
|
|
2
|
+
import type { RoutingInstructionState } from '../../routing-instructions.js';
|
|
3
|
+
type RoutingInstructionStateStoreLike = {
|
|
4
|
+
loadSync: (key: string) => RoutingInstructionState | null;
|
|
5
|
+
saveAsync: (key: string, state: RoutingInstructionState | null) => void;
|
|
6
|
+
saveSync?: (key: string, state: RoutingInstructionState | null) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function resolveStopMessageScope(metadata: RouterMetadataInput): string | undefined;
|
|
9
|
+
export declare function getRoutingInstructionState(stickyKey: string | undefined, routingInstructionState: Map<string, RoutingInstructionState>, routingStateStore: RoutingInstructionStateStoreLike): RoutingInstructionState;
|
|
10
|
+
export declare function persistRoutingInstructionState(key: string, state: RoutingInstructionState, routingStateStore: RoutingInstructionStateStoreLike): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mergeStopMessageFromPersisted } from '../../stop-message-state-sync.js';
|
|
2
|
+
export function resolveStopMessageScope(metadata) {
|
|
3
|
+
const sessionId = typeof metadata?.sessionId === 'string' ? String(metadata.sessionId).trim() : '';
|
|
4
|
+
if (sessionId) {
|
|
5
|
+
return `session:${sessionId}`;
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
export function getRoutingInstructionState(stickyKey, routingInstructionState, routingStateStore) {
|
|
10
|
+
const key = stickyKey || 'default';
|
|
11
|
+
const existing = routingInstructionState.get(key);
|
|
12
|
+
// 对 session:/conversation: 作用域,在每次读取时尝试从磁盘刷新 stopMessage 相关字段,
|
|
13
|
+
// 确保 servertool(如 stop_message_auto)通过 sticky-session-store 更新的使用次数
|
|
14
|
+
// 能在 VirtualRouter 日志中实时反映出来。
|
|
15
|
+
if (existing && (key.startsWith('session:') || key.startsWith('conversation:'))) {
|
|
16
|
+
try {
|
|
17
|
+
const persisted = routingStateStore.loadSync(key);
|
|
18
|
+
const merged = mergeStopMessageFromPersisted(existing, persisted);
|
|
19
|
+
existing.stopMessageSource = merged.stopMessageSource;
|
|
20
|
+
existing.stopMessageText = merged.stopMessageText;
|
|
21
|
+
existing.stopMessageMaxRepeats = merged.stopMessageMaxRepeats;
|
|
22
|
+
existing.stopMessageUsed = merged.stopMessageUsed;
|
|
23
|
+
existing.stopMessageUpdatedAt = merged.stopMessageUpdatedAt;
|
|
24
|
+
existing.stopMessageLastUsedAt = merged.stopMessageLastUsedAt;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// 刷新失败不影响原有内存状态
|
|
28
|
+
}
|
|
29
|
+
return existing;
|
|
30
|
+
}
|
|
31
|
+
let initial = null;
|
|
32
|
+
// 仅对 session:/conversation: 作用域的 key 尝试从磁盘恢复持久化状态
|
|
33
|
+
if (key.startsWith('session:') || key.startsWith('conversation:')) {
|
|
34
|
+
initial = routingStateStore.loadSync(key);
|
|
35
|
+
}
|
|
36
|
+
if (!initial) {
|
|
37
|
+
initial = {
|
|
38
|
+
forcedTarget: undefined,
|
|
39
|
+
stickyTarget: undefined,
|
|
40
|
+
allowedProviders: new Set(),
|
|
41
|
+
disabledProviders: new Set(),
|
|
42
|
+
disabledKeys: new Map(),
|
|
43
|
+
disabledModels: new Map(),
|
|
44
|
+
stopMessageSource: undefined,
|
|
45
|
+
stopMessageText: undefined,
|
|
46
|
+
stopMessageMaxRepeats: undefined,
|
|
47
|
+
stopMessageUsed: undefined,
|
|
48
|
+
stopMessageUpdatedAt: undefined,
|
|
49
|
+
stopMessageLastUsedAt: undefined
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
routingInstructionState.set(key, initial);
|
|
53
|
+
return initial;
|
|
54
|
+
}
|
|
55
|
+
function isRoutingStateEmpty(state) {
|
|
56
|
+
if (!state) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const noForced = !state.forcedTarget;
|
|
60
|
+
const noSticky = !state.stickyTarget;
|
|
61
|
+
const noPrefer = !state.preferTarget;
|
|
62
|
+
const noAllowed = state.allowedProviders.size === 0;
|
|
63
|
+
const noDisabledProviders = state.disabledProviders.size === 0;
|
|
64
|
+
const noDisabledKeys = state.disabledKeys.size === 0;
|
|
65
|
+
const noDisabledModels = state.disabledModels.size === 0;
|
|
66
|
+
const noStopMessage = (!state.stopMessageText || !state.stopMessageText.trim()) &&
|
|
67
|
+
(typeof state.stopMessageMaxRepeats !== 'number' || !Number.isFinite(state.stopMessageMaxRepeats)) &&
|
|
68
|
+
(typeof state.stopMessageUsed !== 'number' || !Number.isFinite(state.stopMessageUsed)) &&
|
|
69
|
+
(typeof state.stopMessageUpdatedAt !== 'number' || !Number.isFinite(state.stopMessageUpdatedAt)) &&
|
|
70
|
+
(typeof state.stopMessageLastUsedAt !== 'number' || !Number.isFinite(state.stopMessageLastUsedAt));
|
|
71
|
+
return (noForced &&
|
|
72
|
+
noSticky &&
|
|
73
|
+
noPrefer &&
|
|
74
|
+
noAllowed &&
|
|
75
|
+
noDisabledProviders &&
|
|
76
|
+
noDisabledKeys &&
|
|
77
|
+
noDisabledModels &&
|
|
78
|
+
noStopMessage);
|
|
79
|
+
}
|
|
80
|
+
export function persistRoutingInstructionState(key, state, routingStateStore) {
|
|
81
|
+
if (!key || (!key.startsWith('session:') && !key.startsWith('conversation:'))) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const supportsSync = typeof routingStateStore.saveSync === 'function';
|
|
85
|
+
const prefersSync = supportsSync &&
|
|
86
|
+
key.startsWith('session:') &&
|
|
87
|
+
(Boolean(state.stopMessageText && state.stopMessageText.trim()) ||
|
|
88
|
+
(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)) ||
|
|
89
|
+
(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)) ||
|
|
90
|
+
(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)) ||
|
|
91
|
+
(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)));
|
|
92
|
+
if (isRoutingStateEmpty(state)) {
|
|
93
|
+
if (prefersSync) {
|
|
94
|
+
routingStateStore.saveSync(key, null);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
routingStateStore.saveAsync(key, null);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (prefersSync) {
|
|
102
|
+
routingStateStore.saveSync(key, state);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
routingStateStore.saveAsync(key, state);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ProviderRegistry } from './provider-registry.js';
|
|
3
|
-
import type { ProviderErrorEvent, ProviderFailureEvent, ProviderHealthConfig } from './types.js';
|
|
4
|
-
type DebugLike = {
|
|
5
|
-
log?: (...args: unknown[]) => void;
|
|
6
|
-
} | Console | undefined;
|
|
7
|
-
export declare function resetRateLimitBackoffForProvider(providerKey: string): void;
|
|
8
|
-
export declare function applyAntigravityRiskPolicyImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
|
|
9
|
-
export declare function handleProviderFailureImpl(event: ProviderFailureEvent, healthManager: ProviderHealthManager, healthConfig: Required<ProviderHealthConfig>, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void): void;
|
|
10
|
-
export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthConfig: Required<ProviderHealthConfig>): ProviderFailureEvent | null;
|
|
11
|
-
export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
|
|
12
|
-
/**
|
|
13
|
-
* 处理来自 Host 侧的配额恢复事件:
|
|
14
|
-
* - 清除指定 providerKey 在健康管理器中的熔断/冷却状态;
|
|
15
|
-
* - 清理对应的速率退避计数;
|
|
16
|
-
* - 调用调用方提供的 clearProviderCooldown 回调移除显式 cooldown TTL。
|
|
17
|
-
*
|
|
18
|
-
* 返回值表示是否已处理(true=已处理且后续应跳过常规错误映射逻辑)。
|
|
19
|
-
*/
|
|
20
|
-
export declare function applyQuotaRecoveryImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, clearProviderCooldown: (providerKey: string) => void, debug?: DebugLike): boolean;
|
|
21
|
-
export declare function applyQuotaDepletedImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): boolean;
|
|
22
|
-
export declare function deriveReason(code: string, stage: string, statusCode?: number): string;
|
|
23
|
-
export {};
|
|
1
|
+
export { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl, resetRateLimitBackoffForProvider, deriveReason } from './engine/health/index.js';
|