@jsonstudio/llms 0.6.802 → 0.6.938

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.
Files changed (186) hide show
  1. package/dist/bridge/routecodex-adapter.d.ts +74 -0
  2. package/dist/config-unified/enhanced-path-resolver.d.ts +5 -0
  3. package/dist/config-unified/unified-config.d.ts +26 -0
  4. package/dist/conversion/codec-registry.d.ts +10 -0
  5. package/dist/conversion/codecs/gemini-openai-codec.d.ts +16 -0
  6. package/dist/conversion/codecs/openai-openai-codec.d.ts +12 -0
  7. package/dist/conversion/codecs/responses-openai-codec.d.ts +12 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +12 -0
  9. package/dist/conversion/config/config-manager.d.ts +212 -0
  10. package/dist/conversion/hub/config/types.d.ts +26 -0
  11. package/dist/conversion/hub/core/detour-registry.d.ts +9 -0
  12. package/dist/conversion/hub/core/hub-context.d.ts +21 -0
  13. package/dist/conversion/hub/core/index.d.ts +3 -0
  14. package/dist/conversion/hub/core/stage-driver.d.ts +30 -0
  15. package/dist/conversion/hub/format-adapters/anthropic-format-adapter.d.ts +16 -0
  16. package/dist/conversion/hub/format-adapters/chat-format-adapter.d.ts +17 -0
  17. package/dist/conversion/hub/format-adapters/gemini-format-adapter.d.ts +16 -0
  18. package/dist/conversion/hub/format-adapters/index.d.ts +21 -0
  19. package/dist/conversion/hub/hub-feature.d.ts +1 -0
  20. package/dist/conversion/hub/node-support.d.ts +19 -0
  21. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +11 -0
  22. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +7 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline.js +71 -14
  25. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +4 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +23 -1
  27. package/dist/conversion/hub/pipelines/inbound.d.ts +22 -0
  28. package/dist/conversion/hub/pipelines/outbound.d.ts +22 -0
  29. package/dist/conversion/hub/policy/policy-engine.d.ts +46 -0
  30. package/dist/conversion/hub/policy/policy-engine.js +176 -0
  31. package/dist/conversion/hub/policy/protocol-spec.d.ts +50 -0
  32. package/dist/conversion/hub/policy/protocol-spec.js +105 -0
  33. package/dist/conversion/hub/process/chat-process.d.ts +32 -0
  34. package/dist/conversion/hub/registry.d.ts +28 -0
  35. package/dist/conversion/hub/response/chat-response-utils.d.ts +6 -0
  36. package/dist/conversion/hub/response/provider-response.js +31 -0
  37. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +7 -0
  38. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +87 -1
  39. package/dist/conversion/hub/semantic-mappers/index.d.ts +4 -0
  40. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +21 -0
  41. package/dist/conversion/hub/standardized-bridge.d.ts +12 -0
  42. package/dist/conversion/hub/types/chat-schema.d.ts +112 -0
  43. package/dist/conversion/hub/types/errors.d.ts +5 -0
  44. package/dist/conversion/hub/types/format-envelope.d.ts +7 -0
  45. package/dist/conversion/hub/types/index.d.ts +6 -0
  46. package/dist/conversion/hub/types/json.d.ts +9 -0
  47. package/dist/conversion/hub/types/node.d.ts +31 -0
  48. package/dist/conversion/responses/responses-openai-bridge.js +263 -10
  49. package/dist/conversion/schema-validator.d.ts +7 -0
  50. package/dist/conversion/shared/args-mapping.d.ts +18 -0
  51. package/dist/conversion/shared/chat-request-filters.d.ts +9 -0
  52. package/dist/conversion/shared/errors.d.ts +1 -1
  53. package/dist/conversion/shared/gemini-tool-utils.js +61 -0
  54. package/dist/conversion/shared/jsonish.d.ts +3 -0
  55. package/dist/conversion/shared/mcp-injection.d.ts +2 -0
  56. package/dist/conversion/shared/media.d.ts +1 -0
  57. package/dist/conversion/shared/openai-message-normalize.d.ts +1 -0
  58. package/dist/conversion/shared/payload-budget.d.ts +13 -0
  59. package/dist/conversion/shared/reasoning-mapping.d.ts +5 -0
  60. package/dist/conversion/shared/responses-request-adapter.d.ts +1 -28
  61. package/dist/conversion/shared/responses-request-adapter.js +1 -430
  62. package/dist/conversion/shared/snapshot-hooks.js +58 -3
  63. package/dist/conversion/shared/tool-governor.js +8 -2
  64. package/dist/conversion/shared/tool-harvester.d.ts +31 -0
  65. package/dist/conversion/shared/tool-mapping.js +10 -29
  66. package/dist/conversion/types.d.ts +33 -0
  67. package/dist/filters/builtin/add-fields-filter.d.ts +8 -0
  68. package/dist/filters/builtin/blacklist-filter.d.ts +8 -0
  69. package/dist/filters/builtin/whitelist-filter.d.ts +8 -0
  70. package/dist/filters/engine.d.ts +16 -0
  71. package/dist/filters/special/request-tool-choice-policy.d.ts +11 -0
  72. package/dist/filters/special/response-finish-invariants.d.ts +11 -0
  73. package/dist/filters/special/response-openai-to-responses-bridge.d.ts +13 -0
  74. package/dist/filters/special/response-tool-arguments-blacklist.d.ts +12 -0
  75. package/dist/filters/special/response-tool-arguments-schema-converge.d.ts +13 -0
  76. package/dist/filters/special/response-tool-arguments-stringify.d.ts +9 -0
  77. package/dist/filters/special/response-tool-arguments-whitelist.d.ts +11 -0
  78. package/dist/filters/special/tool-filter-hooks.d.ts +19 -0
  79. package/dist/filters/special/tool-post-constraints.d.ts +31 -0
  80. package/dist/filters/types.d.ts +68 -0
  81. package/dist/filters/utils/fieldmap-loader.d.ts +2 -0
  82. package/dist/filters/utils/snapshot-writer.d.ts +10 -0
  83. package/dist/guidance/index.d.ts +3 -0
  84. package/dist/guidance/index.js +78 -83
  85. package/dist/http/sse-response.d.ts +22 -0
  86. package/dist/router/virtual-router/bootstrap.d.ts +6 -0
  87. package/dist/router/virtual-router/bootstrap.js +49 -5
  88. package/dist/router/virtual-router/classifier.d.ts +10 -0
  89. package/dist/router/virtual-router/engine-selection.js +98 -11
  90. package/dist/router/virtual-router/engine.js +177 -31
  91. package/dist/router/virtual-router/error-center.d.ts +10 -0
  92. package/dist/router/virtual-router/features.d.ts +3 -0
  93. package/dist/router/virtual-router/routing-instructions.d.ts +23 -1
  94. package/dist/router/virtual-router/routing-instructions.js +120 -30
  95. package/dist/router/virtual-router/types.d.ts +11 -0
  96. package/dist/servertool/engine.js +189 -16
  97. package/dist/servertool/handlers/apply-patch-guard.js +269 -0
  98. package/dist/servertool/handlers/exec-command-guard.js +558 -0
  99. package/dist/servertool/handlers/followup-message-trimmer.d.ts +16 -0
  100. package/dist/servertool/handlers/followup-message-trimmer.js +198 -0
  101. package/dist/servertool/handlers/followup-request-builder.d.ts +17 -0
  102. package/dist/servertool/handlers/followup-request-builder.js +122 -0
  103. package/dist/servertool/handlers/gemini-empty-reply-continue.js +252 -51
  104. package/dist/servertool/handlers/iflow-model-error-retry.js +12 -22
  105. package/dist/servertool/handlers/stop-message-auto.js +237 -75
  106. package/dist/servertool/handlers/vision.js +15 -27
  107. package/dist/servertool/handlers/web-search.js +17 -43
  108. package/dist/servertool/server-side-tools.d.ts +3 -0
  109. package/dist/servertool/server-side-tools.js +3 -0
  110. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +2 -1
  111. package/dist/sse/json-to-sse/chat-json-to-sse-converter.d.ts +80 -0
  112. package/dist/sse/json-to-sse/event-generators/chat.d.ts +55 -0
  113. package/dist/sse/json-to-sse/event-generators/responses.d.ts +99 -0
  114. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +2 -1
  115. package/dist/sse/json-to-sse/responses-json-to-sse-converter.d.ts +80 -0
  116. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +1 -1
  117. package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +2 -2
  118. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +1 -1
  119. package/dist/sse/json-to-sse/sequencers/responses-sequencer.d.ts +40 -0
  120. package/dist/sse/shared/chat-serializer.d.ts +4 -0
  121. package/dist/sse/shared/constants.d.ts +272 -0
  122. package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +1 -1
  123. package/dist/sse/shared/serializers/base-serializer.d.ts +158 -0
  124. package/dist/sse/shared/serializers/chat-event-serializer.d.ts +82 -0
  125. package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +1 -1
  126. package/dist/sse/shared/serializers/index.d.ts +2 -1
  127. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +123 -0
  128. package/dist/sse/shared/serializers/types.d.ts +51 -0
  129. package/dist/sse/shared/utils.d.ts +254 -0
  130. package/dist/sse/shared/writer.d.ts +2 -2
  131. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -1
  132. package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +1 -1
  133. package/dist/sse/sse-to-json/builders/response-builder.d.ts +1 -1
  134. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +2 -1
  135. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +2 -1
  136. package/dist/sse/sse-to-json/parsers/sse-parser.d.ts +73 -0
  137. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  138. package/dist/sse/types/chat-types.d.ts +1 -1
  139. package/dist/sse/types/responses-types.d.ts +1 -1
  140. package/dist/tools/apply-patch/execution-capturer.d.ts +13 -0
  141. package/dist/tools/apply-patch/execution-capturer.js +158 -0
  142. package/dist/tools/apply-patch/regression-capturer.d.ts +1 -0
  143. package/dist/tools/apply-patch/regression-capturer.js +5 -4
  144. package/dist/tools/apply-patch/structured.js +109 -13
  145. package/dist/tools/apply-patch/validator.js +112 -18
  146. package/dist/tools/tool-registry.d.ts +8 -0
  147. package/dist/tools/tool-registry.js +2 -1
  148. package/package.json +4 -4
  149. package/dist/conversion/compat/actions/apply-patch-format-fixer.js +0 -233
  150. package/dist/conversion/config/compat-profiles.json +0 -38
  151. package/dist/conversion/hub/pipeline/context-limit.d.ts +0 -13
  152. package/dist/conversion/hub/pipeline/context-limit.js +0 -55
  153. package/dist/conversion/hub/response/server-side-tools.d.ts +0 -26
  154. package/dist/conversion/hub/response/server-side-tools.js +0 -383
  155. package/dist/conversion/shared/bridge-conversation-store.d.ts +0 -41
  156. package/dist/conversion/shared/bridge-conversation-store.js +0 -279
  157. package/dist/conversion/shared/bridge-request-adapter.d.ts +0 -28
  158. package/dist/conversion/shared/bridge-request-adapter.js +0 -430
  159. package/dist/conversion/shared/responses-id-utils.js +0 -42
  160. package/dist/conversion/shared/responses-instructions.js +0 -113
  161. package/dist/conversion/shared/responses-message-utils.d.ts +0 -15
  162. package/dist/conversion/shared/responses-message-utils.js +0 -206
  163. package/dist/conversion/shared/responses-metadata.js +0 -1
  164. package/dist/conversion/shared/responses-output-utils.d.ts +0 -7
  165. package/dist/conversion/shared/responses-output-utils.js +0 -108
  166. package/dist/conversion/shared/responses-types.d.ts +0 -33
  167. package/dist/conversion/shared/tool-normalizers.d.ts +0 -4
  168. package/dist/conversion/shared/tool-normalizers.js +0 -84
  169. package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +0 -13
  170. package/dist/filters/special/request-streaming-to-nonstreaming.js +0 -39
  171. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +0 -23
  172. package/dist/filters/special/response-apply-patch-toon-decode.js +0 -460
  173. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +0 -10
  174. package/dist/filters/special/response-tool-arguments-toon-decode.js +0 -154
  175. package/dist/servertool/flow-types.d.ts +0 -40
  176. package/dist/servertool/flow-types.js +0 -1
  177. package/dist/servertool/orchestration-types.d.ts +0 -33
  178. package/dist/servertool/orchestration-types.js +0 -1
  179. package/dist/servertool/vision-tool.d.ts +0 -2
  180. package/dist/servertool/vision-tool.js +0 -185
  181. package/dist/tools/patch-args-normalizer.d.ts +0 -15
  182. package/dist/tools/patch-args-normalizer.js +0 -472
  183. package/dist/utils/toon.d.ts +0 -4
  184. package/dist/utils/toon.js +0 -75
  185. /package/dist/{conversion/compat/actions/apply-patch-format-fixer.d.ts → servertool/handlers/apply-patch-guard.d.ts} +0 -0
  186. /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
- throw new VirtualRouterError('No available providers after applying routing instructions', VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, {
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: Array.from(state.allowedProviders),
85
- disabledProviders: Array.from(state.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 filteredCandidates = filterCandidatesByRoutingState(candidates, state, deps.routing, deps.providerRegistry);
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
- throw new VirtualRouterError('No available providers after applying routing instructions', VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, {
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: Array.from(state.allowedProviders),
106
- disabledProviders: Array.from(state.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 runtimeKey = providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
826
- if (runtimeKey) {
827
- return { mode: 'exact', keys: [runtimeKey] };
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 baseState = this.getRoutingInstructionState(stickyKey);
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
- const effectiveKey = stickyKey || 'default';
167
- this.routingInstructionState.set(effectiveKey, routingState);
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
- const routingMode = this.resolveRoutingMode([...metadataInstructions, ...instructions], routingState);
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
- classification = metadata.routeHint && metadata.routeHint.trim()
288
- ? {
289
- routeName: metadata.routeHint.trim(),
290
- confidence: 1,
291
- reasoning: `route_hint:${metadata.routeHint.trim()}`,
292
- fallback: false,
293
- candidates: [metadata.routeHint.trim()]
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
- : this.classifier.classify(features);
296
- requestedRoute = this.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
297
- selection = this.selectProvider(requestedRoute, metadata, classification, features, routingState);
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 = loadRoutingInstructionStateSync(key);
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 = loadRoutingInstructionStateSync(key);
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>;