@jsonstudio/llms 0.6.2979 → 0.6.3238

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 (246) 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/archive/chat-mapper.archive.d.ts +8 -0
  28. package/dist/conversion/hub/operation-table/semantic-mappers/archive/chat-mapper.archive.js +404 -0
  29. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +5 -381
  30. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +2 -2
  31. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +2 -8
  32. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline.js +50 -3
  34. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +1 -1
  35. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +62 -0
  36. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +3 -1
  37. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +1 -1
  38. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/chat-process-semantics-bridge.d.ts +1 -1
  39. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +42 -29
  40. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +12 -0
  41. package/dist/conversion/hub/policy/protocol-spec.js +1 -1
  42. package/dist/conversion/hub/process/chat-process-clock-reminders.js +1 -1
  43. package/dist/conversion/hub/process/chat-process-clock-tools.js +1 -1
  44. package/dist/conversion/hub/process/chat-process-continue-execution.js +1 -1
  45. package/dist/conversion/hub/process/chat-process-servertool-orchestration.js +1 -1
  46. package/dist/conversion/hub/process/chat-process-web-search.js +1 -1
  47. package/dist/conversion/hub/response/provider-response.js +14 -5
  48. package/dist/conversion/hub/response/response-mappers.js +23 -1
  49. package/dist/conversion/hub/response/response-runtime.js +28 -5
  50. package/dist/conversion/hub/snapshot-recorder.js +3 -92
  51. package/dist/conversion/hub/tool-governance/engine.d.ts +8 -0
  52. package/dist/conversion/hub/tool-governance/engine.js +40 -193
  53. package/dist/conversion/hub/tool-governance/rules.js +73 -69
  54. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +1 -1
  55. package/dist/conversion/index.d.ts +1 -2
  56. package/dist/conversion/index.js +1 -2
  57. package/dist/conversion/{shared/jsonish.js → jsonish.js} +1 -1
  58. package/dist/conversion/{shared/mcp-injection.js → mcp-injection.js} +1 -1
  59. package/dist/conversion/media.js +4 -0
  60. package/dist/conversion/{shared/metadata-passthrough.d.ts → metadata-passthrough.d.ts} +1 -1
  61. package/dist/conversion/{shared/metadata-passthrough.js → metadata-passthrough.js} +2 -2
  62. package/dist/conversion/payload-budget.js +47 -0
  63. package/dist/conversion/protocol-field-allowlists.d.ts +7 -0
  64. package/dist/conversion/protocol-field-allowlists.js +9 -0
  65. package/dist/conversion/{shared/protocol-state.d.ts → protocol-state.d.ts} +2 -2
  66. package/dist/conversion/{shared/protocol-state.js → protocol-state.js} +2 -2
  67. package/dist/conversion/{shared/errors.d.ts → provider-protocol-error.d.ts} +0 -3
  68. package/dist/conversion/provider-protocol-error.js +25 -0
  69. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +8 -5
  70. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +1 -1
  71. package/dist/conversion/responses/responses-openai-bridge.d.ts +1 -1
  72. package/dist/conversion/responses/responses-openai-bridge.js +43 -10
  73. package/dist/conversion/{shared/runtime-metadata.d.ts → runtime-metadata.d.ts} +1 -1
  74. package/dist/conversion/{shared/runtime-metadata.js → runtime-metadata.js} +2 -2
  75. package/dist/conversion/shared/anthropic-message-utils.js +19 -8
  76. package/dist/conversion/shared/chat-request-filters.d.ts +3 -4
  77. package/dist/conversion/shared/chat-request-filters.js +22 -78
  78. package/dist/conversion/shared/gemini-tool-utils.d.ts +1 -1
  79. package/dist/conversion/shared/openai-finalizer.js +1 -0
  80. package/dist/conversion/shared/openai-message-normalize.js +2 -2
  81. package/dist/conversion/shared/reasoning-normalizer.js +6 -0
  82. package/dist/conversion/shared/reasoning-utils.js +5 -2
  83. package/dist/conversion/shared/responses-conversation-store.js +1 -1
  84. package/dist/conversion/shared/responses-output-builder.js +55 -11
  85. package/dist/conversion/shared/responses-reasoning-registry.d.ts +14 -2
  86. package/dist/conversion/shared/responses-reasoning-registry.js +34 -6
  87. package/dist/conversion/shared/responses-response-utils.js +99 -9
  88. package/dist/conversion/shared/responses-tool-utils.js +1 -1
  89. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +1 -1
  90. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -2
  91. package/dist/conversion/shared/text-markup-normalizer.js +1 -1
  92. package/dist/conversion/shared/tool-filter-pipeline.js +1 -1
  93. package/dist/conversion/shared/tool-governor.js +3 -3
  94. package/dist/conversion/shared/tool-mapping.d.ts +1 -1
  95. package/dist/conversion/{shared/snapshot-utils.d.ts → snapshot-utils.d.ts} +11 -0
  96. package/dist/conversion/{shared/snapshot-utils.js → snapshot-utils.js} +14 -23
  97. package/dist/conversion/types/text-markup-normalizer.d.ts +13 -0
  98. package/dist/conversion/types/text-markup-normalizer.js +1 -0
  99. package/dist/filters/special/request-tools-normalize.js +1 -1
  100. package/dist/filters/special/response-tool-text-canonicalize.js +2 -2
  101. package/dist/native/router_hotpath_napi.node +0 -0
  102. package/dist/quota/quota-manager.js +31 -59
  103. package/dist/quota/quota-state.js +14 -7
  104. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +1 -0
  105. package/dist/router/virtual-router/bootstrap/profile-builder.js +13 -0
  106. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +2 -0
  107. package/dist/router/virtual-router/bootstrap/provider-normalization.js +4 -1
  108. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +7 -0
  109. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +44 -0
  110. package/dist/router/virtual-router/bootstrap.js +2 -0
  111. package/dist/router/virtual-router/engine/routing-state/store.d.ts +1 -2
  112. package/dist/router/virtual-router/engine/routing-state/store.js +2 -2
  113. package/dist/router/virtual-router/engine-legacy/config.d.ts +11 -0
  114. package/dist/router/virtual-router/engine-legacy/config.js +108 -0
  115. package/dist/router/virtual-router/engine-legacy/direct-model.d.ts +10 -0
  116. package/dist/router/virtual-router/engine-legacy/direct-model.js +38 -0
  117. package/dist/router/virtual-router/engine-legacy/health.d.ts +13 -0
  118. package/dist/router/virtual-router/engine-legacy/health.js +104 -0
  119. package/dist/router/virtual-router/engine-legacy/helpers.d.ts +16 -0
  120. package/dist/router/virtual-router/engine-legacy/helpers.js +226 -0
  121. package/dist/router/virtual-router/engine-legacy/route-finalize.d.ts +9 -0
  122. package/dist/router/virtual-router/engine-legacy/route-finalize.js +84 -0
  123. package/dist/router/virtual-router/engine-legacy/route-selection.d.ts +17 -0
  124. package/dist/router/virtual-router/engine-legacy/route-selection.js +205 -0
  125. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.d.ts +3 -0
  126. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.js +36 -0
  127. package/dist/router/virtual-router/engine-legacy/route-state.d.ts +12 -0
  128. package/dist/router/virtual-router/engine-legacy/route-state.js +386 -0
  129. package/dist/router/virtual-router/engine-legacy/route-utils.d.ts +19 -0
  130. package/dist/router/virtual-router/engine-legacy/route-utils.js +212 -0
  131. package/dist/router/virtual-router/engine-legacy/routing.d.ts +8 -0
  132. package/dist/router/virtual-router/engine-legacy/routing.js +8 -0
  133. package/dist/router/virtual-router/engine-legacy/selection-core.d.ts +28 -0
  134. package/dist/router/virtual-router/engine-legacy/selection-core.js +112 -0
  135. package/dist/router/virtual-router/engine-legacy/selection-state.d.ts +16 -0
  136. package/dist/router/virtual-router/engine-legacy/selection-state.js +187 -0
  137. package/dist/router/virtual-router/engine-legacy/state-accessors.d.ts +21 -0
  138. package/dist/router/virtual-router/engine-legacy/state-accessors.js +118 -0
  139. package/dist/router/virtual-router/engine-legacy.d.ts +123 -0
  140. package/dist/router/virtual-router/engine-legacy.js +194 -0
  141. package/dist/router/virtual-router/engine-logging.d.ts +2 -0
  142. package/dist/router/virtual-router/engine-logging.js +7 -2
  143. package/dist/router/virtual-router/engine-selection/key-parsing.js +0 -3
  144. package/dist/router/virtual-router/engine-selection/native-chat-request-filter-semantics.d.ts +1 -0
  145. package/dist/router/virtual-router/engine-selection/native-chat-request-filter-semantics.js +54 -0
  146. package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.d.ts +10 -0
  147. package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.js +67 -0
  148. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.d.ts +30 -0
  149. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js +202 -0
  150. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-semantic-mappers.d.ts +2 -0
  151. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-semantic-mappers.js +83 -0
  152. package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +43 -2
  153. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +75 -0
  154. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +205 -0
  155. package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.d.ts +3 -0
  156. package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.js +109 -0
  157. package/dist/router/virtual-router/engine-selection/native-virtual-router-engine-proxy.d.ts +16 -0
  158. package/dist/router/virtual-router/engine-selection/native-virtual-router-engine-proxy.js +14 -0
  159. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.d.ts +2 -0
  160. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.js +86 -0
  161. package/dist/router/virtual-router/engine-selection/tier-selection-quota-integration.js +100 -0
  162. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +99 -0
  163. package/dist/router/virtual-router/engine.d.ts +22 -105
  164. package/dist/router/virtual-router/engine.js +274 -1641
  165. package/dist/router/virtual-router/load-balancer.d.ts +8 -0
  166. package/dist/router/virtual-router/load-balancer.js +65 -2
  167. package/dist/router/virtual-router/provider-registry.js +2 -0
  168. package/dist/router/virtual-router/routing-instructions/clean.d.ts +3 -0
  169. package/dist/router/virtual-router/routing-instructions/clean.js +34 -0
  170. package/dist/router/virtual-router/routing-instructions/parse.d.ts +18 -0
  171. package/dist/router/virtual-router/routing-instructions/parse.js +377 -0
  172. package/dist/router/virtual-router/routing-instructions/state.d.ts +4 -0
  173. package/dist/router/virtual-router/routing-instructions/state.js +245 -0
  174. package/dist/router/virtual-router/routing-instructions/types.d.ts +70 -0
  175. package/dist/router/virtual-router/routing-instructions/types.js +2 -0
  176. package/dist/router/virtual-router/routing-instructions.d.ts +5 -89
  177. package/dist/router/virtual-router/routing-instructions.js +4 -655
  178. package/dist/router/virtual-router/sticky-session-store.d.ts +4 -0
  179. package/dist/router/virtual-router/sticky-session-store.js +19 -81
  180. package/dist/router/virtual-router/tool-signals.js +21 -3
  181. package/dist/router/virtual-router/types.d.ts +4 -0
  182. package/dist/servertool/clock/session-scope.js +32 -1
  183. package/dist/servertool/engine.js +79 -8
  184. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +1 -1
  185. package/dist/servertool/handlers/clock-auto.js +1 -1
  186. package/dist/servertool/handlers/clock.js +1 -1
  187. package/dist/servertool/handlers/compaction-detect.d.ts +1 -1
  188. package/dist/servertool/handlers/compaction-detect.js +1 -1
  189. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  190. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  191. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  192. package/dist/servertool/handlers/review.js +1 -1
  193. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +1 -1
  194. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +1 -1
  195. package/dist/servertool/handlers/stop-message-auto.js +1 -1
  196. package/dist/servertool/handlers/vision.js +1 -1
  197. package/dist/servertool/handlers/web-search.js +1 -1
  198. package/dist/servertool/reenter-backend.js +1 -1
  199. package/dist/servertool/server-side-tools.js +2 -2
  200. package/dist/servertool/stop-gateway-context.js +1 -1
  201. package/dist/servertool/stop-message-compare-context.js +1 -1
  202. package/dist/sse/json-to-sse/event-generators/responses.d.ts +4 -0
  203. package/dist/sse/json-to-sse/event-generators/responses.js +95 -1
  204. package/dist/sse/json-to-sse/sequencers/responses-sequencer.js +6 -4
  205. package/dist/sse/sse-to-json/builders/response-builder.d.ts +8 -0
  206. package/dist/sse/sse-to-json/builders/response-builder.js +162 -4
  207. package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +2 -0
  208. package/dist/sse/types/responses-types.d.ts +6 -2
  209. package/dist/tools/apply-patch/structured/coercion.js +5 -0
  210. package/dist/tools/args-json.js +29 -0
  211. package/package.json +8 -5
  212. package/dist/conversion/shared/args-mapping.js +0 -77
  213. package/dist/conversion/shared/compaction-detect.js +0 -4
  214. package/dist/conversion/shared/errors.js +0 -31
  215. package/dist/conversion/shared/media.js +0 -4
  216. package/dist/conversion/shared/payload-budget.js +0 -165
  217. package/dist/conversion/shared/protocol-field-allowlists.d.ts +0 -7
  218. package/dist/conversion/shared/protocol-field-allowlists.js +0 -149
  219. package/dist/conversion/shared/snapshot-hooks.d.ts +0 -11
  220. package/dist/conversion/shared/snapshot-hooks.js +0 -503
  221. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +0 -2
  222. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +0 -129
  223. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +0 -4
  224. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +0 -637
  225. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +0 -21
  226. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +0 -177
  227. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +0 -5
  228. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +0 -385
  229. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +0 -10
  230. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +0 -602
  231. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +0 -5
  232. package/dist/conversion/shared/text-markup-normalizer/extractors.js +0 -4
  233. package/dist/conversion/shared/tool-canonicalizer.d.ts +0 -2
  234. package/dist/conversion/shared/tool-canonicalizer.js +0 -38
  235. /package/dist/conversion/{shared/args-mapping.d.ts → args-mapping.d.ts} +0 -0
  236. /package/dist/conversion/{shared/bridge-actions.d.ts → bridge-actions.d.ts} +0 -0
  237. /package/dist/conversion/{shared/bridge-id-utils.d.ts → bridge-id-utils.d.ts} +0 -0
  238. /package/dist/conversion/{shared/bridge-instructions.d.ts → bridge-instructions.d.ts} +0 -0
  239. /package/dist/conversion/{shared/bridge-metadata.d.ts → bridge-metadata.d.ts} +0 -0
  240. /package/dist/conversion/{shared/bridge-policies.d.ts → bridge-policies.d.ts} +0 -0
  241. /package/dist/conversion/{shared/jsonish.d.ts → jsonish.d.ts} +0 -0
  242. /package/dist/conversion/{shared/mcp-injection.d.ts → mcp-injection.d.ts} +0 -0
  243. /package/dist/conversion/{shared/media.d.ts → media.d.ts} +0 -0
  244. /package/dist/conversion/{shared/payload-budget.d.ts → payload-budget.d.ts} +0 -0
  245. /package/dist/conversion/{shared → types}/bridge-message-types.d.ts +0 -0
  246. /package/dist/conversion/{shared → types}/bridge-message-types.js +0 -0
@@ -0,0 +1,386 @@
1
+ import { applyRoutingInstructions, cleanMessagesFromRoutingInstructions, parseRoutingInstructions } from '../routing-instructions.js';
2
+ import { buildMetadataInstructions } from '../engine/routing-state/metadata.js';
3
+ import { getRoutingInstructionState, persistRoutingInstructionState, resolveStopMessageScope } from '../engine/routing-state/store.js';
4
+ import { ensureStopMessageModeMaxRepeats } from '../routing-stop-message-state-codec.js';
5
+ import { extractMessageText } from '../message-utils.js';
6
+ import { cleanResponsesContextFromRoutingInstructions, getLatestUserTextFromResponsesContext, hasLatestUserRoutingInstructionMarker, hasRoutingInstructionMarker, hasRoutingInstructionMarkerInResponsesContext, isServerToolFollowupRequest, isStopScopeTraceEnabled, normalizeStopMessageAiMode, normalizeStopMessageStageMode, stripClientInjectScopedFields, stripStopMessageFields } from './helpers.js';
7
+ import { enforceAllowlistIntersection } from './route-state-allowlist.js';
8
+ export function buildRoutingState(engine, request, metadata) {
9
+ const stickyKey = engine.resolveStickyKey(metadata);
10
+ const sessionScope = engine.resolveSessionScope(metadata);
11
+ const stopMessageScope = resolveStopMessageScope(metadata);
12
+ // Route sticky state remains session/request scoped for routing behavior,
13
+ // but stopMessage state is tmux/clockd scoped and merged separately below.
14
+ const stateKey = sessionScope || stickyKey || 'default';
15
+ const baseState = getRoutingInstructionState(stateKey, engine.routingInstructionState, engine.routingStateStore);
16
+ let routingState = stripStopMessageFields(baseState);
17
+ const metadataInstructions = buildMetadataInstructions(metadata);
18
+ if (metadataInstructions.length > 0) {
19
+ routingState = applyRoutingInstructions(metadataInstructions, routingState);
20
+ }
21
+ const disableStickyRoutes = metadata &&
22
+ typeof metadata === 'object' &&
23
+ metadata.disableStickyRoutes === true;
24
+ if (disableStickyRoutes && (routingState.stickyTarget || routingState.preferTarget)) {
25
+ routingState = {
26
+ ...routingState,
27
+ stickyTarget: undefined,
28
+ preferTarget: undefined
29
+ };
30
+ }
31
+ if (stopMessageScope) {
32
+ const sessionState = getRoutingInstructionState(stopMessageScope, engine.routingInstructionState, engine.routingStateStore);
33
+ if (ensureStopMessageModeMaxRepeats(sessionState)) {
34
+ engine.routingInstructionState.set(stopMessageScope, sessionState);
35
+ persistRoutingInstructionState(stopMessageScope, sessionState, engine.routingStateStore);
36
+ }
37
+ if (typeof sessionState.stopMessageText === 'string' ||
38
+ typeof sessionState.stopMessageMaxRepeats === 'number' ||
39
+ typeof sessionState.stopMessageStageMode === 'string' ||
40
+ typeof sessionState.stopMessageAiMode === 'string' ||
41
+ typeof sessionState.stopMessageAiSeedPrompt === 'string' ||
42
+ Array.isArray(sessionState.stopMessageAiHistory)) {
43
+ routingState = {
44
+ ...routingState,
45
+ stopMessageText: sessionState.stopMessageText,
46
+ stopMessageMaxRepeats: sessionState.stopMessageMaxRepeats,
47
+ stopMessageUsed: sessionState.stopMessageUsed,
48
+ stopMessageUpdatedAt: sessionState.stopMessageUpdatedAt,
49
+ stopMessageLastUsedAt: sessionState.stopMessageLastUsedAt,
50
+ stopMessageStageMode: sessionState.stopMessageStageMode,
51
+ stopMessageAiMode: sessionState.stopMessageAiMode,
52
+ stopMessageAiSeedPrompt: sessionState.stopMessageAiSeedPrompt,
53
+ stopMessageAiHistory: sessionState.stopMessageAiHistory
54
+ };
55
+ }
56
+ if (typeof sessionState.preCommandScriptPath === 'string' && sessionState.preCommandScriptPath.trim()) {
57
+ routingState = {
58
+ ...routingState,
59
+ preCommandSource: sessionState.preCommandSource,
60
+ preCommandScriptPath: sessionState.preCommandScriptPath,
61
+ preCommandUpdatedAt: sessionState.preCommandUpdatedAt
62
+ };
63
+ }
64
+ }
65
+ let parsedInstructions = parseRoutingInstructions(request.messages);
66
+ const responsesContext = request.semantics && typeof request.semantics === 'object'
67
+ ? request.semantics.responses?.context
68
+ : undefined;
69
+ const responsesLatestUserText = getLatestUserTextFromResponsesContext(responsesContext);
70
+ const responsesHasMarker = hasRoutingInstructionMarkerInResponsesContext(responsesContext);
71
+ if (!parsedInstructions.length && responsesHasMarker) {
72
+ parsedInstructions = parseRoutingInstructions([
73
+ { role: 'user', content: responsesLatestUserText }
74
+ ]);
75
+ }
76
+ const markerDetected = hasRoutingInstructionMarker(request.messages) || responsesHasMarker;
77
+ const latestMessage = request.messages.length > 0 ? request.messages[request.messages.length - 1] : undefined;
78
+ const latestUserText = latestMessage && latestMessage.role === 'user' ? extractMessageText(latestMessage).trim() : '';
79
+ const stopMessageLogText = responsesHasMarker ? responsesLatestUserText : latestUserText;
80
+ const containsStopMessage = Boolean(stopMessageLogText && /stopmessage/i.test(stopMessageLogText));
81
+ const stopMessageTypes = parsedInstructions
82
+ .filter((entry) => entry.type === 'stopMessageSet' ||
83
+ entry.type === 'stopMessageMode' ||
84
+ entry.type === 'stopMessageClear')
85
+ .map((entry) => entry.type);
86
+ if (containsStopMessage || stopMessageTypes.length > 0) {
87
+ const preview = stopMessageLogText.replace(/\s+/g, ' ').slice(0, 120);
88
+ engine.debug?.log?.(`[virtual-router][stop_message_parse] requestId=${metadata.requestId || 'n/a'} marker=${markerDetected ? 'detected' : 'missing'} parsed=${stopMessageTypes.join(',') || 'none'} preview=${preview}`);
89
+ }
90
+ if (markerDetected && isStopScopeTraceEnabled()) {
91
+ const parsedTypes = parsedInstructions.map((entry) => entry.type);
92
+ engine.debug?.log?.(`[virtual-router][instruction_parse] requestId=${metadata.requestId || 'n/a'} marker=detected parsed=${parsedTypes.join(',') || 'none'}`);
93
+ }
94
+ else if (isStopScopeTraceEnabled()) {
95
+ if (stopMessageLogText && /stopmessage/i.test(stopMessageLogText)) {
96
+ const preview = stopMessageLogText.replace(/\s+/g, ' ').slice(0, 120);
97
+ engine.debug?.log?.(`[virtual-router][instruction_parse] requestId=${metadata.requestId || 'n/a'} marker=missing contains_stopmessage=yes preview=${preview}`);
98
+ }
99
+ }
100
+ const serverToolFollowup = isServerToolFollowupRequest(metadata);
101
+ const latestUserHasMarker = hasLatestUserRoutingInstructionMarker(request.messages) || responsesHasMarker;
102
+ let instructions = parsedInstructions;
103
+ if (serverToolFollowup && instructions.length > 0) {
104
+ instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet' &&
105
+ entry.type !== 'stopMessageMode' &&
106
+ entry.type !== 'stopMessageClear' &&
107
+ entry.type !== 'preCommandSet' &&
108
+ entry.type !== 'preCommandClear');
109
+ }
110
+ if (stopMessageScope && parsedInstructions.length > 0) {
111
+ const sessionState = getRoutingInstructionState(stopMessageScope, engine.routingInstructionState, engine.routingStateStore);
112
+ const hasStaleStopMessageInstruction = !latestUserHasMarker &&
113
+ parsedInstructions.some((entry) => entry.type === 'stopMessageSet' || entry.type === 'stopMessageMode');
114
+ if (hasStaleStopMessageInstruction) {
115
+ const hasActiveStopState = typeof sessionState.stopMessageText === 'string' ||
116
+ typeof sessionState.stopMessageMaxRepeats === 'number' ||
117
+ typeof sessionState.stopMessageStageMode === 'string' ||
118
+ typeof sessionState.stopMessageAiMode === 'string' ||
119
+ typeof sessionState.stopMessageAiSeedPrompt === 'string' ||
120
+ Array.isArray(sessionState.stopMessageAiHistory);
121
+ const hasStopLifecycleStamp = (typeof sessionState.stopMessageUpdatedAt === 'number' &&
122
+ Number.isFinite(sessionState.stopMessageUpdatedAt)) ||
123
+ (typeof sessionState.stopMessageLastUsedAt === 'number' &&
124
+ Number.isFinite(sessionState.stopMessageLastUsedAt));
125
+ if (hasActiveStopState || hasStopLifecycleStamp) {
126
+ instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet' && entry.type !== 'stopMessageMode');
127
+ }
128
+ }
129
+ }
130
+ // stopMessage/precommand require explicit client inject scope (tmux).
131
+ // When scope is missing, drop only these instructions and keep request passthrough.
132
+ if (instructions.length > 0 && !stopMessageScope) {
133
+ const blockedInstructionTypes = instructions
134
+ .filter((entry) => entry.type === 'stopMessageSet' ||
135
+ entry.type === 'stopMessageMode' ||
136
+ entry.type === 'stopMessageClear' ||
137
+ entry.type === 'preCommandSet' ||
138
+ entry.type === 'preCommandClear')
139
+ .map((entry) => entry.type);
140
+ if (blockedInstructionTypes.length > 0 && isStopScopeTraceEnabled()) {
141
+ engine.debug?.log?.(`[virtual-router][stop_scope] requestId=${metadata.requestId || 'n/a'} stage=drop reason=missing_tmux_scope instructions=${blockedInstructionTypes.join(',')}`);
142
+ }
143
+ instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet' &&
144
+ entry.type !== 'stopMessageMode' &&
145
+ entry.type !== 'stopMessageClear' &&
146
+ entry.type !== 'preCommandSet' &&
147
+ entry.type !== 'preCommandClear');
148
+ }
149
+ if (hasRoutingInstructionMarker(request.messages) || responsesHasMarker) {
150
+ request.messages = cleanMessagesFromRoutingInstructions(request.messages);
151
+ cleanResponsesContextFromRoutingInstructions(responsesContext);
152
+ }
153
+ let appliedRoutingState = routingState;
154
+ if (instructions.length > 0) {
155
+ appliedRoutingState = applyRoutingInstructions(instructions, routingState);
156
+ const persistedBaseState = stopMessageScope
157
+ ? stripClientInjectScopedFields(appliedRoutingState)
158
+ : appliedRoutingState;
159
+ routingState = persistedBaseState;
160
+ engine.routingInstructionState.set(stateKey, persistedBaseState);
161
+ persistRoutingInstructionState(stateKey, persistedBaseState, engine.routingStateStore);
162
+ // Persist stopMessage under tmux/clockd scope so client injection and trigger matching
163
+ // use the same scope and never fall back to generic session keys.
164
+ if (stopMessageScope) {
165
+ const hasStopMessageSet = instructions.some((entry) => entry.type === 'stopMessageSet');
166
+ const hasStopMessageMode = instructions.some((entry) => entry.type === 'stopMessageMode');
167
+ const hasGlobalClear = instructions.some((entry) => entry.type === 'clear');
168
+ const hasStopMessageClear = hasGlobalClear || instructions.some((entry) => entry.type === 'stopMessageClear');
169
+ if (hasStopMessageSet || hasStopMessageMode || hasStopMessageClear) {
170
+ const activeInstructionTypes = instructions
171
+ .filter((entry) => entry.type === 'stopMessageSet' ||
172
+ entry.type === 'stopMessageMode' ||
173
+ entry.type === 'stopMessageClear')
174
+ .map((entry) => entry.type);
175
+ if (isStopScopeTraceEnabled()) {
176
+ engine.debug?.log?.(`[virtual-router][stop_scope] requestId=${metadata.requestId || 'n/a'} stage=apply scope=${stopMessageScope} instructions=${activeInstructionTypes.join(',') || 'none'}`);
177
+ }
178
+ const sessionState = getRoutingInstructionState(stopMessageScope, engine.routingInstructionState, engine.routingStateStore);
179
+ let nextSessionState = {
180
+ ...sessionState
181
+ };
182
+ let shouldPersistSessionState = false;
183
+ const hasStopMessageStateChanged = () => {
184
+ return (nextSessionState.stopMessageText !== sessionState.stopMessageText ||
185
+ nextSessionState.stopMessageMaxRepeats !== sessionState.stopMessageMaxRepeats ||
186
+ nextSessionState.stopMessageUsed !== sessionState.stopMessageUsed ||
187
+ nextSessionState.stopMessageUpdatedAt !== sessionState.stopMessageUpdatedAt ||
188
+ nextSessionState.stopMessageLastUsedAt !== sessionState.stopMessageLastUsedAt ||
189
+ nextSessionState.stopMessageSource !== sessionState.stopMessageSource ||
190
+ nextSessionState.stopMessageStageMode !== sessionState.stopMessageStageMode ||
191
+ nextSessionState.stopMessageAiMode !== sessionState.stopMessageAiMode ||
192
+ nextSessionState.stopMessageAiSeedPrompt !== sessionState.stopMessageAiSeedPrompt ||
193
+ JSON.stringify(nextSessionState.stopMessageAiHistory || []) !==
194
+ JSON.stringify(sessionState.stopMessageAiHistory || []));
195
+ };
196
+ if (hasStopMessageClear) {
197
+ if (hasGlobalClear) {
198
+ // <**clear**> is a hard reset: clear all session-scoped routing state
199
+ // and let persistRoutingInstructionState delete persistence markers.
200
+ nextSessionState = {
201
+ forcedTarget: undefined,
202
+ stickyTarget: undefined,
203
+ preferTarget: undefined,
204
+ allowedProviders: new Set(),
205
+ disabledProviders: new Set(),
206
+ disabledKeys: new Map(),
207
+ disabledModels: new Map(),
208
+ stopMessageSource: undefined,
209
+ stopMessageText: undefined,
210
+ stopMessageMaxRepeats: undefined,
211
+ stopMessageUsed: undefined,
212
+ stopMessageUpdatedAt: undefined,
213
+ stopMessageLastUsedAt: undefined,
214
+ stopMessageStageMode: undefined,
215
+ stopMessageAiMode: undefined,
216
+ stopMessageAiSeedPrompt: undefined,
217
+ stopMessageAiHistory: undefined,
218
+ preCommandSource: undefined,
219
+ preCommandScriptPath: undefined,
220
+ preCommandUpdatedAt: undefined
221
+ };
222
+ shouldPersistSessionState = true;
223
+ }
224
+ else {
225
+ nextSessionState.stopMessageText = undefined;
226
+ nextSessionState.stopMessageMaxRepeats = undefined;
227
+ nextSessionState.stopMessageUsed = undefined;
228
+ nextSessionState.stopMessageUpdatedAt = undefined;
229
+ nextSessionState.stopMessageLastUsedAt = undefined;
230
+ nextSessionState.stopMessageSource = undefined;
231
+ nextSessionState.stopMessageStageMode = undefined;
232
+ nextSessionState.stopMessageAiMode = undefined;
233
+ nextSessionState.stopMessageAiSeedPrompt = undefined;
234
+ nextSessionState.stopMessageAiHistory = undefined;
235
+ shouldPersistSessionState = true;
236
+ }
237
+ }
238
+ else if (hasStopMessageSet || hasStopMessageMode) {
239
+ nextSessionState.stopMessageText =
240
+ typeof appliedRoutingState.stopMessageText === 'string' &&
241
+ appliedRoutingState.stopMessageText.trim()
242
+ ? appliedRoutingState.stopMessageText
243
+ : undefined;
244
+ nextSessionState.stopMessageMaxRepeats =
245
+ typeof appliedRoutingState.stopMessageMaxRepeats === 'number' &&
246
+ Number.isFinite(appliedRoutingState.stopMessageMaxRepeats)
247
+ ? Math.floor(appliedRoutingState.stopMessageMaxRepeats)
248
+ : undefined;
249
+ nextSessionState.stopMessageUsed =
250
+ typeof appliedRoutingState.stopMessageUsed === 'number' &&
251
+ Number.isFinite(appliedRoutingState.stopMessageUsed)
252
+ ? Math.max(0, Math.floor(appliedRoutingState.stopMessageUsed))
253
+ : undefined;
254
+ nextSessionState.stopMessageUpdatedAt =
255
+ typeof appliedRoutingState.stopMessageUpdatedAt === 'number' &&
256
+ Number.isFinite(appliedRoutingState.stopMessageUpdatedAt)
257
+ ? appliedRoutingState.stopMessageUpdatedAt
258
+ : undefined;
259
+ nextSessionState.stopMessageLastUsedAt =
260
+ typeof appliedRoutingState.stopMessageLastUsedAt === 'number' &&
261
+ Number.isFinite(appliedRoutingState.stopMessageLastUsedAt)
262
+ ? appliedRoutingState.stopMessageLastUsedAt
263
+ : undefined;
264
+ nextSessionState.stopMessageSource =
265
+ typeof appliedRoutingState.stopMessageSource === 'string' &&
266
+ appliedRoutingState.stopMessageSource.trim()
267
+ ? appliedRoutingState.stopMessageSource.trim()
268
+ : undefined;
269
+ nextSessionState.stopMessageStageMode = normalizeStopMessageStageMode(appliedRoutingState.stopMessageStageMode);
270
+ nextSessionState.stopMessageAiMode = normalizeStopMessageAiMode(appliedRoutingState.stopMessageAiMode);
271
+ nextSessionState.stopMessageAiSeedPrompt =
272
+ typeof appliedRoutingState.stopMessageAiSeedPrompt === 'string' &&
273
+ appliedRoutingState.stopMessageAiSeedPrompt.trim()
274
+ ? appliedRoutingState.stopMessageAiSeedPrompt.trim()
275
+ : undefined;
276
+ nextSessionState.stopMessageAiHistory = Array.isArray(appliedRoutingState.stopMessageAiHistory)
277
+ ? appliedRoutingState.stopMessageAiHistory
278
+ : undefined;
279
+ shouldPersistSessionState = hasStopMessageStateChanged();
280
+ }
281
+ if (shouldPersistSessionState) {
282
+ engine.routingInstructionState.set(stopMessageScope, nextSessionState);
283
+ persistRoutingInstructionState(stopMessageScope, nextSessionState, engine.routingStateStore);
284
+ }
285
+ else {
286
+ nextSessionState = sessionState;
287
+ }
288
+ // 日志展示使用 session scope 的 stopMessage 状态,避免每次解析重复刷新时间/次数。
289
+ if (typeof nextSessionState.stopMessageText === 'string' ||
290
+ typeof nextSessionState.stopMessageMaxRepeats === 'number' ||
291
+ typeof nextSessionState.stopMessageStageMode === 'string' ||
292
+ typeof nextSessionState.stopMessageAiMode === 'string' ||
293
+ typeof nextSessionState.stopMessageAiSeedPrompt === 'string' ||
294
+ Array.isArray(nextSessionState.stopMessageAiHistory)) {
295
+ routingState.stopMessageText = nextSessionState.stopMessageText;
296
+ routingState.stopMessageMaxRepeats = nextSessionState.stopMessageMaxRepeats;
297
+ routingState.stopMessageUsed = nextSessionState.stopMessageUsed;
298
+ routingState.stopMessageUpdatedAt = nextSessionState.stopMessageUpdatedAt;
299
+ routingState.stopMessageLastUsedAt = nextSessionState.stopMessageLastUsedAt;
300
+ routingState.stopMessageStageMode = nextSessionState.stopMessageStageMode;
301
+ routingState.stopMessageAiMode = nextSessionState.stopMessageAiMode;
302
+ routingState.stopMessageAiSeedPrompt = nextSessionState.stopMessageAiSeedPrompt;
303
+ routingState.stopMessageAiHistory = nextSessionState.stopMessageAiHistory;
304
+ }
305
+ }
306
+ }
307
+ }
308
+ if (instructions.length > 0 && stopMessageScope) {
309
+ const hasPreCommandSet = instructions.some((entry) => entry.type === 'preCommandSet');
310
+ const hasPreCommandClear = instructions.some((entry) => entry.type === 'preCommandClear');
311
+ if (hasPreCommandSet || hasPreCommandClear) {
312
+ const sessionState = getRoutingInstructionState(stopMessageScope, engine.routingInstructionState, engine.routingStateStore);
313
+ const nextSessionState = {
314
+ ...sessionState
315
+ };
316
+ let changed = false;
317
+ if (hasPreCommandClear) {
318
+ changed =
319
+ typeof sessionState.preCommandScriptPath === 'string' ||
320
+ typeof sessionState.preCommandSource === 'string' ||
321
+ typeof sessionState.preCommandUpdatedAt === 'number';
322
+ nextSessionState.preCommandScriptPath = undefined;
323
+ nextSessionState.preCommandSource = undefined;
324
+ nextSessionState.preCommandUpdatedAt = Date.now();
325
+ }
326
+ if (hasPreCommandSet) {
327
+ const scriptPath = typeof appliedRoutingState.preCommandScriptPath === 'string'
328
+ ? appliedRoutingState.preCommandScriptPath.trim()
329
+ : '';
330
+ if (scriptPath) {
331
+ if (sessionState.preCommandScriptPath !== scriptPath) {
332
+ changed = true;
333
+ }
334
+ nextSessionState.preCommandScriptPath = scriptPath;
335
+ nextSessionState.preCommandSource = 'explicit';
336
+ nextSessionState.preCommandUpdatedAt =
337
+ typeof appliedRoutingState.preCommandUpdatedAt === 'number' &&
338
+ Number.isFinite(appliedRoutingState.preCommandUpdatedAt)
339
+ ? appliedRoutingState.preCommandUpdatedAt
340
+ : Date.now();
341
+ }
342
+ }
343
+ if (changed) {
344
+ engine.routingInstructionState.set(stopMessageScope, nextSessionState);
345
+ persistRoutingInstructionState(stopMessageScope, nextSessionState, engine.routingStateStore);
346
+ routingState.preCommandScriptPath = nextSessionState.preCommandScriptPath;
347
+ routingState.preCommandSource = nextSessionState.preCommandSource;
348
+ routingState.preCommandUpdatedAt = nextSessionState.preCommandUpdatedAt;
349
+ }
350
+ }
351
+ }
352
+ if (instructions.length === 0 && stopMessageScope) {
353
+ const sessionState = getRoutingInstructionState(stopMessageScope, engine.routingInstructionState, engine.routingStateStore);
354
+ if (typeof sessionState.stopMessageText === 'string' ||
355
+ typeof sessionState.stopMessageMaxRepeats === 'number' ||
356
+ typeof sessionState.stopMessageStageMode === 'string' ||
357
+ typeof sessionState.stopMessageAiMode === 'string' ||
358
+ typeof sessionState.stopMessageAiSeedPrompt === 'string' ||
359
+ Array.isArray(sessionState.stopMessageAiHistory)) {
360
+ routingState.stopMessageText = sessionState.stopMessageText;
361
+ routingState.stopMessageMaxRepeats = sessionState.stopMessageMaxRepeats;
362
+ routingState.stopMessageUsed = sessionState.stopMessageUsed;
363
+ routingState.stopMessageUpdatedAt = sessionState.stopMessageUpdatedAt;
364
+ routingState.stopMessageLastUsedAt = sessionState.stopMessageLastUsedAt;
365
+ routingState.stopMessageStageMode = sessionState.stopMessageStageMode;
366
+ routingState.stopMessageAiMode = sessionState.stopMessageAiMode;
367
+ routingState.stopMessageAiSeedPrompt = sessionState.stopMessageAiSeedPrompt;
368
+ routingState.stopMessageAiHistory = sessionState.stopMessageAiHistory;
369
+ }
370
+ if (typeof sessionState.preCommandScriptPath === 'string' && sessionState.preCommandScriptPath.trim()) {
371
+ routingState.preCommandScriptPath = sessionState.preCommandScriptPath;
372
+ routingState.preCommandSource = sessionState.preCommandSource;
373
+ routingState.preCommandUpdatedAt = sessionState.preCommandUpdatedAt;
374
+ }
375
+ }
376
+ // Guardrail: if a session is restricted to providers that do not exist in any routing pools,
377
+ // we must not hard-fail the request loop. Auto-clear the allowlist and fall back to normal routing.
378
+ routingState = enforceAllowlistIntersection(engine, routingState, stateKey);
379
+ return {
380
+ routingState,
381
+ stateKey,
382
+ stopMessageScope,
383
+ metadataInstructions,
384
+ instructions
385
+ };
386
+ }
@@ -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
+ }