@jsonstudio/llms 0.6.1892 → 0.6.2172

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 (159) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.js +16 -2
  2. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
  4. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
  5. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
  7. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
  8. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
  9. package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
  10. package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
  11. package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
  12. package/dist/conversion/compat/antigravity-session-signature.js +15 -0
  13. package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
  14. package/dist/conversion/compat/profiles/chat-glm.json +22 -0
  15. package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
  16. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
  17. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
  18. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
  19. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
  20. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
  22. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
  24. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
  25. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
  27. package/dist/conversion/hub/process/chat-process.js +85 -18
  28. package/dist/conversion/hub/response/provider-response.js +21 -50
  29. package/dist/conversion/hub/response/response-runtime.js +71 -10
  30. package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
  31. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
  32. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
  33. package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
  34. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
  35. package/dist/conversion/responses/responses-openai-bridge.js +193 -504
  36. package/dist/conversion/shared/anthropic-message-utils.js +82 -2
  37. package/dist/conversion/shared/bridge-message-utils.js +92 -39
  38. package/dist/conversion/shared/snapshot-hooks.js +8 -13
  39. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
  40. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
  41. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
  42. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
  43. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
  44. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
  45. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
  46. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
  47. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
  48. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
  49. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
  50. package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
  51. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
  52. package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
  53. package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
  54. package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
  55. package/dist/conversion/shared/tool-governor.js +136 -10
  56. package/dist/filters/utils/snapshot-writer.js +3 -3
  57. package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
  58. package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
  59. package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
  60. package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
  61. package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
  62. package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
  63. package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
  64. package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
  65. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
  66. package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
  67. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
  68. package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
  69. package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
  70. package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
  71. package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
  72. package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
  73. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
  74. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
  75. package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
  76. package/dist/router/virtual-router/bootstrap/utils.js +41 -0
  77. package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
  78. package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
  79. package/dist/router/virtual-router/bootstrap.d.ts +0 -4
  80. package/dist/router/virtual-router/bootstrap.js +31 -1275
  81. package/dist/router/virtual-router/classifier.js +32 -14
  82. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
  83. package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
  84. package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
  85. package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
  86. package/dist/router/virtual-router/engine/route-analytics.js +44 -0
  87. package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
  88. package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
  89. package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
  90. package/dist/router/virtual-router/engine-logging.d.ts +42 -1
  91. package/dist/router/virtual-router/engine-logging.js +82 -15
  92. package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
  93. package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
  94. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
  95. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
  96. package/dist/router/virtual-router/engine-selection/tier-selection.js +31 -1
  97. package/dist/router/virtual-router/engine.d.ts +21 -7
  98. package/dist/router/virtual-router/engine.js +198 -194
  99. package/dist/router/virtual-router/features.js +12 -4
  100. package/dist/router/virtual-router/message-utils.d.ts +8 -0
  101. package/dist/router/virtual-router/message-utils.js +170 -45
  102. package/dist/router/virtual-router/pre-command-file-resolver.js +40 -2
  103. package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
  104. package/dist/router/virtual-router/routing-instructions.js +18 -2
  105. package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
  106. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
  107. package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
  108. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  109. package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
  110. package/dist/router/virtual-router/token-counter.js +51 -10
  111. package/dist/router/virtual-router/tool-signals.js +4 -0
  112. package/dist/router/virtual-router/types.d.ts +15 -0
  113. package/dist/servertool/clock/session-scope.d.ts +3 -0
  114. package/dist/servertool/clock/session-scope.js +52 -0
  115. package/dist/servertool/clock/state.js +9 -0
  116. package/dist/servertool/clock/tasks.js +12 -1
  117. package/dist/servertool/clock/types.d.ts +3 -0
  118. package/dist/servertool/engine.js +177 -31
  119. package/dist/servertool/handlers/clock-auto.js +2 -8
  120. package/dist/servertool/handlers/clock.js +6 -9
  121. package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
  122. package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
  123. package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
  124. package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
  125. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
  126. package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
  127. package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
  128. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
  129. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
  130. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -1
  131. package/dist/servertool/handlers/stop-message-auto.js +80 -556
  132. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
  133. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
  134. package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
  135. package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
  136. package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
  137. package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
  138. package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
  139. package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
  140. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
  141. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
  142. package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
  143. package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
  144. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
  145. package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
  146. package/dist/servertool/handlers/web-search.js +117 -0
  147. package/dist/servertool/server-side-tools.d.ts +0 -1
  148. package/dist/servertool/server-side-tools.js +4 -3
  149. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  150. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
  151. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
  152. package/dist/telemetry/stats-center.d.ts +9 -0
  153. package/dist/telemetry/stats-center.js +29 -1
  154. package/dist/tools/apply-patch/structured/coercion.js +3 -11
  155. package/dist/tools/exec-command/validator.d.ts +1 -0
  156. package/dist/tools/exec-command/validator.js +132 -0
  157. package/dist/tools/tool-registry.d.ts +1 -0
  158. package/dist/tools/tool-registry.js +1 -1
  159. package/package.json +1 -1
@@ -1,10 +1,51 @@
1
1
  import { type ClassificationResult, type RoutingFeatures, type RoutingInstructionMode, type VirtualRouterContextRoutingConfig } from './types.js';
2
2
  import { ProviderRegistry } from './provider-registry.js';
3
3
  import type { RoutingInstructionState } from './routing-instructions.js';
4
+ import type { VirtualRouterHitEvent } from '../../telemetry/stats-center.js';
4
5
  type LoggingDeps = {
5
6
  providerRegistry: ProviderRegistry;
6
7
  contextRouting: VirtualRouterContextRoutingConfig | undefined;
7
8
  };
9
+ export type StopMessageRuntimeSummary = {
10
+ hasAny: boolean;
11
+ safeText?: string;
12
+ mode: 'on' | 'off' | 'auto' | 'unset';
13
+ maxRepeats: number;
14
+ used: number;
15
+ remaining: number;
16
+ active: boolean;
17
+ updatedAt?: number;
18
+ lastUsedAt?: number;
19
+ };
20
+ export type VirtualRouterHitRecord = {
21
+ timestampMs: number;
22
+ routeName: string;
23
+ poolId?: string;
24
+ providerKey: string;
25
+ modelId?: string;
26
+ hitReason?: string;
27
+ stickyScope?: string;
28
+ requestTokens?: number;
29
+ selectionPenalty?: number;
30
+ stopMessage: StopMessageRuntimeSummary;
31
+ };
32
+ export type VirtualRouterHitEventMeta = {
33
+ requestId: string;
34
+ entryEndpoint?: string;
35
+ };
36
+ export declare function createVirtualRouterHitRecord(input: {
37
+ routeName: string;
38
+ poolId?: string;
39
+ providerKey: string;
40
+ modelId?: string;
41
+ hitReason?: string;
42
+ stickyScope?: string;
43
+ routingState?: RoutingInstructionState;
44
+ requestTokens?: number;
45
+ selectionPenalty?: number;
46
+ timestampMs?: number;
47
+ }): VirtualRouterHitRecord;
48
+ export declare function toVirtualRouterHitEvent(record: VirtualRouterHitRecord, meta: VirtualRouterHitEventMeta): VirtualRouterHitEvent;
8
49
  export declare function formatStickyScope(scope?: string): string | undefined;
9
50
  export declare function parseProviderKey(providerKey: string): {
10
51
  providerId: string;
@@ -16,5 +57,5 @@ export declare function describeTargetProvider(providerKey: string, fallbackMode
16
57
  resolvedModel?: string;
17
58
  };
18
59
  export declare function buildHitReason(routeUsed: string, providerKey: string, classification: ClassificationResult, features: RoutingFeatures, mode: RoutingInstructionMode | undefined, deps: LoggingDeps): string;
19
- export declare function formatVirtualRouterHit(routeName: string, poolId: string | undefined, providerKey: string, modelId?: string, hitReason?: string, stickyScope?: string, routingState?: RoutingInstructionState): string;
60
+ export declare function formatVirtualRouterHit(record: VirtualRouterHitRecord): string;
20
61
  export {};
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_MODEL_CONTEXT_TOKENS, DEFAULT_ROUTE } from './types.js';
2
+ const DEFAULT_STOP_MESSAGE_MAX_REPEATS = 10;
2
3
  function summarizeStopMessageRuntime(state) {
3
4
  if (!state) {
4
5
  return {
@@ -14,9 +15,14 @@ function summarizeStopMessageRuntime(state) {
14
15
  const safeText = text ? (text.length > 24 ? `${text.slice(0, 21)}…` : text) : undefined;
15
16
  const modeRaw = typeof state.stopMessageStageMode === 'string' ? state.stopMessageStageMode.trim().toLowerCase() : '';
16
17
  const mode = modeRaw === 'on' || modeRaw === 'off' || modeRaw === 'auto' ? modeRaw : 'unset';
17
- const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
18
+ const parsedMaxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
18
19
  ? Math.max(0, Math.floor(state.stopMessageMaxRepeats))
19
20
  : 0;
21
+ const maxRepeats = parsedMaxRepeats > 0
22
+ ? parsedMaxRepeats
23
+ : mode === 'on' || mode === 'auto'
24
+ ? DEFAULT_STOP_MESSAGE_MAX_REPEATS
25
+ : 0;
20
26
  const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
21
27
  ? Math.max(0, Math.floor(state.stopMessageUsed))
22
28
  : 0;
@@ -41,6 +47,55 @@ function summarizeStopMessageRuntime(state) {
41
47
  ...(lastUsedAt ? { lastUsedAt } : {})
42
48
  };
43
49
  }
50
+ function normalizePositiveInteger(value) {
51
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
52
+ return undefined;
53
+ }
54
+ return Math.floor(value);
55
+ }
56
+ function normalizeRoundedInteger(value) {
57
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
58
+ return undefined;
59
+ }
60
+ return Math.max(0, Math.round(value));
61
+ }
62
+ export function createVirtualRouterHitRecord(input) {
63
+ return {
64
+ timestampMs: typeof input.timestampMs === 'number' && Number.isFinite(input.timestampMs)
65
+ ? input.timestampMs
66
+ : Date.now(),
67
+ routeName: input.routeName,
68
+ ...(input.poolId ? { poolId: input.poolId } : {}),
69
+ providerKey: input.providerKey,
70
+ ...(input.modelId ? { modelId: input.modelId } : {}),
71
+ ...(input.hitReason ? { hitReason: input.hitReason } : {}),
72
+ ...(input.stickyScope ? { stickyScope: input.stickyScope } : {}),
73
+ ...(typeof normalizeRoundedInteger(input.requestTokens) === 'number'
74
+ ? { requestTokens: normalizeRoundedInteger(input.requestTokens) }
75
+ : {}),
76
+ ...(typeof normalizePositiveInteger(input.selectionPenalty) === 'number'
77
+ ? { selectionPenalty: normalizePositiveInteger(input.selectionPenalty) }
78
+ : {}),
79
+ stopMessage: summarizeStopMessageRuntime(input.routingState)
80
+ };
81
+ }
82
+ export function toVirtualRouterHitEvent(record, meta) {
83
+ return {
84
+ requestId: meta.requestId,
85
+ timestamp: record.timestampMs,
86
+ entryEndpoint: meta.entryEndpoint || '/v1/chat/completions',
87
+ routeName: record.routeName,
88
+ pool: record.poolId || record.routeName,
89
+ providerKey: record.providerKey,
90
+ ...(record.modelId ? { modelId: record.modelId } : {}),
91
+ ...(record.hitReason ? { reason: record.hitReason } : {}),
92
+ ...(typeof record.requestTokens === 'number' ? { requestTokens: record.requestTokens } : {}),
93
+ ...(typeof record.selectionPenalty === 'number' ? { selectionPenalty: record.selectionPenalty } : {}),
94
+ stopMessageActive: record.stopMessage.active,
95
+ ...(record.stopMessage.mode !== 'unset' ? { stopMessageMode: record.stopMessage.mode } : {}),
96
+ ...(record.stopMessage.remaining >= 0 ? { stopMessageRemaining: record.stopMessage.remaining } : {})
97
+ };
98
+ }
44
99
  export function formatStickyScope(scope) {
45
100
  if (!scope || scope.trim().length === 0) {
46
101
  return undefined;
@@ -175,9 +230,9 @@ export function buildHitReason(routeUsed, providerKey, classification, features,
175
230
  }
176
231
  return base;
177
232
  }
178
- export function formatVirtualRouterHit(routeName, poolId, providerKey, modelId, hitReason, stickyScope, routingState) {
233
+ export function formatVirtualRouterHit(record) {
179
234
  try {
180
- const now = new Date();
235
+ const now = new Date(record.timestampMs);
181
236
  const hours = String(now.getHours()).padStart(2, '0');
182
237
  const minutes = String(now.getMinutes()).padStart(2, '0');
183
238
  const seconds = String(now.getSeconds()).padStart(2, '0');
@@ -186,18 +241,24 @@ export function formatVirtualRouterHit(routeName, poolId, providerKey, modelId,
186
241
  const reset = '\x1b[0m';
187
242
  const timeColor = '\x1b[90m';
188
243
  const stickyColor = '\x1b[33m';
189
- const routeColor = resolveRouteColor(routeName);
244
+ const routeColor = resolveRouteColor(record.routeName);
190
245
  const stopColor = '\x1b[38;5;214m';
191
246
  const prefix = `${prefixColor}[virtual-router-hit]${reset}`;
192
247
  const timeLabel = `${timeColor}${timestamp}${reset}`;
193
- const { providerLabel, resolvedModel } = describeTargetProvider(providerKey, modelId);
194
- const routeLabel = poolId ? `${routeName}/${poolId}` : routeName;
248
+ const { providerLabel, resolvedModel } = describeTargetProvider(record.providerKey, record.modelId);
249
+ const routeLabel = record.poolId ? `${record.routeName}/${record.poolId}` : record.routeName;
195
250
  const targetLabel = `${routeLabel} -> ${providerLabel}${resolvedModel ? '.' + resolvedModel : ''}`;
196
- const stickyText = formatStickyScope(stickyScope);
251
+ const stickyText = formatStickyScope(record.stickyScope);
197
252
  const stickyLabel = stickyText ? ` ${stickyColor}[sticky:${stickyText}]${reset}` : '';
198
- const reasonLabel = hitReason ? ` reason=${hitReason}` : '';
253
+ const reasonLabel = record.hitReason ? ` reason=${record.hitReason}` : '';
254
+ const requestTokenLabel = typeof record.requestTokens === 'number' && Number.isFinite(record.requestTokens)
255
+ ? ` reqTokens=${Math.max(0, Math.round(record.requestTokens))}`
256
+ : '';
257
+ const penaltyLabel = typeof record.selectionPenalty === 'number' && Number.isFinite(record.selectionPenalty) && record.selectionPenalty > 0
258
+ ? ` penalty=${Math.floor(record.selectionPenalty)}`
259
+ : '';
199
260
  let stopLabel = '';
200
- const stop = summarizeStopMessageRuntime(routingState);
261
+ const stop = record.stopMessage;
201
262
  if (stop.hasAny) {
202
263
  const parts = [
203
264
  stop.safeText ? `"${stop.safeText}"` : '"(mode-only)"',
@@ -214,22 +275,28 @@ export function formatVirtualRouterHit(routeName, poolId, providerKey, modelId,
214
275
  }
215
276
  stopLabel = ` ${stopColor}[stopMessage:${parts.join(' ')}]${reset}`;
216
277
  }
217
- return `${prefix} ${timeLabel} ${routeColor}${targetLabel}${stickyLabel}${reasonLabel}${stopLabel}${reset}`;
278
+ return `${prefix} ${timeLabel} ${routeColor}${targetLabel}${stickyLabel}${reasonLabel}${requestTokenLabel}${penaltyLabel}${stopLabel}${reset}`;
218
279
  }
219
280
  catch {
220
- const now = new Date();
281
+ const now = new Date(record.timestampMs);
221
282
  const timestamp = now.toLocaleTimeString('zh-CN', { hour12: false });
222
- const routeLabel = poolId ? `${routeName}/${poolId}` : routeName;
223
- const stickyText = formatStickyScope(stickyScope);
283
+ const routeLabel = record.poolId ? `${record.routeName}/${record.poolId}` : record.routeName;
284
+ const stickyText = formatStickyScope(record.stickyScope);
224
285
  const stickyLabel = stickyText ? ` [sticky:${stickyText}]` : '';
286
+ const requestTokenLabel = typeof record.requestTokens === 'number' && Number.isFinite(record.requestTokens)
287
+ ? ` reqTokens=${Math.max(0, Math.round(record.requestTokens))}`
288
+ : '';
289
+ const penaltyLabel = typeof record.selectionPenalty === 'number' && Number.isFinite(record.selectionPenalty) && record.selectionPenalty > 0
290
+ ? ` penalty=${Math.floor(record.selectionPenalty)}`
291
+ : '';
225
292
  let stopLabel = '';
226
- const stop = summarizeStopMessageRuntime(routingState);
293
+ const stop = record.stopMessage;
227
294
  if (stop.hasAny) {
228
295
  const safeText = stop.safeText ? `"${stop.safeText}"` : '"(mode-only)"';
229
296
  const rounds = stop.maxRepeats > 0 ? `${stop.used}/${stop.maxRepeats}` : `${stop.used}/-`;
230
297
  const left = stop.remaining >= 0 ? String(stop.remaining) : 'n/a';
231
298
  stopLabel = ` [stopMessage:${safeText} mode=${stop.mode} round=${rounds} active=${stop.active ? 'yes' : 'no'} left=${left}]`;
232
299
  }
233
- return `[virtual-router-hit] ${timestamp} ${routeLabel} -> ${providerKey}${modelId ? '.' + modelId : ''}${stickyLabel}${hitReason ? ` reason=${hitReason}` : ''}${stopLabel}`;
300
+ return `[virtual-router-hit] ${timestamp} ${routeLabel} -> ${record.providerKey}${record.modelId ? '.' + record.modelId : ''}${stickyLabel}${record.hitReason ? ` reason=${record.hitReason}` : ''}${requestTokenLabel}${penaltyLabel}${stopLabel}`;
234
301
  }
235
302
  }
@@ -0,0 +1,3 @@
1
+ import type { ProviderRegistry } from '../provider-registry.js';
2
+ import type { RoutingFeatures } from '../types.js';
3
+ export declare function providerSupportsMultimodalRequest(providerKey: string, features: RoutingFeatures, providerRegistry: ProviderRegistry): boolean;
@@ -0,0 +1,26 @@
1
+ import { extractProviderId, getProviderModelId } from './key-parsing.js';
2
+ function isQwen35PlusProvider(providerKey, providerRegistry) {
3
+ const providerId = (extractProviderId(providerKey) ?? '').trim().toLowerCase();
4
+ if (providerId !== 'qwen') {
5
+ return false;
6
+ }
7
+ const modelId = (getProviderModelId(providerKey, providerRegistry) ?? '').trim().toLowerCase();
8
+ if (!modelId) {
9
+ return false;
10
+ }
11
+ return modelId === 'qwen3.5-plus' || modelId === 'qwen3-5-plus' || modelId === 'qwen3_5-plus';
12
+ }
13
+ export function providerSupportsMultimodalRequest(providerKey, features, providerRegistry) {
14
+ if (!features.hasImageAttachment) {
15
+ return true;
16
+ }
17
+ if (!isQwen35PlusProvider(providerKey, providerRegistry)) {
18
+ return true;
19
+ }
20
+ if (features.hasVideoAttachment !== true) {
21
+ return true;
22
+ }
23
+ const hasRemoteVideo = features.hasRemoteVideoAttachment === true;
24
+ const hasLocalVideo = features.hasLocalVideoAttachment === true;
25
+ return hasRemoteVideo && !hasLocalVideo;
26
+ }
@@ -43,6 +43,10 @@ export function buildRouteCandidates(requestedRoute, classificationCandidates, f
43
43
  const forceVision = routeHasForceFlag('vision', routing);
44
44
  const hasMultimodalTargets = routeHasTargets(routing.multimodal);
45
45
  const hasVisionTargets = routeHasTargets(routing.vision);
46
+ const hasLocalVideoAttachment = features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true;
47
+ if (features.hasImageAttachment && hasLocalVideoAttachment && hasVisionTargets) {
48
+ return ['vision'];
49
+ }
46
50
  const normalized = normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
47
51
  const baseList = [];
48
52
  if (classificationCandidates && classificationCandidates.length) {
@@ -59,9 +63,9 @@ export function buildRouteCandidates(requestedRoute, classificationCandidates, f
59
63
  baseList.unshift('multimodal');
60
64
  }
61
65
  }
62
- else if (hasVisionTargets) {
66
+ if (hasVisionTargets) {
63
67
  if (!baseList.includes('vision')) {
64
- baseList.unshift('vision');
68
+ baseList.push('vision');
65
69
  }
66
70
  }
67
71
  if (!forceVision && hasMultimodalTargets) {
@@ -10,6 +10,7 @@ export type SelectionDeps = {
10
10
  contextAdvisor: ContextAdvisor;
11
11
  loadBalancer: RouteLoadBalancer;
12
12
  isProviderCoolingDown: (providerKey: string) => boolean;
13
+ getProviderCooldownRemainingMs?: (providerKey: string) => number;
13
14
  resolveStickyKey: (metadata: RouterMetadataInput) => string | undefined;
14
15
  quotaView?: ProviderQuotaView;
15
16
  aliasQueueStore?: Map<string, string[]>;
@@ -3,6 +3,7 @@ import { resolveContextWeightedConfig } from '../context-weighted.js';
3
3
  import { resolveHealthWeightedConfig } from '../health-weighted.js';
4
4
  import { pinCandidatesByAliasQueue, resolveAliasSelectionStrategy } from './alias-selection.js';
5
5
  import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './key-parsing.js';
6
+ import { providerSupportsMultimodalRequest } from './multimodal-capability.js';
6
7
  import { selectProviderKeyFromCandidatePool } from './tier-selection-select.js';
7
8
  import { lookupAntigravityPinnedAliasForSessionId, unpinAntigravitySessionAliasForSessionId } from '../../../conversion/compat/antigravity-session-signature.js';
8
9
  const DEFAULT_ANTIGRAVITY_ALIAS_SESSION_COOLDOWN_MS = 5 * 60_000;
@@ -190,6 +191,9 @@ function applyAntigravityAliasSessionLeases(targets, deps, metadata) {
190
191
  const strictBinding = strictRequested && Boolean(pinnedLeaseKey);
191
192
  const geminiSessionKey = buildScopedSessionKey(sessionKey);
192
193
  let preferredGeminiRuntimeKey = pinnedLeaseKey || sessionAliasStore.get(geminiSessionKey);
194
+ if (preferredGeminiRuntimeKey && !preferredGeminiRuntimeKey.includes('::')) {
195
+ preferredGeminiRuntimeKey = buildAntigravityLeaseRuntimeKey(preferredGeminiRuntimeKey);
196
+ }
193
197
  if (preferredGeminiRuntimeKey && !pinnedLeaseKey) {
194
198
  const lease = leaseStore.get(preferredGeminiRuntimeKey);
195
199
  if (lease && lease.sessionKey !== geminiSessionKey && now - lease.lastSeenAt < cooldownMs) {
@@ -264,7 +268,7 @@ function applyAntigravityAliasSessionLeases(targets, deps, metadata) {
264
268
  blocked += 1;
265
269
  return false;
266
270
  });
267
- return { targets: filtered, blocked, preferredPinned, pinnedStrict };
271
+ return { targets: filtered, blocked, preferredPinned, pinnedStrict, preferredRuntimeKey: preferredGeminiRuntimeKey };
268
272
  }
269
273
  export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features, deps, options) {
270
274
  const { disabledProviders, disabledKeysMap, allowedProviders, disabledModels, requiredProviderKeys } = options;
@@ -385,6 +389,7 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
385
389
  targets = filtered;
386
390
  }
387
391
  if (features.hasImageAttachment && routeName === 'multimodal') {
392
+ targets = targets.filter((key) => providerSupportsMultimodalRequest(key, features, deps.providerRegistry));
388
393
  const kimiTargets = targets.filter((key) => {
389
394
  const modelId = getProviderModelId(key, deps.providerRegistry) ?? '';
390
395
  return modelId.trim().toLowerCase() === 'kimi-k2.5';
@@ -448,6 +453,31 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
448
453
  const candidatesForSelect = avoidAntigravityOnRetry
449
454
  ? preferNonAntigravityWhenPossible(candidatePool)
450
455
  : candidatePool;
456
+ if (leaseResult.preferredRuntimeKey && !leaseResult.pinnedStrict) {
457
+ const preferredCandidates = candidatesForSelect.filter((key) => isAntigravityGeminiModelKey(key, deps) && extractLeaseRuntimeKey(key, deps) === leaseResult.preferredRuntimeKey);
458
+ if (preferredCandidates.length > 0) {
459
+ const preferredProviderKey = selectProviderKeyFromCandidatePool({
460
+ routeName,
461
+ tier,
462
+ stickyKey,
463
+ candidates: preferredCandidates,
464
+ isSafePool,
465
+ deps,
466
+ options,
467
+ contextResult,
468
+ warnRatio,
469
+ excludedKeys,
470
+ isRecoveryAttempt,
471
+ now,
472
+ nowForWeights,
473
+ healthWeightedCfg,
474
+ contextWeightedCfg
475
+ });
476
+ if (preferredProviderKey) {
477
+ return { providerKey: preferredProviderKey, poolTargets: tier.targets, tierId: tier.id };
478
+ }
479
+ }
480
+ }
451
481
  const providerKey = selectProviderKeyFromCandidatePool({
452
482
  routeName,
453
483
  tier,
@@ -11,16 +11,14 @@ export declare class VirtualRouterEngine {
11
11
  private routing;
12
12
  private readonly providerRegistry;
13
13
  private readonly healthManager;
14
- private readonly providerCooldowns;
14
+ private get providerCooldowns();
15
15
  private loadBalancer;
16
16
  private classifier;
17
17
  private readonly contextAdvisor;
18
18
  private contextRouting;
19
- private routeStats;
20
- private readonly aliasQueueStore;
21
- private readonly antigravityAliasLeaseStore;
22
- private readonly antigravitySessionAliasStore;
23
- private antigravityAliasReuseCooldownMs;
19
+ private readonly routeAnalytics;
20
+ private stickySessionManager;
21
+ private cooldownManager;
24
22
  private antigravityLeasePersistence;
25
23
  private readonly debug;
26
24
  private healthConfig;
@@ -30,6 +28,11 @@ export declare class VirtualRouterEngine {
30
28
  private routingStateStore;
31
29
  private routingInstructionState;
32
30
  private quotaView?;
31
+ /**
32
+ * Backward-compatible test/debug surface used by existing regression scripts.
33
+ * Keep this as a read-only view over StickySessionManager storage.
34
+ */
35
+ get antigravitySessionAliasStore(): Map<string, string>;
33
36
  constructor(deps?: {
34
37
  healthStore?: VirtualRouterHealthStore;
35
38
  routingStateStore?: RoutingInstructionStateStore;
@@ -41,6 +44,7 @@ export declare class VirtualRouterEngine {
41
44
  quotaView?: ProviderQuotaView | null;
42
45
  }): void;
43
46
  private parseDirectProviderModel;
47
+ private shouldFallbackDirectModelForMedia;
44
48
  initialize(config: VirtualRouterConfig): void;
45
49
  route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
46
50
  target: TargetMetadata;
@@ -57,6 +61,15 @@ export declare class VirtualRouterEngine {
57
61
  providers: string[];
58
62
  hits: number;
59
63
  lastUsedProvider?: string;
64
+ lastHit?: {
65
+ timestampMs: number;
66
+ reason?: string;
67
+ requestTokens?: number;
68
+ selectionPenalty?: number;
69
+ stopMessageActive: boolean;
70
+ stopMessageMode?: "on" | "off" | "auto";
71
+ stopMessageRemaining?: number;
72
+ };
60
73
  }>;
61
74
  health: import("./types.js").ProviderHealthState[];
62
75
  };
@@ -67,7 +80,7 @@ export declare class VirtualRouterEngine {
67
80
  private normalizeRouteAlias;
68
81
  private validateConfig;
69
82
  private selectProvider;
70
- private incrementRouteStat;
83
+ private resolveSelectionPenalty;
71
84
  private providerHealthConfig;
72
85
  private resolveStickyKey;
73
86
  private resolveSessionScope;
@@ -105,6 +118,7 @@ export declare class VirtualRouterEngine {
105
118
  private markProviderCooldown;
106
119
  private clearProviderCooldown;
107
120
  private isProviderCoolingDown;
121
+ private getProviderCooldownRemainingMs;
108
122
  private restoreHealthFromStore;
109
123
  private buildHealthSnapshot;
110
124
  private persistHealthSnapshot;