@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,205 @@
1
+ import { DEFAULT_ROUTE, VirtualRouterError, VirtualRouterErrorCode } from '../types.js';
2
+ import { buildRoutingFeatures } from '../features.js';
3
+ import { persistRoutingInstructionState } from '../engine/routing-state/store.js';
4
+ import { selectDirectProviderModel, selectFromStickyPool as selectFromStickyPoolImpl } from '../engine/routing-pools/index.js';
5
+ export function selectRoutingTarget(engine, request, metadata, routingState, stateKey) {
6
+ const features = buildRoutingFeatures(request, metadata);
7
+ const directProviderModel = engine.parseDirectProviderModel(request?.model);
8
+ let classification;
9
+ let requestedRoute;
10
+ let selection = null;
11
+ const selectionDeps = {
12
+ routing: engine.routing,
13
+ providerRegistry: engine.providerRegistry,
14
+ healthManager: engine.healthManager,
15
+ contextAdvisor: engine.contextAdvisor,
16
+ loadBalancer: engine.loadBalancer,
17
+ isProviderCoolingDown: (key) => engine.isProviderCoolingDown(key),
18
+ resolveStickyKey: (m) => engine.resolveStickyKey(m),
19
+ quotaView: engine.quotaView
20
+ };
21
+ if (directProviderModel) {
22
+ const forceMediaFallback = engine.shouldFallbackDirectModelForMedia(directProviderModel, features);
23
+ const providerKeys = engine.providerRegistry.listProviderKeys(directProviderModel.providerId);
24
+ let hasModel = false;
25
+ for (const key of providerKeys) {
26
+ try {
27
+ const profile = engine.providerRegistry.get(key);
28
+ if (profile?.modelId === directProviderModel.modelId) {
29
+ hasModel = true;
30
+ break;
31
+ }
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ }
37
+ if (!hasModel) {
38
+ throw new VirtualRouterError(`Unknown model ${directProviderModel.modelId} for provider ${directProviderModel.providerId}`, VirtualRouterErrorCode.CONFIG_ERROR, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
39
+ }
40
+ if (!forceMediaFallback) {
41
+ const directSelection = selectDirectProviderModel(directProviderModel.providerId, directProviderModel.modelId, metadata, features, routingState, selectionDeps);
42
+ if (!directSelection) {
43
+ throw new VirtualRouterError(`All providers unavailable for model ${directProviderModel.providerId}.${directProviderModel.modelId}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
44
+ }
45
+ classification = {
46
+ routeName: 'direct',
47
+ confidence: 1,
48
+ reasoning: `direct_model:${directProviderModel.providerId}.${directProviderModel.modelId}`,
49
+ fallback: false,
50
+ candidates: ['direct']
51
+ };
52
+ requestedRoute = 'direct';
53
+ selection = directSelection;
54
+ }
55
+ else {
56
+ classification = engine.classifier.classify(features);
57
+ requestedRoute = engine.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
58
+ selection = engine.selectProvider(requestedRoute, metadata, classification, features, routingState);
59
+ }
60
+ }
61
+ else {
62
+ // Prefer target (from "<**!provider.model**>") is evaluated before routing classification.
63
+ const preferTarget = routingState.preferTarget;
64
+ if (preferTarget && typeof preferTarget.provider === 'string' && preferTarget.provider.trim()) {
65
+ const providerId = preferTarget.provider.trim();
66
+ const keyAlias = typeof preferTarget.keyAlias === 'string' ? preferTarget.keyAlias.trim() : '';
67
+ const modelId = typeof preferTarget.model === 'string' ? preferTarget.model.trim() : '';
68
+ const keyIndex = typeof preferTarget.keyIndex === 'number' && Number.isFinite(preferTarget.keyIndex)
69
+ ? Math.floor(preferTarget.keyIndex)
70
+ : undefined;
71
+ const candidateKeys = [];
72
+ if (keyIndex !== undefined && keyIndex > 0) {
73
+ const runtimeKey = engine.providerRegistry.resolveRuntimeKeyByIndex(providerId, keyIndex);
74
+ if (runtimeKey) {
75
+ candidateKeys.push(runtimeKey);
76
+ }
77
+ }
78
+ else if (modelId) {
79
+ const allKeys = engine.providerRegistry.listProviderKeys(providerId);
80
+ for (const key of allKeys) {
81
+ if (keyAlias) {
82
+ const prefix = `${providerId}.${keyAlias}.`;
83
+ if (!key.startsWith(prefix)) {
84
+ continue;
85
+ }
86
+ }
87
+ try {
88
+ const profile = engine.providerRegistry.get(key);
89
+ if (profile?.modelId === modelId) {
90
+ candidateKeys.push(key);
91
+ }
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ }
97
+ }
98
+ const allowAliasRotation = !keyAlias && keyIndex === undefined;
99
+ const eligibleKeys = (() => {
100
+ if (candidateKeys.length === 0) {
101
+ return [];
102
+ }
103
+ const quotaView = selectionDeps.quotaView;
104
+ const now = quotaView ? Date.now() : 0;
105
+ return candidateKeys.filter((key) => {
106
+ if (!quotaView) {
107
+ if (engine.isProviderCoolingDown(key)) {
108
+ return false;
109
+ }
110
+ if (!engine.healthManager.isAvailable(key)) {
111
+ return false;
112
+ }
113
+ return true;
114
+ }
115
+ const entry = quotaView(key);
116
+ if (!entry) {
117
+ return true;
118
+ }
119
+ if (!entry.inPool) {
120
+ return false;
121
+ }
122
+ if (entry.cooldownUntil && entry.cooldownUntil > now) {
123
+ return false;
124
+ }
125
+ if (entry.blacklistUntil && entry.blacklistUntil > now) {
126
+ return false;
127
+ }
128
+ return true;
129
+ });
130
+ })();
131
+ const preferSelection = eligibleKeys.length > 0
132
+ ? selectFromStickyPoolImpl(new Set(eligibleKeys), metadata, features, routingState, selectionDeps, {
133
+ allowAliasRotation
134
+ })
135
+ : null;
136
+ if (preferSelection) {
137
+ classification = {
138
+ routeName: 'prefer',
139
+ confidence: 1,
140
+ reasoning: keyIndex !== undefined ? `prefer_key:${providerId}.${keyIndex}` : `prefer_model:${providerId}.${modelId}`,
141
+ fallback: false,
142
+ candidates: ['prefer']
143
+ };
144
+ requestedRoute = 'prefer';
145
+ selection = {
146
+ ...preferSelection,
147
+ routeUsed: 'prefer',
148
+ poolId: 'prefer-primary'
149
+ };
150
+ }
151
+ else if (routingState.preferTarget) {
152
+ // Auto-clear only when the target becomes invalid or blocked by explicit routing instructions.
153
+ // Do NOT clear for temporary unavailability (e.g. 429 cooldown, quota cooldown, transient health).
154
+ const shouldAutoClear = (() => {
155
+ if (candidateKeys.length === 0) {
156
+ return true;
157
+ }
158
+ // Prefer selection failed despite eligible keys existing: treat as a hard block (e.g. routing rules).
159
+ if (eligibleKeys.length > 0) {
160
+ return true;
161
+ }
162
+ // If quota explicitly marks the preferred target as out-of-pool, clear the prefer instruction so
163
+ // the router can fall back to other targets without repeatedly retrying an impossible preference.
164
+ if (selectionDeps.quotaView) {
165
+ for (const key of candidateKeys) {
166
+ const entry = selectionDeps.quotaView(key);
167
+ if (entry && entry.inPool === false) {
168
+ return true;
169
+ }
170
+ }
171
+ }
172
+ return false;
173
+ })();
174
+ if (shouldAutoClear) {
175
+ routingState = {
176
+ ...routingState,
177
+ preferTarget: undefined
178
+ };
179
+ engine.routingInstructionState.set(stateKey, routingState);
180
+ persistRoutingInstructionState(stateKey, routingState, engine.routingStateStore);
181
+ }
182
+ }
183
+ }
184
+ if (!selection) {
185
+ classification = metadata.routeHint && metadata.routeHint.trim()
186
+ ? {
187
+ routeName: metadata.routeHint.trim(),
188
+ confidence: 1,
189
+ reasoning: `route_hint:${metadata.routeHint.trim()}`,
190
+ fallback: false,
191
+ candidates: [metadata.routeHint.trim()]
192
+ }
193
+ : engine.classifier.classify(features);
194
+ requestedRoute = engine.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
195
+ selection = engine.selectProvider(requestedRoute, metadata, classification, features, routingState);
196
+ }
197
+ }
198
+ return {
199
+ routingState,
200
+ requestedRoute,
201
+ classification: classification,
202
+ selection: selection,
203
+ features
204
+ };
205
+ }
@@ -0,0 +1,3 @@
1
+ import type { RoutingInstructionState } from '../routing-instructions.js';
2
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
3
+ export declare function enforceAllowlistIntersection(engine: VirtualRouterEngine, routingState: RoutingInstructionState, stateKey: string): RoutingInstructionState;
@@ -0,0 +1,36 @@
1
+ import { extractProviderId } from '../engine/provider-key/parse.js';
2
+ import { persistRoutingInstructionState } from '../engine/routing-state/store.js';
3
+ export function enforceAllowlistIntersection(engine, routingState, stateKey) {
4
+ if (routingState.allowedProviders.size === 0) {
5
+ return routingState;
6
+ }
7
+ const providersInRouting = new Set();
8
+ for (const pools of Object.values(engine.routing)) {
9
+ if (!Array.isArray(pools))
10
+ continue;
11
+ for (const pool of pools) {
12
+ if (!pool || !Array.isArray(pool.targets))
13
+ continue;
14
+ for (const key of pool.targets) {
15
+ if (typeof key !== 'string' || !key)
16
+ continue;
17
+ const providerId = extractProviderId(key);
18
+ if (providerId) {
19
+ providersInRouting.add(providerId);
20
+ }
21
+ }
22
+ }
23
+ }
24
+ const allowed = Array.from(routingState.allowedProviders).filter((provider) => typeof provider === 'string');
25
+ const hasIntersection = allowed.some((provider) => providersInRouting.has(provider));
26
+ if (!hasIntersection) {
27
+ const nextState = {
28
+ ...routingState,
29
+ allowedProviders: new Set()
30
+ };
31
+ engine.routingInstructionState.set(stateKey, nextState);
32
+ persistRoutingInstructionState(stateKey, nextState, engine.routingStateStore);
33
+ return nextState;
34
+ }
35
+ return routingState;
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProcessedRequest, StandardizedRequest } from '../../../conversion/hub/types/standardized.js';
2
+ import type { RouterMetadataInput } from '../types.js';
3
+ import type { RoutingInstruction, RoutingInstructionState } from '../routing-instructions.js';
4
+ import type { VirtualRouterEngine } from '../engine-legacy.js';
5
+ export type RoutingStateResult = {
6
+ routingState: RoutingInstructionState;
7
+ stateKey: string;
8
+ stopMessageScope?: string;
9
+ metadataInstructions: RoutingInstruction[];
10
+ instructions: RoutingInstruction[];
11
+ };
12
+ export declare function buildRoutingState(engine: VirtualRouterEngine, request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): RoutingStateResult;
@@ -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
+ }