@jsonstudio/llms 0.6.2979 → 0.6.3214

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 (242) hide show
  1. package/dist/conversion/args-mapping.js +8 -0
  2. package/dist/conversion/{shared/bridge-actions.js → bridge-actions.js} +2 -1
  3. package/dist/conversion/{shared/bridge-id-utils.js → bridge-id-utils.js} +1 -1
  4. package/dist/conversion/{shared/bridge-instructions.js → bridge-instructions.js} +1 -1
  5. package/dist/conversion/{shared/bridge-message-utils.d.ts → bridge-message-utils.d.ts} +1 -1
  6. package/dist/conversion/{shared/bridge-message-utils.js → bridge-message-utils.js} +5 -149
  7. package/dist/conversion/{shared/bridge-metadata.js → bridge-metadata.js} +1 -1
  8. package/dist/conversion/{shared/bridge-policies.js → bridge-policies.js} +1 -1
  9. package/dist/conversion/codecs/gemini-openai-codec.js +27 -8
  10. package/dist/conversion/codecs/responses-openai-codec.js +1 -1
  11. package/dist/conversion/{shared/compaction-detect.d.ts → compaction-detect.d.ts} +1 -1
  12. package/dist/conversion/compaction-detect.js +4 -0
  13. package/dist/conversion/compat/actions/apply-patch-fixer.js +2 -2
  14. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +0 -1
  15. package/dist/conversion/compat/actions/deepseek-web-response.js +15 -405
  16. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +1 -1
  17. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +1 -1
  18. package/dist/conversion/compat/actions/qwen-transform.js +74 -2
  19. package/dist/conversion/compat/actions/snapshot.js +1 -1
  20. package/dist/conversion/compat/antigravity-session-signature.js +36 -0
  21. package/dist/conversion/compat/profiles/chat-deepseek-web.json +0 -22
  22. package/dist/conversion/compat/profiles/chat-glm.json +251 -72
  23. package/dist/conversion/compat/profiles/chat-iflow.json +174 -39
  24. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -14
  25. package/dist/conversion/hub/operation-table/operation-table-runner.js +2 -2
  26. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +1 -1
  27. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +7 -4
  28. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +2 -2
  29. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +2 -8
  30. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline.js +50 -3
  32. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +1 -1
  33. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +62 -0
  34. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +3 -1
  35. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +1 -1
  36. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/chat-process-semantics-bridge.d.ts +1 -1
  37. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +42 -29
  38. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +12 -0
  39. package/dist/conversion/hub/policy/protocol-spec.js +1 -1
  40. package/dist/conversion/hub/process/chat-process-clock-reminders.js +1 -1
  41. package/dist/conversion/hub/process/chat-process-clock-tools.js +1 -1
  42. package/dist/conversion/hub/process/chat-process-continue-execution.js +1 -1
  43. package/dist/conversion/hub/process/chat-process-servertool-orchestration.js +1 -1
  44. package/dist/conversion/hub/process/chat-process-web-search.js +1 -1
  45. package/dist/conversion/hub/response/provider-response.js +14 -5
  46. package/dist/conversion/hub/response/response-mappers.js +23 -1
  47. package/dist/conversion/hub/response/response-runtime.js +28 -5
  48. package/dist/conversion/hub/snapshot-recorder.js +1 -1
  49. package/dist/conversion/hub/tool-governance/engine.d.ts +8 -0
  50. package/dist/conversion/hub/tool-governance/engine.js +25 -68
  51. package/dist/conversion/hub/tool-governance/rules.js +73 -69
  52. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +1 -1
  53. package/dist/conversion/index.d.ts +1 -2
  54. package/dist/conversion/index.js +1 -2
  55. package/dist/conversion/{shared/jsonish.js → jsonish.js} +1 -1
  56. package/dist/conversion/{shared/mcp-injection.js → mcp-injection.js} +1 -1
  57. package/dist/conversion/media.js +4 -0
  58. package/dist/conversion/{shared/metadata-passthrough.d.ts → metadata-passthrough.d.ts} +1 -1
  59. package/dist/conversion/{shared/metadata-passthrough.js → metadata-passthrough.js} +2 -2
  60. package/dist/conversion/payload-budget.js +47 -0
  61. package/dist/conversion/protocol-field-allowlists.d.ts +7 -0
  62. package/dist/conversion/protocol-field-allowlists.js +9 -0
  63. package/dist/conversion/{shared/protocol-state.d.ts → protocol-state.d.ts} +2 -2
  64. package/dist/conversion/{shared/protocol-state.js → protocol-state.js} +2 -2
  65. package/dist/conversion/{shared/errors.d.ts → provider-protocol-error.d.ts} +0 -3
  66. package/dist/conversion/provider-protocol-error.js +25 -0
  67. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +8 -5
  68. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +1 -1
  69. package/dist/conversion/responses/responses-openai-bridge.d.ts +1 -1
  70. package/dist/conversion/responses/responses-openai-bridge.js +43 -10
  71. package/dist/conversion/{shared/runtime-metadata.d.ts → runtime-metadata.d.ts} +1 -1
  72. package/dist/conversion/{shared/runtime-metadata.js → runtime-metadata.js} +2 -2
  73. package/dist/conversion/shared/anthropic-message-utils.js +19 -8
  74. package/dist/conversion/shared/chat-request-filters.d.ts +3 -4
  75. package/dist/conversion/shared/chat-request-filters.js +22 -78
  76. package/dist/conversion/shared/gemini-tool-utils.d.ts +1 -1
  77. package/dist/conversion/shared/openai-finalizer.js +1 -0
  78. package/dist/conversion/shared/openai-message-normalize.js +2 -2
  79. package/dist/conversion/shared/reasoning-normalizer.js +6 -0
  80. package/dist/conversion/shared/reasoning-utils.js +5 -2
  81. package/dist/conversion/shared/responses-conversation-store.js +1 -1
  82. package/dist/conversion/shared/responses-output-builder.js +55 -11
  83. package/dist/conversion/shared/responses-reasoning-registry.d.ts +14 -2
  84. package/dist/conversion/shared/responses-reasoning-registry.js +34 -6
  85. package/dist/conversion/shared/responses-response-utils.js +99 -9
  86. package/dist/conversion/shared/responses-tool-utils.js +1 -1
  87. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +1 -1
  88. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -2
  89. package/dist/conversion/shared/text-markup-normalizer.js +1 -1
  90. package/dist/conversion/shared/tool-filter-pipeline.js +1 -1
  91. package/dist/conversion/shared/tool-governor.js +3 -3
  92. package/dist/conversion/shared/tool-mapping.d.ts +1 -1
  93. package/dist/conversion/{shared/snapshot-utils.d.ts → snapshot-utils.d.ts} +11 -0
  94. package/dist/conversion/{shared/snapshot-utils.js → snapshot-utils.js} +14 -23
  95. package/dist/conversion/types/text-markup-normalizer.d.ts +13 -0
  96. package/dist/conversion/types/text-markup-normalizer.js +1 -0
  97. package/dist/filters/special/request-tools-normalize.js +1 -1
  98. package/dist/filters/special/response-tool-text-canonicalize.js +2 -2
  99. package/dist/native/router_hotpath_napi.node +0 -0
  100. package/dist/quota/quota-manager.js +31 -59
  101. package/dist/quota/quota-state.js +14 -7
  102. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +1 -0
  103. package/dist/router/virtual-router/bootstrap/profile-builder.js +13 -0
  104. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +2 -0
  105. package/dist/router/virtual-router/bootstrap/provider-normalization.js +4 -1
  106. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +7 -0
  107. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +44 -0
  108. package/dist/router/virtual-router/bootstrap.js +2 -0
  109. package/dist/router/virtual-router/engine/routing-state/store.d.ts +1 -2
  110. package/dist/router/virtual-router/engine/routing-state/store.js +2 -2
  111. package/dist/router/virtual-router/engine-legacy/config.d.ts +11 -0
  112. package/dist/router/virtual-router/engine-legacy/config.js +108 -0
  113. package/dist/router/virtual-router/engine-legacy/direct-model.d.ts +10 -0
  114. package/dist/router/virtual-router/engine-legacy/direct-model.js +38 -0
  115. package/dist/router/virtual-router/engine-legacy/health.d.ts +13 -0
  116. package/dist/router/virtual-router/engine-legacy/health.js +104 -0
  117. package/dist/router/virtual-router/engine-legacy/helpers.d.ts +16 -0
  118. package/dist/router/virtual-router/engine-legacy/helpers.js +226 -0
  119. package/dist/router/virtual-router/engine-legacy/route-finalize.d.ts +9 -0
  120. package/dist/router/virtual-router/engine-legacy/route-finalize.js +84 -0
  121. package/dist/router/virtual-router/engine-legacy/route-selection.d.ts +17 -0
  122. package/dist/router/virtual-router/engine-legacy/route-selection.js +205 -0
  123. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.d.ts +3 -0
  124. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.js +36 -0
  125. package/dist/router/virtual-router/engine-legacy/route-state.d.ts +12 -0
  126. package/dist/router/virtual-router/engine-legacy/route-state.js +386 -0
  127. package/dist/router/virtual-router/engine-legacy/route-utils.d.ts +19 -0
  128. package/dist/router/virtual-router/engine-legacy/route-utils.js +212 -0
  129. package/dist/router/virtual-router/engine-legacy/routing.d.ts +8 -0
  130. package/dist/router/virtual-router/engine-legacy/routing.js +8 -0
  131. package/dist/router/virtual-router/engine-legacy/selection-core.d.ts +28 -0
  132. package/dist/router/virtual-router/engine-legacy/selection-core.js +112 -0
  133. package/dist/router/virtual-router/engine-legacy/selection-state.d.ts +16 -0
  134. package/dist/router/virtual-router/engine-legacy/selection-state.js +187 -0
  135. package/dist/router/virtual-router/engine-legacy/state-accessors.d.ts +21 -0
  136. package/dist/router/virtual-router/engine-legacy/state-accessors.js +118 -0
  137. package/dist/router/virtual-router/engine-legacy.d.ts +123 -0
  138. package/dist/router/virtual-router/engine-legacy.js +194 -0
  139. package/dist/router/virtual-router/engine-logging.d.ts +2 -0
  140. package/dist/router/virtual-router/engine-logging.js +7 -2
  141. package/dist/router/virtual-router/engine-selection/key-parsing.js +0 -3
  142. package/dist/router/virtual-router/engine-selection/native-chat-request-filter-semantics.d.ts +1 -0
  143. package/dist/router/virtual-router/engine-selection/native-chat-request-filter-semantics.js +54 -0
  144. package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.d.ts +10 -0
  145. package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.js +67 -0
  146. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.d.ts +22 -0
  147. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js +154 -0
  148. package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +38 -2
  149. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +75 -0
  150. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +205 -0
  151. package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.d.ts +2 -0
  152. package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.js +69 -0
  153. package/dist/router/virtual-router/engine-selection/native-virtual-router-engine-proxy.d.ts +16 -0
  154. package/dist/router/virtual-router/engine-selection/native-virtual-router-engine-proxy.js +14 -0
  155. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.d.ts +2 -0
  156. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.js +86 -0
  157. package/dist/router/virtual-router/engine-selection/tier-selection-quota-integration.js +100 -0
  158. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +99 -0
  159. package/dist/router/virtual-router/engine.d.ts +22 -105
  160. package/dist/router/virtual-router/engine.js +274 -1641
  161. package/dist/router/virtual-router/load-balancer.d.ts +8 -0
  162. package/dist/router/virtual-router/load-balancer.js +65 -2
  163. package/dist/router/virtual-router/provider-registry.js +2 -0
  164. package/dist/router/virtual-router/routing-instructions/clean.d.ts +3 -0
  165. package/dist/router/virtual-router/routing-instructions/clean.js +34 -0
  166. package/dist/router/virtual-router/routing-instructions/parse.d.ts +18 -0
  167. package/dist/router/virtual-router/routing-instructions/parse.js +377 -0
  168. package/dist/router/virtual-router/routing-instructions/state.d.ts +4 -0
  169. package/dist/router/virtual-router/routing-instructions/state.js +245 -0
  170. package/dist/router/virtual-router/routing-instructions/types.d.ts +70 -0
  171. package/dist/router/virtual-router/routing-instructions/types.js +2 -0
  172. package/dist/router/virtual-router/routing-instructions.d.ts +5 -89
  173. package/dist/router/virtual-router/routing-instructions.js +4 -655
  174. package/dist/router/virtual-router/sticky-session-store.d.ts +4 -0
  175. package/dist/router/virtual-router/sticky-session-store.js +19 -81
  176. package/dist/router/virtual-router/tool-signals.js +21 -3
  177. package/dist/router/virtual-router/types.d.ts +4 -0
  178. package/dist/servertool/clock/session-scope.js +32 -1
  179. package/dist/servertool/engine.js +79 -8
  180. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +1 -1
  181. package/dist/servertool/handlers/clock-auto.js +1 -1
  182. package/dist/servertool/handlers/clock.js +1 -1
  183. package/dist/servertool/handlers/compaction-detect.d.ts +1 -1
  184. package/dist/servertool/handlers/compaction-detect.js +1 -1
  185. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  186. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  187. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  188. package/dist/servertool/handlers/review.js +1 -1
  189. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +1 -1
  190. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +1 -1
  191. package/dist/servertool/handlers/stop-message-auto.js +1 -1
  192. package/dist/servertool/handlers/vision.js +1 -1
  193. package/dist/servertool/handlers/web-search.js +1 -1
  194. package/dist/servertool/reenter-backend.js +1 -1
  195. package/dist/servertool/server-side-tools.js +2 -2
  196. package/dist/servertool/stop-gateway-context.js +1 -1
  197. package/dist/servertool/stop-message-compare-context.js +1 -1
  198. package/dist/sse/json-to-sse/event-generators/responses.d.ts +4 -0
  199. package/dist/sse/json-to-sse/event-generators/responses.js +95 -1
  200. package/dist/sse/json-to-sse/sequencers/responses-sequencer.js +6 -4
  201. package/dist/sse/sse-to-json/builders/response-builder.d.ts +8 -0
  202. package/dist/sse/sse-to-json/builders/response-builder.js +162 -4
  203. package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +2 -0
  204. package/dist/sse/types/responses-types.d.ts +6 -2
  205. package/dist/tools/apply-patch/structured/coercion.js +5 -0
  206. package/dist/tools/args-json.js +29 -0
  207. package/package.json +8 -5
  208. package/dist/conversion/shared/args-mapping.js +0 -77
  209. package/dist/conversion/shared/compaction-detect.js +0 -4
  210. package/dist/conversion/shared/errors.js +0 -31
  211. package/dist/conversion/shared/media.js +0 -4
  212. package/dist/conversion/shared/payload-budget.js +0 -165
  213. package/dist/conversion/shared/protocol-field-allowlists.d.ts +0 -7
  214. package/dist/conversion/shared/protocol-field-allowlists.js +0 -149
  215. package/dist/conversion/shared/snapshot-hooks.d.ts +0 -11
  216. package/dist/conversion/shared/snapshot-hooks.js +0 -503
  217. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +0 -2
  218. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +0 -129
  219. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +0 -4
  220. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +0 -637
  221. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +0 -21
  222. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +0 -177
  223. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +0 -5
  224. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +0 -385
  225. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +0 -10
  226. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +0 -602
  227. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +0 -5
  228. package/dist/conversion/shared/text-markup-normalizer/extractors.js +0 -4
  229. package/dist/conversion/shared/tool-canonicalizer.d.ts +0 -2
  230. package/dist/conversion/shared/tool-canonicalizer.js +0 -38
  231. /package/dist/conversion/{shared/args-mapping.d.ts → args-mapping.d.ts} +0 -0
  232. /package/dist/conversion/{shared/bridge-actions.d.ts → bridge-actions.d.ts} +0 -0
  233. /package/dist/conversion/{shared/bridge-id-utils.d.ts → bridge-id-utils.d.ts} +0 -0
  234. /package/dist/conversion/{shared/bridge-instructions.d.ts → bridge-instructions.d.ts} +0 -0
  235. /package/dist/conversion/{shared/bridge-metadata.d.ts → bridge-metadata.d.ts} +0 -0
  236. /package/dist/conversion/{shared/bridge-policies.d.ts → bridge-policies.d.ts} +0 -0
  237. /package/dist/conversion/{shared/jsonish.d.ts → jsonish.d.ts} +0 -0
  238. /package/dist/conversion/{shared/mcp-injection.d.ts → mcp-injection.d.ts} +0 -0
  239. /package/dist/conversion/{shared/media.d.ts → media.d.ts} +0 -0
  240. /package/dist/conversion/{shared/payload-budget.d.ts → payload-budget.d.ts} +0 -0
  241. /package/dist/conversion/{shared → types}/bridge-message-types.d.ts +0 -0
  242. /package/dist/conversion/{shared → types}/bridge-message-types.js +0 -0
@@ -0,0 +1,19 @@
1
+ import type { RoutePoolTier } from '../types.js';
2
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
3
+ export declare function normalizeRouteAlias(routeName: string | undefined): string;
4
+ export declare function buildRouteCandidates(engine: VirtualRouterEngine, requestedRoute: string, classificationCandidates: string[] | undefined, features: {
5
+ hasImageAttachment?: boolean;
6
+ hasVideoAttachment?: boolean;
7
+ hasLocalVideoAttachment?: boolean;
8
+ }): string[];
9
+ export declare function reorderForInlineVision(engine: VirtualRouterEngine, routeNames: string[]): string[];
10
+ export declare function reorderForPreferredModel(engine: VirtualRouterEngine, routeNames: string[], modelId: string): string[];
11
+ export declare function routeSupportsModel(engine: VirtualRouterEngine, routeName: string, modelId: string): boolean;
12
+ export declare function routeSupportsInlineVision(engine: VirtualRouterEngine, routeName: string): boolean;
13
+ export declare function sortByPriority(routeNames: string[]): string[];
14
+ export declare function routeWeight(routeName: string): number;
15
+ export declare function routeHasForceFlag(engine: VirtualRouterEngine, routeName: string): boolean;
16
+ export declare function routeHasTargets(engine: VirtualRouterEngine, pools?: RoutePoolTier[]): boolean;
17
+ export declare function hasPrimaryPool(engine: VirtualRouterEngine, pools?: RoutePoolTier[]): boolean;
18
+ export declare function sortRoutePools(_engine: VirtualRouterEngine, pools?: RoutePoolTier[]): RoutePoolTier[];
19
+ export declare function flattenPoolTargets(_engine: VirtualRouterEngine, pools?: RoutePoolTier[]): string[];
@@ -0,0 +1,212 @@
1
+ import { DEFAULT_ROUTE, ROUTE_PRIORITY } from '../types.js';
2
+ export function normalizeRouteAlias(routeName) {
3
+ const base = routeName && routeName.trim() ? routeName.trim() : DEFAULT_ROUTE;
4
+ return base;
5
+ }
6
+ export function buildRouteCandidates(engine, requestedRoute, classificationCandidates, features) {
7
+ const forceVision = routeHasForceFlag(engine, 'vision');
8
+ const hasMultimodalTargets = routeHasTargets(engine, engine.routing.multimodal);
9
+ const hasVisionTargets = routeHasTargets(engine, engine.routing.vision);
10
+ const hasLocalVideoAttachment = features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true;
11
+ if (features.hasImageAttachment && hasLocalVideoAttachment && hasVisionTargets) {
12
+ return ['vision'];
13
+ }
14
+ const normalized = normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
15
+ const baseList = [];
16
+ if (classificationCandidates && classificationCandidates.length) {
17
+ for (const candidate of classificationCandidates) {
18
+ baseList.push(normalizeRouteAlias(candidate));
19
+ }
20
+ }
21
+ else if (normalized) {
22
+ baseList.push(normalized);
23
+ }
24
+ if (features.hasImageAttachment) {
25
+ if (hasMultimodalTargets) {
26
+ if (!baseList.includes('multimodal')) {
27
+ baseList.unshift('multimodal');
28
+ }
29
+ }
30
+ if (hasVisionTargets && (!hasMultimodalTargets || forceVision)) {
31
+ if (!baseList.includes('vision')) {
32
+ baseList.push('vision');
33
+ }
34
+ }
35
+ if (!forceVision && hasMultimodalTargets) {
36
+ const visionAwareRoutes = [DEFAULT_ROUTE, 'thinking'];
37
+ for (const routeName of visionAwareRoutes) {
38
+ if (routeHasTargets(engine, engine.routing[routeName]) && !baseList.includes(routeName)) {
39
+ baseList.push(routeName);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ let ordered = sortByPriority(baseList);
45
+ if (features.hasImageAttachment && !forceVision && hasMultimodalTargets) {
46
+ ordered = reorderForInlineVision(engine, ordered);
47
+ }
48
+ if (features.hasImageAttachment && hasMultimodalTargets) {
49
+ ordered = reorderForPreferredModel(engine, ordered, 'kimi-k2.5');
50
+ }
51
+ const deduped = [];
52
+ for (const routeName of ordered) {
53
+ if (routeName && !deduped.includes(routeName)) {
54
+ deduped.push(routeName);
55
+ }
56
+ }
57
+ if (!deduped.includes(DEFAULT_ROUTE)) {
58
+ deduped.push(DEFAULT_ROUTE);
59
+ }
60
+ const filtered = deduped.filter((routeName) => routeHasTargets(engine, engine.routing[routeName]));
61
+ if (!filtered.includes(DEFAULT_ROUTE) && routeHasTargets(engine, engine.routing[DEFAULT_ROUTE])) {
62
+ filtered.push(DEFAULT_ROUTE);
63
+ }
64
+ return filtered.length ? filtered : [DEFAULT_ROUTE];
65
+ }
66
+ export function reorderForInlineVision(engine, routeNames) {
67
+ const unique = Array.from(new Set(routeNames.filter(Boolean)));
68
+ if (!unique.length) {
69
+ return unique;
70
+ }
71
+ // 仅当 default/thinking 中存在 Responses/Gemini 提供方时,才将其提前作为「一次完成」优先级。
72
+ const inlinePreferred = [];
73
+ const inlineRoutes = [DEFAULT_ROUTE, 'thinking'];
74
+ for (const routeName of inlineRoutes) {
75
+ if (routeSupportsInlineVision(engine, routeName) && !inlinePreferred.includes(routeName)) {
76
+ inlinePreferred.push(routeName);
77
+ }
78
+ }
79
+ if (!inlinePreferred.length) {
80
+ return unique;
81
+ }
82
+ const remaining = [];
83
+ for (const routeName of unique) {
84
+ if (!inlinePreferred.includes(routeName)) {
85
+ remaining.push(routeName);
86
+ }
87
+ }
88
+ return [...inlinePreferred, ...remaining];
89
+ }
90
+ export function reorderForPreferredModel(engine, routeNames, modelId) {
91
+ const unique = Array.from(new Set(routeNames.filter(Boolean)));
92
+ if (!unique.length) {
93
+ return unique;
94
+ }
95
+ const preferred = unique.filter((routeName) => routeSupportsModel(engine, routeName, modelId));
96
+ if (!preferred.length) {
97
+ return unique;
98
+ }
99
+ const remaining = unique.filter((routeName) => !preferred.includes(routeName));
100
+ return [...preferred, ...remaining];
101
+ }
102
+ export function routeSupportsModel(engine, routeName, modelId) {
103
+ const normalizedModel = modelId.trim().toLowerCase();
104
+ if (!normalizedModel) {
105
+ return false;
106
+ }
107
+ const pools = engine.routing[routeName];
108
+ if (!Array.isArray(pools)) {
109
+ return false;
110
+ }
111
+ for (const pool of pools) {
112
+ if (!Array.isArray(pool.targets)) {
113
+ continue;
114
+ }
115
+ for (const providerKey of pool.targets) {
116
+ try {
117
+ const profile = engine.providerRegistry.get(providerKey);
118
+ const candidate = typeof profile.modelId === 'string' ? profile.modelId.trim().toLowerCase() : '';
119
+ if (candidate === normalizedModel) {
120
+ return true;
121
+ }
122
+ }
123
+ catch {
124
+ // ignore unknown provider keys during capability probing
125
+ }
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ export function routeSupportsInlineVision(engine, routeName) {
131
+ const pools = engine.routing[routeName];
132
+ if (!Array.isArray(pools)) {
133
+ return false;
134
+ }
135
+ for (const pool of pools) {
136
+ if (!Array.isArray(pool.targets)) {
137
+ continue;
138
+ }
139
+ for (const providerKey of pool.targets) {
140
+ try {
141
+ const profile = engine.providerRegistry.get(providerKey);
142
+ if (profile.providerType === 'responses' || profile.providerType === 'gemini') {
143
+ return true;
144
+ }
145
+ }
146
+ catch {
147
+ // ignore unknown provider keys during capability probing
148
+ }
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+ export function sortByPriority(routeNames) {
154
+ return [...routeNames].sort((a, b) => routeWeight(a) - routeWeight(b));
155
+ }
156
+ export function routeWeight(routeName) {
157
+ const idx = ROUTE_PRIORITY.indexOf(routeName);
158
+ return idx >= 0 ? idx : ROUTE_PRIORITY.length;
159
+ }
160
+ export function routeHasForceFlag(engine, routeName) {
161
+ const pools = engine.routing[routeName];
162
+ if (!Array.isArray(pools)) {
163
+ return false;
164
+ }
165
+ return pools.some((pool) => pool.force);
166
+ }
167
+ export function routeHasTargets(engine, pools) {
168
+ if (!Array.isArray(pools)) {
169
+ return false;
170
+ }
171
+ return pools.some((pool) => Array.isArray(pool.targets) && pool.targets.length > 0);
172
+ }
173
+ export function hasPrimaryPool(engine, pools) {
174
+ if (!Array.isArray(pools)) {
175
+ return false;
176
+ }
177
+ return pools.some((pool) => !pool.backup && Array.isArray(pool.targets) && pool.targets.length > 0);
178
+ }
179
+ export function sortRoutePools(_engine, pools) {
180
+ if (!Array.isArray(pools)) {
181
+ return [];
182
+ }
183
+ return pools
184
+ .filter((pool) => Array.isArray(pool.targets) && pool.targets.length > 0)
185
+ .sort((a, b) => {
186
+ if (a.backup && !b.backup)
187
+ return 1;
188
+ if (!a.backup && b.backup)
189
+ return -1;
190
+ if (a.priority !== b.priority) {
191
+ return b.priority - a.priority;
192
+ }
193
+ return a.id.localeCompare(b.id);
194
+ });
195
+ }
196
+ export function flattenPoolTargets(_engine, pools) {
197
+ const flattened = [];
198
+ if (!Array.isArray(pools)) {
199
+ return flattened;
200
+ }
201
+ for (const pool of pools) {
202
+ if (!Array.isArray(pool.targets)) {
203
+ continue;
204
+ }
205
+ for (const target of pool.targets) {
206
+ if (typeof target === 'string' && target && !flattened.includes(target)) {
207
+ flattened.push(target);
208
+ }
209
+ }
210
+ }
211
+ return flattened;
212
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProcessedRequest, StandardizedRequest } from '../../../conversion/hub/types/standardized.js';
2
+ import type { RouterMetadataInput, RoutingDecision, RoutingDiagnostics, TargetMetadata } from '../types.js';
3
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
4
+ export declare function routeRequest(engine: VirtualRouterEngine, request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
5
+ target: TargetMetadata;
6
+ decision: RoutingDecision;
7
+ diagnostics: RoutingDiagnostics;
8
+ };
@@ -0,0 +1,8 @@
1
+ import { buildRoutingState } from './route-state.js';
2
+ import { selectRoutingTarget } from './route-selection.js';
3
+ import { finalizeRoutingDecision } from './route-finalize.js';
4
+ export function routeRequest(engine, request, metadata) {
5
+ const routingStateResult = buildRoutingState(engine, request, metadata);
6
+ const selectionResult = selectRoutingTarget(engine, request, metadata, routingStateResult.routingState, routingStateResult.stateKey);
7
+ return finalizeRoutingDecision(engine, metadata, selectionResult.routingState, routingStateResult.metadataInstructions, routingStateResult.instructions, selectionResult);
8
+ }
@@ -0,0 +1,28 @@
1
+ import type { ClassificationResult, RouterMetadataInput, RoutingFeatures } from '../types.js';
2
+ import type { RoutingInstructionState } from '../routing-instructions.js';
3
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
4
+ export declare function selectProvider(engine: VirtualRouterEngine, requestedRoute: string, metadata: RouterMetadataInput, classification: ClassificationResult, features: RoutingFeatures, routingState?: RoutingInstructionState): {
5
+ providerKey: string;
6
+ routeUsed: string;
7
+ pool: string[];
8
+ poolId?: string;
9
+ };
10
+ export declare function selectFromCandidates(engine: VirtualRouterEngine, routes: string[], metadata: RouterMetadataInput, classification: ClassificationResult, features: RoutingFeatures, state: RoutingInstructionState, requiredProviderKeys?: Set<string>, allowAliasRotation?: boolean): {
11
+ providerKey: string;
12
+ routeUsed: string;
13
+ pool: string[];
14
+ poolId?: string;
15
+ };
16
+ /**
17
+ * 在 sticky 模式下,仅在 sticky 池内选择 Provider:
18
+ * - stickyKeySet 表示已经解析并通过健康检查的 providerKey 集合;
19
+ * - 不再依赖 routing[*].targets 中是否挂载这些 key,避免「未初始化路由池」导致 sticky 池为空;
20
+ * - 仍然尊重 allowed/disabledProviders、disabledKeys、disabledModels 以及上下文长度。
21
+ */
22
+ export declare function selectFromStickyPool(engine: VirtualRouterEngine, stickyKeySet: Set<string>, metadata: RouterMetadataInput, features: RoutingFeatures, state: RoutingInstructionState, allowAliasRotation?: boolean): {
23
+ providerKey: string;
24
+ routeUsed: string;
25
+ pool: string[];
26
+ poolId?: string;
27
+ } | null;
28
+ export declare function extractExcludedProviderKeySet(engine: VirtualRouterEngine, metadata: RouterMetadataInput | undefined): Set<string>;
@@ -0,0 +1,112 @@
1
+ import { DEFAULT_ROUTE } from '../types.js';
2
+ import { getRoutingInstructionState } from '../engine/routing-state/store.js';
3
+ import { selectProviderImpl } from '../engine/routing-pools/index.js';
4
+ import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from '../engine/provider-key/parse.js';
5
+ export function selectProvider(engine, requestedRoute, metadata, classification, features, routingState) {
6
+ const activeState = routingState ||
7
+ getRoutingInstructionState(engine.resolveStickyKey(metadata), engine.routingInstructionState, engine.routingStateStore);
8
+ return selectProviderImpl(requestedRoute, metadata, classification, features, activeState, {
9
+ routing: engine.routing,
10
+ providerRegistry: engine.providerRegistry,
11
+ healthManager: engine.healthManager,
12
+ contextAdvisor: engine.contextAdvisor,
13
+ loadBalancer: engine.loadBalancer,
14
+ isProviderCoolingDown: (key) => engine.isProviderCoolingDown(key),
15
+ getProviderCooldownRemainingMs: (key) => engine.getProviderCooldownRemainingMs(key),
16
+ resolveStickyKey: (m) => engine.resolveStickyKey(m),
17
+ quotaView: engine.quotaView,
18
+ aliasQueueStore: engine.stickySessionManager.getAllStores().aliasQueueStore,
19
+ antigravityAliasLeaseStore: engine.stickySessionManager.getAllStores().aliasLeaseStore,
20
+ antigravitySessionAliasStore: engine.stickySessionManager.getAllStores().sessionAliasStore,
21
+ antigravityAliasReuseCooldownMs: engine.stickySessionManager.getAliasReuseCooldownMs()
22
+ }, { routingState });
23
+ }
24
+ export function selectFromCandidates(engine, routes, metadata, classification, features, state, requiredProviderKeys, allowAliasRotation) {
25
+ // legacy helper kept for backward compatibility; selection logic moved to engine-selection.ts
26
+ return selectProviderImpl(engine.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE), metadata, classification, features, state, {
27
+ routing: engine.routing,
28
+ providerRegistry: engine.providerRegistry,
29
+ healthManager: engine.healthManager,
30
+ contextAdvisor: engine.contextAdvisor,
31
+ loadBalancer: engine.loadBalancer,
32
+ isProviderCoolingDown: (key) => engine.isProviderCoolingDown(key),
33
+ getProviderCooldownRemainingMs: (key) => engine.getProviderCooldownRemainingMs(key),
34
+ resolveStickyKey: (m) => engine.resolveStickyKey(m),
35
+ quotaView: engine.quotaView,
36
+ aliasQueueStore: engine.stickySessionManager.getAllStores().aliasQueueStore
37
+ }, { routingState: state });
38
+ }
39
+ /**
40
+ * 在 sticky 模式下,仅在 sticky 池内选择 Provider:
41
+ * - stickyKeySet 表示已经解析并通过健康检查的 providerKey 集合;
42
+ * - 不再依赖 routing[*].targets 中是否挂载这些 key,避免「未初始化路由池」导致 sticky 池为空;
43
+ * - 仍然尊重 allowed/disabledProviders、disabledKeys、disabledModels 以及上下文长度。
44
+ */
45
+ export function selectFromStickyPool(engine, stickyKeySet, metadata, features, state, allowAliasRotation) {
46
+ if (!stickyKeySet || stickyKeySet.size === 0) {
47
+ return null;
48
+ }
49
+ const allowedProviders = new Set(state.allowedProviders);
50
+ const disabledProviders = new Set(state.disabledProviders);
51
+ const disabledKeysMap = new Map(Array.from(state.disabledKeys.entries()).map(([provider, keys]) => [
52
+ provider,
53
+ new Set(Array.from(keys).map((k) => (typeof k === 'string' ? k : k + 1)))
54
+ ]));
55
+ const disabledModels = new Map(Array.from(state.disabledModels.entries()).map(([provider, models]) => [provider, new Set(models)]));
56
+ // 初始候选集合:sticky 池中的所有 key
57
+ // In quota routing mode, cooldown is controlled by quotaView only.
58
+ let candidates = Array.from(stickyKeySet).filter((key) => (engine.quotaView ? true : !engine.isProviderCoolingDown(key)));
59
+ // 应用 provider 白名单 / 黑名单
60
+ if (allowedProviders.size > 0) {
61
+ candidates = candidates.filter((key) => {
62
+ const providerId = extractProviderId(key);
63
+ return providerId && allowedProviders.has(providerId);
64
+ });
65
+ }
66
+ if (disabledProviders.size > 0) {
67
+ candidates = candidates.filter((key) => {
68
+ const providerId = extractProviderId(key);
69
+ return providerId && !disabledProviders.has(providerId);
70
+ });
71
+ }
72
+ // 应用 key / model 级别黑名单
73
+ if (disabledKeysMap.size > 0 || disabledModels.size > 0) {
74
+ candidates = candidates.filter((key) => {
75
+ const providerId = extractProviderId(key);
76
+ if (!providerId) {
77
+ return true;
78
+ }
79
+ const disabledKeys = disabledKeysMap.get(providerId);
80
+ if (disabledKeys && disabledKeys.size > 0) {
81
+ const keyAlias = extractKeyAlias(key);
82
+ const keyIndex = extractKeyIndex(key);
83
+ if (keyAlias && disabledKeys.has(keyAlias)) {
84
+ return false;
85
+ }
86
+ if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
87
+ return false;
88
+ }
89
+ }
90
+ const disabledModelSet = disabledModels.get(providerId);
91
+ if (disabledModelSet && disabledModelSet.size > 0) {
92
+ const modelId = getProviderModelId(key, engine.providerRegistry);
93
+ if (modelId && disabledModelSet.has(modelId)) {
94
+ return false;
95
+ }
96
+ }
97
+ return true;
98
+ });
99
+ }
100
+ if (!candidates.length) {
101
+ return null;
102
+ }
103
+ const stickyKey = allowAliasRotation ? undefined : engine.resolveStickyKey(metadata);
104
+ const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
105
+ ? Math.max(0, features.estimatedTokens)
106
+ : 0;
107
+ // delegate to selection module
108
+ return null;
109
+ }
110
+ export function extractExcludedProviderKeySet(engine, metadata) {
111
+ return engine.routeAnalytics.extractExcludedProviderKeySet(metadata);
112
+ }
@@ -0,0 +1,16 @@
1
+ import type { RoutingInstructionState } from '../routing-instructions.js';
2
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
3
+ export declare function resolveSelectionPenalty(engine: VirtualRouterEngine, providerKey: string): number | undefined;
4
+ export declare function resolveInstructionProcessModeForSelection(engine: VirtualRouterEngine, providerKey: string, routingState: RoutingInstructionState): 'chat' | 'passthrough' | undefined;
5
+ export declare function resolveInstructionTarget(engine: VirtualRouterEngine, target: NonNullable<RoutingInstructionState['forcedTarget']>): {
6
+ mode: 'exact' | 'filter';
7
+ keys: string[];
8
+ } | null;
9
+ export declare function filterCandidatesByRoutingState(engine: VirtualRouterEngine, routes: string[], state: RoutingInstructionState): string[];
10
+ /**
11
+ * 在已有候选路由集合上,筛选出真正挂载了 sticky 池内 providerKey 的路由,
12
+ * 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
13
+ * sticky 模式,就不会再命中独立的 tools 池(例如 glm/qwen 工具模型)。
14
+ * 若候选集合中完全没有挂载 sticky key 的路由,则尝试在 default 路由上兜底。
15
+ */
16
+ export declare function buildStickyRouteCandidatesFromFiltered(engine: VirtualRouterEngine, filteredCandidates: string[], stickyKeySet: Set<string>): string[];
@@ -0,0 +1,187 @@
1
+ import { DEFAULT_ROUTE } from '../types.js';
2
+ import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from '../engine/provider-key/parse.js';
3
+ import { routeHasTargets, sortByPriority, flattenPoolTargets } from './route-utils.js';
4
+ export function resolveSelectionPenalty(engine, providerKey) {
5
+ if (!engine.quotaView) {
6
+ return undefined;
7
+ }
8
+ try {
9
+ const entry = engine.quotaView(providerKey);
10
+ const raw = entry?.selectionPenalty;
11
+ if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) {
12
+ return undefined;
13
+ }
14
+ return Math.floor(raw);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ export function resolveInstructionProcessModeForSelection(engine, providerKey, routingState) {
21
+ const candidates = [
22
+ routingState.forcedTarget,
23
+ routingState.stickyTarget,
24
+ routingState.preferTarget
25
+ ];
26
+ for (const candidate of candidates) {
27
+ const processMode = candidate?.processMode;
28
+ if (!processMode) {
29
+ continue;
30
+ }
31
+ const resolved = resolveInstructionTarget(engine, candidate);
32
+ if (!resolved) {
33
+ continue;
34
+ }
35
+ if (resolved.keys.includes(providerKey)) {
36
+ return processMode;
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+ export function resolveInstructionTarget(engine, target) {
42
+ if (!target || !target.provider) {
43
+ return null;
44
+ }
45
+ const providerId = target.provider;
46
+ const providerKeys = engine.providerRegistry.listProviderKeys(providerId);
47
+ if (providerKeys.length === 0) {
48
+ return null;
49
+ }
50
+ const alias = typeof target.keyAlias === 'string' ? target.keyAlias.trim() : '';
51
+ const aliasExplicit = alias.length > 0 && target.pathLength === 3;
52
+ if (aliasExplicit) {
53
+ const runtimeKey = engine.providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
54
+ if (runtimeKey) {
55
+ return { mode: 'exact', keys: [runtimeKey] };
56
+ }
57
+ }
58
+ if (typeof target.keyIndex === 'number' && target.keyIndex > 0) {
59
+ const runtimeKey = engine.providerRegistry.resolveRuntimeKeyByIndex(providerId, target.keyIndex);
60
+ if (runtimeKey) {
61
+ return { mode: 'exact', keys: [runtimeKey] };
62
+ }
63
+ }
64
+ if (target.model && target.model.trim()) {
65
+ const normalizedModel = target.model.trim();
66
+ const matchingKeys = providerKeys.filter((key) => {
67
+ const modelId = getProviderModelId(key, engine.providerRegistry);
68
+ return modelId === normalizedModel;
69
+ });
70
+ if (matchingKeys.length > 0) {
71
+ return { mode: 'filter', keys: matchingKeys };
72
+ }
73
+ }
74
+ if (alias && !aliasExplicit) {
75
+ const legacyKey = engine.providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
76
+ if (legacyKey) {
77
+ return { mode: 'exact', keys: [legacyKey] };
78
+ }
79
+ }
80
+ return { mode: 'filter', keys: providerKeys };
81
+ }
82
+ export function filterCandidatesByRoutingState(engine, routes, state) {
83
+ // console.log('[filter] routes:', routes, 'state:', {
84
+ // allowed: Array.from(state.allowedProviders),
85
+ // disabled: Array.from(state.disabledProviders)
86
+ // });
87
+ if (state.allowedProviders.size === 0 &&
88
+ state.disabledProviders.size === 0 &&
89
+ state.disabledKeys.size === 0 &&
90
+ state.disabledModels.size === 0) {
91
+ return routes;
92
+ }
93
+ return routes.filter((routeName) => {
94
+ const pools = engine.routing[routeName];
95
+ if (!pools)
96
+ return false;
97
+ for (const pool of pools) {
98
+ if (!Array.isArray(pool.targets) || pool.targets.length === 0) {
99
+ continue;
100
+ }
101
+ for (const providerKey of pool.targets) {
102
+ const providerId = extractProviderId(providerKey);
103
+ // console.log('[filter] checking', providerKey, 'id=', providerId);
104
+ if (!providerId)
105
+ continue;
106
+ if (state.allowedProviders.size > 0 && !state.allowedProviders.has(providerId)) {
107
+ // console.log('[filter] dropped by allowed list');
108
+ continue;
109
+ }
110
+ if (state.disabledProviders.has(providerId)) {
111
+ continue;
112
+ }
113
+ const disabledKeys = state.disabledKeys.get(providerId);
114
+ if (disabledKeys && disabledKeys.size > 0) {
115
+ const keyAlias = extractKeyAlias(providerKey);
116
+ const keyIndex = extractKeyIndex(providerKey);
117
+ if (keyAlias && disabledKeys.has(keyAlias)) {
118
+ continue;
119
+ }
120
+ if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
121
+ continue;
122
+ }
123
+ }
124
+ const disabledModels = state.disabledModels.get(providerId);
125
+ if (disabledModels && disabledModels.size > 0) {
126
+ const modelId = getProviderModelId(providerKey, engine.providerRegistry);
127
+ if (modelId && disabledModels.has(modelId)) {
128
+ continue;
129
+ }
130
+ }
131
+ return true;
132
+ }
133
+ }
134
+ return false;
135
+ });
136
+ }
137
+ /**
138
+ * 在已有候选路由集合上,筛选出真正挂载了 sticky 池内 providerKey 的路由,
139
+ * 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
140
+ * sticky 模式,就不会再命中独立的 tools 池(例如 glm/qwen 工具模型)。
141
+ * 若候选集合中完全没有挂载 sticky key 的路由,则尝试在 default 路由上兜底。
142
+ */
143
+ export function buildStickyRouteCandidatesFromFiltered(engine, filteredCandidates, stickyKeySet) {
144
+ const routesWithSticky = [];
145
+ const candidateSet = new Set(filteredCandidates.filter((name) => name && name !== 'tools'));
146
+ for (const routeName of candidateSet) {
147
+ const pools = engine.routing[routeName];
148
+ if (!routeHasTargets(engine, pools)) {
149
+ continue;
150
+ }
151
+ const targets = flattenPoolTargets(engine, pools);
152
+ if (!targets.some((key) => stickyKeySet.has(key))) {
153
+ continue;
154
+ }
155
+ routesWithSticky.push(routeName);
156
+ }
157
+ // 若当前候选路由中没有任何挂载 sticky key 的路由,尝试直接在 default 路由上兜底;
158
+ // 若 default 也不包含 sticky key,则视为 sticky 配置失效,由调用方回落到非 sticky 逻辑。
159
+ if (routesWithSticky.length === 0) {
160
+ const defaultPools = engine.routing[DEFAULT_ROUTE];
161
+ if (routeHasTargets(engine, defaultPools)) {
162
+ const targets = flattenPoolTargets(engine, defaultPools);
163
+ if (targets.some((key) => stickyKeySet.has(key))) {
164
+ return [DEFAULT_ROUTE];
165
+ }
166
+ }
167
+ return [];
168
+ }
169
+ const ordered = sortByPriority(routesWithSticky);
170
+ const result = [];
171
+ let hasDefault = false;
172
+ for (const routeName of ordered) {
173
+ if (routeName === DEFAULT_ROUTE) {
174
+ hasDefault = true;
175
+ continue;
176
+ }
177
+ if (!result.includes(routeName)) {
178
+ result.push(routeName);
179
+ }
180
+ }
181
+ // default 路由若包含 sticky key,则始终放在候选列表最后,用于 sticky 模式兜底。
182
+ if (hasDefault && !result.includes(DEFAULT_ROUTE)) {
183
+ result.push(DEFAULT_ROUTE);
184
+ }
185
+ return result;
186
+ }
187
+ // Intentionally no extra exports here; keep this module focused on state filtering.
@@ -0,0 +1,21 @@
1
+ import type { PreCommandStateSnapshot, RouterMetadataInput, StopMessageStateSnapshot } from '../types.js';
2
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
3
+ export declare function getStopMessageState(engine: VirtualRouterEngine, metadata: RouterMetadataInput): StopMessageStateSnapshot | null;
4
+ export declare function getPreCommandState(engine: VirtualRouterEngine, metadata: RouterMetadataInput): PreCommandStateSnapshot | null;
5
+ export declare function getStatus(engine: VirtualRouterEngine): {
6
+ routes: Record<string, {
7
+ providers: string[];
8
+ hits: number;
9
+ lastUsedProvider?: string;
10
+ lastHit?: {
11
+ timestampMs: number;
12
+ reason?: string;
13
+ requestTokens?: number;
14
+ selectionPenalty?: number;
15
+ stopMessageActive: boolean;
16
+ stopMessageMode?: "on" | "off" | "auto";
17
+ stopMessageRemaining?: number;
18
+ };
19
+ }>;
20
+ health: import("../types.js").ProviderHealthState[];
21
+ };