@jsonstudio/llms 0.6.1739 → 0.6.1890

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 (107) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
  2. package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
  3. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
  4. package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
  5. package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
  6. package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
  12. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
  14. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
  15. package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
  16. package/dist/conversion/hub/policy/policy-engine.js +8 -0
  17. package/dist/conversion/hub/process/chat-process.js +466 -16
  18. package/dist/conversion/hub/response/provider-response.js +0 -35
  19. package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
  20. package/dist/conversion/responses/responses-openai-bridge.js +166 -8
  21. package/dist/conversion/shared/anthropic-message-utils.js +10 -1
  22. package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
  23. package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
  24. package/dist/conversion/shared/tool-governor.js +102 -0
  25. package/dist/guidance/index.js +17 -0
  26. package/dist/router/virtual-router/bootstrap.js +46 -1
  27. package/dist/router/virtual-router/classifier.js +59 -4
  28. package/dist/router/virtual-router/engine/health/index.js +6 -6
  29. package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
  30. package/dist/router/virtual-router/engine-logging.js +62 -24
  31. package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
  33. package/dist/router/virtual-router/engine.d.ts +3 -1
  34. package/dist/router/virtual-router/engine.js +359 -39
  35. package/dist/router/virtual-router/features.js +2 -1
  36. package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
  37. package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
  38. package/dist/router/virtual-router/provider-registry.js +3 -1
  39. package/dist/router/virtual-router/routing-instructions.d.ts +15 -1
  40. package/dist/router/virtual-router/routing-instructions.js +110 -151
  41. package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
  42. package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
  43. package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
  45. package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
  46. package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
  47. package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
  48. package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
  49. package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
  50. package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
  51. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
  52. package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
  53. package/dist/router/virtual-router/sticky-session-store.js +206 -57
  54. package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
  55. package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
  56. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  57. package/dist/router/virtual-router/stop-message-state-sync.js +5 -0
  58. package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
  59. package/dist/router/virtual-router/token-file-scanner.js +64 -3
  60. package/dist/router/virtual-router/tool-signals.d.ts +5 -0
  61. package/dist/router/virtual-router/tool-signals.js +42 -3
  62. package/dist/router/virtual-router/types.d.ts +19 -1
  63. package/dist/router/virtual-router/types.js +1 -0
  64. package/dist/servertool/clock/config.d.ts +1 -1
  65. package/dist/servertool/clock/config.js +27 -4
  66. package/dist/servertool/clock/state.js +41 -2
  67. package/dist/servertool/clock/task-store.d.ts +2 -2
  68. package/dist/servertool/clock/task-store.js +1 -1
  69. package/dist/servertool/clock/tasks.d.ts +3 -1
  70. package/dist/servertool/clock/tasks.js +209 -18
  71. package/dist/servertool/clock/types.d.ts +17 -0
  72. package/dist/servertool/continue-execution/log.d.ts +3 -0
  73. package/dist/servertool/continue-execution/log.js +13 -0
  74. package/dist/servertool/engine.js +414 -68
  75. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
  76. package/dist/servertool/handlers/clock-auto.js +54 -71
  77. package/dist/servertool/handlers/clock.js +121 -6
  78. package/dist/servertool/handlers/continue-execution.d.ts +1 -0
  79. package/dist/servertool/handlers/continue-execution.js +91 -0
  80. package/dist/servertool/handlers/followup-request-builder.js +13 -0
  81. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  82. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  83. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  84. package/dist/servertool/handlers/stop-message-auto.js +386 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +43 -0
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +684 -0
  87. package/dist/servertool/handlers/vision.js +1 -1
  88. package/dist/servertool/log/progress-file.d.ts +14 -0
  89. package/dist/servertool/log/progress-file.js +88 -0
  90. package/dist/servertool/pre-command-hooks.d.ts +17 -0
  91. package/dist/servertool/pre-command-hooks.js +491 -0
  92. package/dist/servertool/registry.d.ts +23 -6
  93. package/dist/servertool/registry.js +66 -1
  94. package/dist/servertool/server-side-tools.d.ts +1 -0
  95. package/dist/servertool/server-side-tools.js +216 -14
  96. package/dist/servertool/stop-gateway-context.d.ts +14 -0
  97. package/dist/servertool/stop-gateway-context.js +167 -0
  98. package/dist/servertool/stop-message-compare-context.d.ts +24 -0
  99. package/dist/servertool/stop-message-compare-context.js +133 -0
  100. package/dist/servertool/types.d.ts +12 -0
  101. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  102. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
  103. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
  104. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
  105. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
  106. package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
  107. package/package.json +1 -1
@@ -11,6 +11,12 @@ export class RoutingClassifier {
11
11
  }
12
12
  classify(features) {
13
13
  const lastToolCategory = features.lastAssistantToolCategory;
14
+ const webSearchDeclared = features.hasWebSearchToolDeclared === true;
15
+ const webSearchIntent = detectWebSearchIntent(features.userTextSample);
16
+ const localToolContinuation = lastToolCategory === 'read' ||
17
+ lastToolCategory === 'write' ||
18
+ lastToolCategory === 'search' ||
19
+ lastToolCategory === 'other';
14
20
  const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
15
21
  const latestMessageFromUser = features.latestMessageFromUser === true;
16
22
  const thinkingContinuation = lastToolCategory === 'read';
@@ -21,6 +27,10 @@ export class RoutingClassifier {
21
27
  const toolsContinuation = lastToolCategory === 'other';
22
28
  const hasToolActivity = features.hasTools || features.hasToolCallResponses;
23
29
  const evaluationMap = {
30
+ multimodal: {
31
+ triggered: features.hasImageAttachment,
32
+ reason: 'multimodal:media-detected'
33
+ },
24
34
  vision: {
25
35
  triggered: features.hasImageAttachment,
26
36
  reason: 'vision:media-detected'
@@ -38,10 +48,16 @@ export class RoutingClassifier {
38
48
  reason: 'coding:last-tool-write'
39
49
  },
40
50
  web_search: {
41
- // web_search 路由不再基于上一轮工具分类或本轮是否声明 web_search 工具自动触发,
42
- // 仅保留为显式路由指令/未来扩展的占位,默认不命中。
43
- triggered: false,
44
- reason: 'web_search:disabled'
51
+ // web_search 仅由“当前请求”触发:
52
+ // - 显式声明 web_search 工具;或
53
+ // - 用户输入命中联网搜索意图关键词。
54
+ // 不再使用上一轮 websearch 续写,避免连续轮次被粘住。
55
+ triggered: !localToolContinuation && (webSearchDeclared || webSearchIntent),
56
+ reason: webSearchDeclared && webSearchIntent
57
+ ? 'web_search:tool+intent'
58
+ : webSearchDeclared
59
+ ? 'web_search:tool-declared'
60
+ : 'web_search:intent-keyword'
45
61
  },
46
62
  search: {
47
63
  // search 路由:仅在上一轮 assistant 使用 search 类工具时继续命中,
@@ -99,6 +115,45 @@ export class RoutingClassifier {
99
115
  return index >= 0 ? index : ROUTE_PRIORITY.length;
100
116
  }
101
117
  }
118
+ function detectWebSearchIntent(text) {
119
+ if (!text || !text.trim()) {
120
+ return false;
121
+ }
122
+ const normalized = text.toLowerCase();
123
+ const directKeywords = [
124
+ 'web search',
125
+ 'web_search',
126
+ 'websearch',
127
+ 'search the web',
128
+ 'internet search',
129
+ 'search online',
130
+ '搜索网页',
131
+ '联网搜索',
132
+ '上网搜索',
133
+ '上网查',
134
+ '网上搜',
135
+ '谷歌搜索',
136
+ 'google search'
137
+ ];
138
+ if (directKeywords.some((keyword) => normalized.includes(keyword))) {
139
+ return true;
140
+ }
141
+ const enVerb = ['search', 'find', 'lookup', 'look up', 'google'];
142
+ const enNoun = ['web', 'internet', 'online', 'news', 'latest', 'today'];
143
+ const hasEnVerb = enVerb.some((keyword) => normalized.includes(keyword));
144
+ const hasEnNoun = enNoun.some((keyword) => normalized.includes(keyword));
145
+ if (hasEnVerb && hasEnNoun) {
146
+ return true;
147
+ }
148
+ const zhVerb = ['搜索', '查找', '查询', '搜'];
149
+ const zhNoun = ['网络', '联网', '网页', '新闻', '资讯', '实时', '最新', '今天'];
150
+ const hasZhVerb = zhVerb.some((keyword) => text.includes(keyword));
151
+ const hasZhNoun = zhNoun.some((keyword) => text.includes(keyword));
152
+ if (text.includes('上网') || (hasZhVerb && hasZhNoun)) {
153
+ return true;
154
+ }
155
+ return false;
156
+ }
102
157
  function normalizeList(source, fallback) {
103
158
  if (!source || source.length === 0) {
104
159
  return fallback;
@@ -66,17 +66,17 @@ function readEnvDuration(name, fallbackMs) {
66
66
  }
67
67
  /**
68
68
  * 对没有 quotaResetDelay 的 429 错误,在 VirtualRouter 内部维护一个简单的阶梯退避策略:
69
- * - 默认:第 1 次 5 分钟,第 2 次 1 小时,第 3 次 6 小时,第 4 次及以上 24 小时封顶;
70
- * - 可通过环境变量 ROUTECODEX_RL_SCHEDULE / RCC_RL_SCHEDULE 调整(例如 "5m,1h,6h,24h")。
69
+ * - 默认:第 1 次 3 秒,第 2 次 10 秒,第 3 次 31 秒,第 4 次及以上 61 秒封顶;
70
+ * - 可通过环境变量 ROUTECODEX_RL_SCHEDULE / RCC_RL_SCHEDULE 调整(例如 "3s,10s,31s,61s")。
71
71
  *
72
72
  * 这里的“次数”针对 providerKey 计数,并带有简单的时间窗口:若距离上次 429 超过 24 小时,则重置计数。
73
73
  * 该状态仅用于路由决策,不反映在 healthConfig 上,使 Host 与 VirtualRouter 对 429 处理职责清晰分层。
74
74
  */
75
75
  const NO_QUOTA_RATE_LIMIT_SCHEDULE_MS = readEnvSchedule('ROUTECODEX_RL_SCHEDULE', [
76
- 5 * 60_000,
77
- 60 * 60_000,
78
- 6 * 60 * 60_000,
79
- 24 * 60 * 60_000
76
+ 3_000,
77
+ 10_000,
78
+ 31_000,
79
+ 61_000
80
80
  ]);
81
81
  const rateLimitBackoffByProvider = new Map();
82
82
  const RATE_LIMIT_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_RL_RESET_WINDOW', 24 * 60 * 60_000);
@@ -22,6 +22,11 @@ export function getRoutingInstructionState(stickyKey, routingInstructionState, r
22
22
  existing.stopMessageUsed = merged.stopMessageUsed;
23
23
  existing.stopMessageUpdatedAt = merged.stopMessageUpdatedAt;
24
24
  existing.stopMessageLastUsedAt = merged.stopMessageLastUsedAt;
25
+ if (persisted) {
26
+ existing.preCommandSource = persisted.preCommandSource;
27
+ existing.preCommandScriptPath = persisted.preCommandScriptPath;
28
+ existing.preCommandUpdatedAt = persisted.preCommandUpdatedAt;
29
+ }
25
30
  }
26
31
  catch {
27
32
  // 刷新失败不影响原有内存状态
@@ -46,7 +51,10 @@ export function getRoutingInstructionState(stickyKey, routingInstructionState, r
46
51
  stopMessageMaxRepeats: undefined,
47
52
  stopMessageUsed: undefined,
48
53
  stopMessageUpdatedAt: undefined,
49
- stopMessageLastUsedAt: undefined
54
+ stopMessageLastUsedAt: undefined,
55
+ preCommandSource: undefined,
56
+ preCommandScriptPath: undefined,
57
+ preCommandUpdatedAt: undefined
50
58
  };
51
59
  }
52
60
  routingInstructionState.set(key, initial);
@@ -68,6 +76,8 @@ function isRoutingStateEmpty(state) {
68
76
  (typeof state.stopMessageUsed !== 'number' || !Number.isFinite(state.stopMessageUsed)) &&
69
77
  (typeof state.stopMessageUpdatedAt !== 'number' || !Number.isFinite(state.stopMessageUpdatedAt)) &&
70
78
  (typeof state.stopMessageLastUsedAt !== 'number' || !Number.isFinite(state.stopMessageLastUsedAt));
79
+ const noPreCommand = (!state.preCommandScriptPath || !state.preCommandScriptPath.trim()) &&
80
+ (typeof state.preCommandUpdatedAt !== 'number' || !Number.isFinite(state.preCommandUpdatedAt));
71
81
  return (noForced &&
72
82
  noSticky &&
73
83
  noPrefer &&
@@ -75,7 +85,8 @@ function isRoutingStateEmpty(state) {
75
85
  noDisabledProviders &&
76
86
  noDisabledKeys &&
77
87
  noDisabledModels &&
78
- noStopMessage);
88
+ noStopMessage &&
89
+ noPreCommand);
79
90
  }
80
91
  export function persistRoutingInstructionState(key, state, routingStateStore) {
81
92
  if (!key || (!key.startsWith('session:') && !key.startsWith('conversation:'))) {
@@ -88,7 +99,9 @@ export function persistRoutingInstructionState(key, state, routingStateStore) {
88
99
  (typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)) ||
89
100
  (typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)) ||
90
101
  (typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)) ||
91
- (typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)));
102
+ (typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)) ||
103
+ Boolean(state.preCommandScriptPath && state.preCommandScriptPath.trim()) ||
104
+ (typeof state.preCommandUpdatedAt === 'number' && Number.isFinite(state.preCommandUpdatedAt)));
92
105
  if (isRoutingStateEmpty(state)) {
93
106
  if (prefersSync) {
94
107
  routingStateStore.saveSync(key, null);
@@ -1,4 +1,46 @@
1
1
  import { DEFAULT_MODEL_CONTEXT_TOKENS, DEFAULT_ROUTE } from './types.js';
2
+ function summarizeStopMessageRuntime(state) {
3
+ if (!state) {
4
+ return {
5
+ hasAny: false,
6
+ mode: 'unset',
7
+ maxRepeats: 0,
8
+ used: 0,
9
+ remaining: -1,
10
+ active: false
11
+ };
12
+ }
13
+ const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
14
+ const safeText = text ? (text.length > 24 ? `${text.slice(0, 21)}…` : text) : undefined;
15
+ const modeRaw = typeof state.stopMessageStageMode === 'string' ? state.stopMessageStageMode.trim().toLowerCase() : '';
16
+ const mode = modeRaw === 'on' || modeRaw === 'off' || modeRaw === 'auto' ? modeRaw : 'unset';
17
+ const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
18
+ ? Math.max(0, Math.floor(state.stopMessageMaxRepeats))
19
+ : 0;
20
+ const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
21
+ ? Math.max(0, Math.floor(state.stopMessageUsed))
22
+ : 0;
23
+ const remaining = maxRepeats > 0 ? Math.max(0, maxRepeats - used) : -1;
24
+ const active = mode !== 'off' && maxRepeats > 0 && (Boolean(text) || mode === 'on' || mode === 'auto');
25
+ const updatedAt = typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)
26
+ ? state.stopMessageUpdatedAt
27
+ : undefined;
28
+ const lastUsedAt = typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
29
+ ? state.stopMessageLastUsedAt
30
+ : undefined;
31
+ const hasAny = Boolean(text) || maxRepeats > 0 || mode !== 'unset' || used > 0;
32
+ return {
33
+ hasAny,
34
+ ...(safeText ? { safeText } : {}),
35
+ mode,
36
+ maxRepeats,
37
+ used,
38
+ remaining,
39
+ active,
40
+ ...(updatedAt ? { updatedAt } : {}),
41
+ ...(lastUsedAt ? { lastUsedAt } : {})
42
+ };
43
+ }
2
44
  export function formatStickyScope(scope) {
3
45
  if (!scope || scope.trim().length === 0) {
4
46
  return undefined;
@@ -45,6 +87,7 @@ export function describeTargetProvider(providerKey, fallbackModelId) {
45
87
  }
46
88
  function resolveRouteColor(routeName) {
47
89
  const map = {
90
+ multimodal: '\x1b[38;5;45m',
48
91
  tools: '\x1b[38;5;214m',
49
92
  thinking: '\x1b[34m',
50
93
  coding: '\x1b[35m',
@@ -154,24 +197,20 @@ export function formatVirtualRouterHit(routeName, poolId, providerKey, modelId,
154
197
  const stickyLabel = stickyText ? ` ${stickyColor}[sticky:${stickyText}]${reset}` : '';
155
198
  const reasonLabel = hitReason ? ` reason=${hitReason}` : '';
156
199
  let stopLabel = '';
157
- if (routingState?.stopMessageText && typeof routingState.stopMessageMaxRepeats === 'number') {
158
- const text = routingState.stopMessageText;
159
- const safeText = text.length > 24 ? `${text.slice(0, 21)}…` : text;
160
- const used = typeof routingState.stopMessageUsed === 'number' && Number.isFinite(routingState.stopMessageUsed)
161
- ? routingState.stopMessageUsed
162
- : 0;
163
- const updatedAt = typeof routingState.stopMessageUpdatedAt === 'number' && Number.isFinite(routingState.stopMessageUpdatedAt)
164
- ? routingState.stopMessageUpdatedAt
165
- : undefined;
166
- const lastUsedAt = typeof routingState.stopMessageLastUsedAt === 'number' && Number.isFinite(routingState.stopMessageLastUsedAt)
167
- ? routingState.stopMessageLastUsedAt
168
- : undefined;
169
- const parts = [`"${safeText}"`, `${used}/${Math.floor(routingState.stopMessageMaxRepeats)}`];
170
- if (updatedAt) {
171
- parts.push(`set=${new Date(updatedAt).toLocaleString(undefined, { hour12: false })}`);
200
+ const stop = summarizeStopMessageRuntime(routingState);
201
+ if (stop.hasAny) {
202
+ const parts = [
203
+ stop.safeText ? `"${stop.safeText}"` : '"(mode-only)"',
204
+ `mode=${stop.mode}`,
205
+ `round=${stop.maxRepeats > 0 ? `${stop.used}/${stop.maxRepeats}` : `${stop.used}/-`}`,
206
+ `active=${stop.active ? 'yes' : 'no'}`,
207
+ `left=${stop.remaining >= 0 ? stop.remaining : 'n/a'}`
208
+ ];
209
+ if (stop.updatedAt) {
210
+ parts.push(`set=${new Date(stop.updatedAt).toLocaleString(undefined, { hour12: false })}`);
172
211
  }
173
- if (lastUsedAt) {
174
- parts.push(`last=${new Date(lastUsedAt).toLocaleString(undefined, { hour12: false })}`);
212
+ if (stop.lastUsedAt) {
213
+ parts.push(`last=${new Date(stop.lastUsedAt).toLocaleString(undefined, { hour12: false })}`);
175
214
  }
176
215
  stopLabel = ` ${stopColor}[stopMessage:${parts.join(' ')}]${reset}`;
177
216
  }
@@ -184,13 +223,12 @@ export function formatVirtualRouterHit(routeName, poolId, providerKey, modelId,
184
223
  const stickyText = formatStickyScope(stickyScope);
185
224
  const stickyLabel = stickyText ? ` [sticky:${stickyText}]` : '';
186
225
  let stopLabel = '';
187
- if (routingState?.stopMessageText && typeof routingState.stopMessageMaxRepeats === 'number') {
188
- const text = routingState.stopMessageText;
189
- const safeText = text.length > 24 ? `${text.slice(0, 21)}…` : text;
190
- const used = typeof routingState.stopMessageUsed === 'number' && Number.isFinite(routingState.stopMessageUsed)
191
- ? routingState.stopMessageUsed
192
- : 0;
193
- stopLabel = ` [stopMessage:"${safeText}" ${used}/${Math.floor(routingState.stopMessageMaxRepeats)}]`;
226
+ const stop = summarizeStopMessageRuntime(routingState);
227
+ if (stop.hasAny) {
228
+ const safeText = stop.safeText ? `"${stop.safeText}"` : '"(mode-only)"';
229
+ const rounds = stop.maxRepeats > 0 ? `${stop.used}/${stop.maxRepeats}` : `${stop.used}/-`;
230
+ const left = stop.remaining >= 0 ? String(stop.remaining) : 'n/a';
231
+ stopLabel = ` [stopMessage:${safeText} mode=${stop.mode} round=${rounds} active=${stop.active ? 'yes' : 'no'} left=${left}]`;
194
232
  }
195
233
  return `[virtual-router-hit] ${timestamp} ${routeLabel} -> ${providerKey}${modelId ? '.' + modelId : ''}${stickyLabel}${hitReason ? ` reason=${hitReason}` : ''}${stopLabel}`;
196
234
  }
@@ -41,6 +41,8 @@ export function sortRoutePools(pools) {
41
41
  }
42
42
  export function buildRouteCandidates(requestedRoute, classificationCandidates, features, routing, providerRegistry) {
43
43
  const forceVision = routeHasForceFlag('vision', routing);
44
+ const hasMultimodalTargets = routeHasTargets(routing.multimodal);
45
+ const hasVisionTargets = routeHasTargets(routing.vision);
44
46
  const normalized = normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
45
47
  const baseList = [];
46
48
  if (classificationCandidates && classificationCandidates.length) {
@@ -51,35 +53,33 @@ export function buildRouteCandidates(requestedRoute, classificationCandidates, f
51
53
  else if (normalized) {
52
54
  baseList.push(normalized);
53
55
  }
54
- if (features.hasImageAttachment && !forceVision) {
55
- const visionAwareRoutes = [DEFAULT_ROUTE, 'thinking'];
56
- for (const routeName of visionAwareRoutes) {
57
- if (routeHasTargets(routing[routeName])) {
58
- if (!baseList.includes(routeName)) {
59
- baseList.push(routeName);
60
- }
61
- }
62
- }
63
- }
64
56
  if (features.hasImageAttachment) {
65
- const allRouteNames = Object.keys(routing);
66
- for (const routeName of allRouteNames) {
67
- if (!routeHasTargets(routing[routeName])) {
68
- continue;
57
+ if (hasMultimodalTargets) {
58
+ if (!baseList.includes('multimodal')) {
59
+ baseList.unshift('multimodal');
69
60
  }
70
- if (!routeSupportsModel(routeName, 'kimi-k2.5', routing, providerRegistry)) {
71
- continue;
61
+ }
62
+ else if (hasVisionTargets) {
63
+ if (!baseList.includes('vision')) {
64
+ baseList.unshift('vision');
72
65
  }
73
- if (!baseList.includes(routeName)) {
74
- baseList.push(routeName);
66
+ }
67
+ if (!forceVision && hasMultimodalTargets) {
68
+ const visionAwareRoutes = [DEFAULT_ROUTE, 'thinking'];
69
+ for (const routeName of visionAwareRoutes) {
70
+ if (routeHasTargets(routing[routeName])) {
71
+ if (!baseList.includes(routeName)) {
72
+ baseList.push(routeName);
73
+ }
74
+ }
75
75
  }
76
76
  }
77
77
  }
78
78
  let ordered = sortByPriority(baseList);
79
- if (features.hasImageAttachment && !forceVision) {
79
+ if (features.hasImageAttachment && !forceVision && hasMultimodalTargets) {
80
80
  ordered = reorderForInlineVision(ordered, routing, providerRegistry);
81
81
  }
82
- if (features.hasImageAttachment) {
82
+ if (features.hasImageAttachment && hasMultimodalTargets) {
83
83
  ordered = reorderForPreferredModel(ordered, 'kimi-k2.5', routing, providerRegistry);
84
84
  }
85
85
  const deduped = [];
@@ -384,7 +384,7 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
384
384
  }
385
385
  targets = filtered;
386
386
  }
387
- if (features.hasImageAttachment) {
387
+ if (features.hasImageAttachment && routeName === 'multimodal') {
388
388
  const kimiTargets = targets.filter((key) => {
389
389
  const modelId = getProviderModelId(key, deps.providerRegistry) ?? '';
390
390
  return modelId.trim().toLowerCase() === 'kimi-k2.5';
@@ -392,7 +392,7 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
392
392
  if (kimiTargets.length) {
393
393
  targets = kimiTargets;
394
394
  }
395
- else if (routeName === DEFAULT_ROUTE || routeName === 'thinking') {
395
+ else {
396
396
  const prioritized = [];
397
397
  const fallthrough = [];
398
398
  for (const key of targets) {
@@ -1,4 +1,4 @@
1
- import { type RoutingDecision, type RoutingDiagnostics, type StopMessageStateSnapshot, type RouterMetadataInput, type VirtualRouterConfig, type TargetMetadata, type ProviderFailureEvent, type ProviderErrorEvent, type ProviderSuccessEvent, type VirtualRouterHealthStore } from './types.js';
1
+ import { type RoutingDecision, type RoutingDiagnostics, type StopMessageStateSnapshot, type PreCommandStateSnapshot, type RouterMetadataInput, type VirtualRouterConfig, type TargetMetadata, type ProviderFailureEvent, type ProviderErrorEvent, type ProviderSuccessEvent, type VirtualRouterHealthStore } from './types.js';
2
2
  import type { ProcessedRequest, StandardizedRequest } from '../../conversion/hub/types/standardized.js';
3
3
  import { type RoutingInstructionState } from './routing-instructions.js';
4
4
  import type { ProviderQuotaView } from './types.js';
@@ -48,6 +48,7 @@ export declare class VirtualRouterEngine {
48
48
  diagnostics: RoutingDiagnostics;
49
49
  };
50
50
  getStopMessageState(metadata: RouterMetadataInput): StopMessageStateSnapshot | null;
51
+ getPreCommandState(metadata: RouterMetadataInput): PreCommandStateSnapshot | null;
51
52
  handleProviderFailure(event: ProviderFailureEvent): void;
52
53
  handleProviderError(event: ProviderErrorEvent): void;
53
54
  handleProviderSuccess(event: ProviderSuccessEvent): void;
@@ -70,6 +71,7 @@ export declare class VirtualRouterEngine {
70
71
  private providerHealthConfig;
71
72
  private resolveStickyKey;
72
73
  private resolveSessionScope;
74
+ private resolveInstructionProcessModeForSelection;
73
75
  private resolveInstructionTarget;
74
76
  private filterCandidatesByRoutingState;
75
77
  private selectFromCandidates;