@jsonstudio/llms 0.6.230 → 0.6.467

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 (81) hide show
  1. package/README.md +2 -0
  2. package/dist/conversion/codecs/gemini-openai-codec.js +24 -2
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  4. package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  5. package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  6. package/dist/conversion/compat/actions/glm-image-content.js +83 -0
  7. package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  8. package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  9. package/dist/conversion/compat/actions/glm-web-search.js +25 -28
  10. package/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
  11. package/dist/conversion/compat/actions/iflow-web-search.js +87 -0
  12. package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  13. package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  14. package/dist/conversion/compat/profiles/chat-glm.json +194 -184
  15. package/dist/conversion/compat/profiles/chat-iflow.json +199 -195
  16. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  17. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  18. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  19. package/dist/conversion/config/sample-config.json +1 -1
  20. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  21. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline.js +32 -1
  23. package/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
  25. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
  26. package/dist/conversion/hub/pipeline/target-utils.js +6 -0
  27. package/dist/conversion/hub/process/chat-process.js +186 -40
  28. package/dist/conversion/hub/response/provider-response.d.ts +13 -1
  29. package/dist/conversion/hub/response/provider-response.js +84 -35
  30. package/dist/conversion/hub/response/server-side-tools.js +61 -4
  31. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
  32. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  33. package/dist/conversion/hub/standardized-bridge.js +14 -0
  34. package/dist/conversion/responses/responses-openai-bridge.js +110 -6
  35. package/dist/conversion/shared/anthropic-message-utils.js +133 -9
  36. package/dist/conversion/shared/bridge-message-utils.js +137 -10
  37. package/dist/conversion/shared/errors.d.ts +20 -0
  38. package/dist/conversion/shared/errors.js +28 -0
  39. package/dist/conversion/shared/responses-conversation-store.js +30 -3
  40. package/dist/conversion/shared/responses-output-builder.js +111 -8
  41. package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  42. package/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +103 -3
  44. package/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
  45. package/dist/filters/special/response-tool-text-canonicalize.js +27 -3
  46. package/dist/router/virtual-router/bootstrap.js +44 -12
  47. package/dist/router/virtual-router/classifier.js +13 -17
  48. package/dist/router/virtual-router/engine.d.ts +39 -0
  49. package/dist/router/virtual-router/engine.js +755 -55
  50. package/dist/router/virtual-router/features.js +1 -1
  51. package/dist/router/virtual-router/message-utils.js +36 -24
  52. package/dist/router/virtual-router/provider-registry.d.ts +15 -0
  53. package/dist/router/virtual-router/provider-registry.js +42 -1
  54. package/dist/router/virtual-router/routing-instructions.d.ts +34 -0
  55. package/dist/router/virtual-router/routing-instructions.js +383 -0
  56. package/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
  57. package/dist/router/virtual-router/sticky-session-store.js +110 -0
  58. package/dist/router/virtual-router/token-counter.js +14 -3
  59. package/dist/router/virtual-router/tool-signals.js +0 -22
  60. package/dist/router/virtual-router/types.d.ts +80 -0
  61. package/dist/router/virtual-router/types.js +2 -1
  62. package/dist/servertool/engine.d.ts +27 -0
  63. package/dist/servertool/engine.js +101 -0
  64. package/dist/servertool/flow-types.d.ts +40 -0
  65. package/dist/servertool/flow-types.js +1 -0
  66. package/dist/servertool/handlers/vision.d.ts +1 -0
  67. package/dist/servertool/handlers/vision.js +194 -0
  68. package/dist/servertool/handlers/web-search.d.ts +1 -0
  69. package/dist/servertool/handlers/web-search.js +791 -0
  70. package/dist/servertool/orchestration-types.d.ts +33 -0
  71. package/dist/servertool/orchestration-types.js +1 -0
  72. package/dist/servertool/registry.d.ts +18 -0
  73. package/dist/servertool/registry.js +27 -0
  74. package/dist/servertool/server-side-tools.d.ts +8 -0
  75. package/dist/servertool/server-side-tools.js +208 -0
  76. package/dist/servertool/types.d.ts +94 -0
  77. package/dist/servertool/types.js +1 -0
  78. package/dist/servertool/vision-tool.d.ts +2 -0
  79. package/dist/servertool/vision-tool.js +185 -0
  80. package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  81. package/package.json +1 -1
@@ -111,6 +111,7 @@ function applyLocalToolGovernance(chatRequest, rawPayload) {
111
111
  }
112
112
  const hasImageHint = detectImageHint(messages, rawPayload);
113
113
  if (hasImageHint) {
114
+ // 有图片线索时不干预工具列表,保持由上游(Codex 等)决定 view_image 的暴露与使用。
114
115
  return chatRequest;
115
116
  }
116
117
  const filteredTools = tools.filter((tool) => {
@@ -0,0 +1,13 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * Ensure assistant.tool_calls[].function.arguments is a JSON string containing valid JSON.
4
+ * - If arguments is not a string, JSON.stringify it.
5
+ * - If arguments is a string but not parseable as JSON, wrap it into a JSON object so the
6
+ * provider always receives syntactically valid JSON (e.g. {"input": "<raw>"}).
7
+ * Also set assistant.content=null when tool_calls exist (request-side invariant).
8
+ */
9
+ export declare class RequestToolCallsStringifyFilter implements Filter<JsonObject> {
10
+ readonly name = "request_toolcalls_stringify";
11
+ readonly stage: FilterContext['stage'];
12
+ apply(input: JsonObject): FilterResult<JsonObject>;
13
+ }
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Ensure assistant.tool_calls[].function.arguments is a JSON string.
2
+ * Ensure assistant.tool_calls[].function.arguments is a JSON string containing valid JSON.
3
+ * - If arguments is not a string, JSON.stringify it.
4
+ * - If arguments is a string but not parseable as JSON, wrap it into a JSON object so the
5
+ * provider always receives syntactically valid JSON (e.g. {"input": "<raw>"}).
3
6
  * Also set assistant.content=null when tool_calls exist (request-side invariant).
4
7
  */
5
8
  export class RequestToolCallsStringifyFilter {
@@ -18,14 +21,111 @@ export class RequestToolCallsStringifyFilter {
18
21
  if (!tc || typeof tc !== 'object')
19
22
  continue;
20
23
  const fn = tc.function || {};
21
- if (fn && typeof fn === 'object' && fn.arguments !== undefined && typeof fn.arguments !== 'string') {
24
+ if (!fn || typeof fn !== 'object')
25
+ continue;
26
+ const currentArgs = fn.arguments;
27
+ const fnName = typeof fn.name === 'string' ? fn.name.trim() : '';
28
+ // Case 1: non-string arguments → stringify directly
29
+ if (currentArgs !== undefined && typeof currentArgs !== 'string') {
22
30
  try {
23
- fn.arguments = JSON.stringify(fn.arguments ?? {});
31
+ fn.arguments = JSON.stringify(currentArgs ?? {});
24
32
  }
25
33
  catch {
26
34
  fn.arguments = '{}';
27
35
  }
28
36
  tc.function = fn;
37
+ continue;
38
+ }
39
+ // Case 2: string arguments → ensure it is valid JSON
40
+ if (typeof currentArgs === 'string') {
41
+ const trimmed = currentArgs.trim();
42
+ if (trimmed.length === 0) {
43
+ fn.arguments = '{}';
44
+ tc.function = fn;
45
+ continue;
46
+ }
47
+ let parsedOk = false;
48
+ let parsedValue = undefined;
49
+ try {
50
+ parsedValue = JSON.parse(trimmed);
51
+ parsedOk = true;
52
+ }
53
+ catch {
54
+ parsedOk = false;
55
+ }
56
+ if (!parsedOk) {
57
+ // Wrap raw string into a JSON object to keep payload syntactically valid.
58
+ // For shell, align with GLM/统一工具治理约定,优先映射到 { command },
59
+ // 其余模型仍使用 { input } 形式。
60
+ try {
61
+ if (fnName === 'shell') {
62
+ fn.arguments = JSON.stringify({ command: currentArgs });
63
+ }
64
+ else if (fnName === 'apply_patch') {
65
+ fn.arguments = JSON.stringify({ patch: currentArgs });
66
+ }
67
+ else {
68
+ fn.arguments = JSON.stringify({ input: currentArgs });
69
+ }
70
+ }
71
+ catch {
72
+ fn.arguments = '{}';
73
+ }
74
+ tc.function = fn;
75
+ continue;
76
+ }
77
+ // 已经是合法 JSON 的场景下,仅对特定工具做形状修复。
78
+ if (parsedOk && fnName === 'apply_patch') {
79
+ try {
80
+ let obj = parsedValue;
81
+ // 1) 若整体是字符串,则视为补丁文本
82
+ if (typeof obj === 'string') {
83
+ obj = { patch: obj };
84
+ }
85
+ if (obj && typeof obj === 'object') {
86
+ const container = obj;
87
+ const rawPatch = container.patch;
88
+ const rawInput = container.input;
89
+ // 2) 若 patch 是形如 JSON 的字符串,尝试解包 {"input": "..."} 或 {"patch": "..."}
90
+ if (typeof rawPatch === 'string') {
91
+ const ptrim = rawPatch.trim();
92
+ if (ptrim.startsWith('{') && ptrim.endsWith('}')) {
93
+ try {
94
+ const inner = JSON.parse(ptrim);
95
+ if (typeof inner.patch === 'string') {
96
+ container.patch = inner.patch;
97
+ }
98
+ else if (typeof inner.input === 'string') {
99
+ container.patch = inner.input;
100
+ }
101
+ else {
102
+ container.patch = ptrim;
103
+ }
104
+ }
105
+ catch {
106
+ container.patch = rawPatch;
107
+ }
108
+ }
109
+ }
110
+ else if (rawPatch === undefined && typeof rawInput === 'string') {
111
+ // 3) 若只有 input 字段,则复制一份到 patch,避免双层包装
112
+ container.patch = rawInput;
113
+ }
114
+ fn.arguments = JSON.stringify(container);
115
+ tc.function = fn;
116
+ continue;
117
+ }
118
+ }
119
+ catch {
120
+ // 回退到原始字符串形状
121
+ fn.arguments = trimmed;
122
+ tc.function = fn;
123
+ continue;
124
+ }
125
+ }
126
+ // 其它合法 JSON 场景保持原样
127
+ fn.arguments = trimmed;
128
+ tc.function = fn;
29
129
  }
30
130
  }
31
131
  }
@@ -0,0 +1,16 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * Canonicalize assistant textual tool markup into tool_calls (Chat path).
4
+ *
5
+ * 行为分两步:
6
+ * 1. 使用 text-markup-normalizer 将纯文本中的工具标记(含 apply_patch)提升为 tool_calls;
7
+ * 2. 再通过 canonicalizeChatResponseTools 统一 tool_calls 形态(content=null、arguments 为字符串等)。
8
+ *
9
+ * 这样可以保证:无论上游 provider 是哪种协议,只要在文本中输出符合规范的 apply_patch / shell 等片段,
10
+ * 在响应侧都会被折叠为标准的 tool_calls,供后续工具治理与客户端透明消费。
11
+ */
12
+ export declare class ResponseToolTextCanonicalizeFilter implements Filter<JsonObject> {
13
+ readonly name = "response_tool_text_canonicalize";
14
+ readonly stage: FilterContext['stage'];
15
+ apply(input: JsonObject): FilterResult<JsonObject>;
16
+ }
@@ -1,16 +1,40 @@
1
1
  /**
2
2
  * Canonicalize assistant textual tool markup into tool_calls (Chat path).
3
- * Wrapper around existing canonicalizer to fit the Filter pipeline.
3
+ *
4
+ * 行为分两步:
5
+ * 1. 使用 text-markup-normalizer 将纯文本中的工具标记(含 apply_patch)提升为 tool_calls;
6
+ * 2. 再通过 canonicalizeChatResponseTools 统一 tool_calls 形态(content=null、arguments 为字符串等)。
7
+ *
8
+ * 这样可以保证:无论上游 provider 是哪种协议,只要在文本中输出符合规范的 apply_patch / shell 等片段,
9
+ * 在响应侧都会被折叠为标准的 tool_calls,供后续工具治理与客户端透明消费。
4
10
  */
5
11
  export class ResponseToolTextCanonicalizeFilter {
6
12
  name = 'response_tool_text_canonicalize';
7
13
  stage = 'response_pre';
8
14
  apply(input) {
9
15
  try {
10
- // Defer to existing canonicalizer for behavior parity
11
16
  // eslint-disable-next-line @typescript-eslint/no-var-requires
12
17
  const { canonicalizeChatResponseTools } = require('../../conversion/shared/tool-canonicalizer.js');
13
- const out = canonicalizeChatResponseTools(input);
18
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
19
+ const { normalizeAssistantTextToToolCalls } = require('../../conversion/shared/text-markup-normalizer.js');
20
+ // 先在文本层面抽取工具调用(apply_patch / shell / MCP 等)
21
+ let working = input && typeof input === 'object' ? JSON.parse(JSON.stringify(input)) : input;
22
+ try {
23
+ const choices = Array.isArray(working?.choices) ? working.choices : [];
24
+ for (const ch of choices) {
25
+ if (!ch || typeof ch !== 'object')
26
+ continue;
27
+ const msg = ch.message;
28
+ if (msg && typeof msg === 'object') {
29
+ ch.message = normalizeAssistantTextToToolCalls(msg);
30
+ }
31
+ }
32
+ working.choices = choices;
33
+ }
34
+ catch {
35
+ // best-effort:文本解析失败时保留原始 payload
36
+ }
37
+ const out = canonicalizeChatResponseTools(working);
14
38
  return { ok: true, data: out };
15
39
  }
16
40
  catch {
@@ -27,7 +27,7 @@ export function bootstrapVirtualRouterConfig(input) {
27
27
  if (!Object.keys(routingSource).length) {
28
28
  throw new VirtualRouterError('Virtual Router routing table cannot be empty', VirtualRouterErrorCode.CONFIG_ERROR);
29
29
  }
30
- const webSearch = normalizeWebSearch(section.webSearch);
30
+ const webSearch = normalizeWebSearch(section.webSearch, routingSource);
31
31
  validateWebSearchRouting(webSearch, routingSource);
32
32
  const { runtimeEntries, aliasIndex } = buildProviderRuntimeEntries(providersSource);
33
33
  const { routing, targetKeys } = expandRoutingTable(routingSource, aliasIndex);
@@ -121,7 +121,8 @@ function buildProviderRuntimeEntries(providers) {
121
121
  streaming: normalizedProvider.streaming,
122
122
  modelStreaming: normalizedProvider.modelStreaming,
123
123
  modelContextTokens: normalizedProvider.modelContextTokens,
124
- defaultContextTokens: normalizedProvider.defaultContextTokens
124
+ defaultContextTokens: normalizedProvider.defaultContextTokens,
125
+ ...(normalizedProvider.serverToolsDisabled ? { serverToolsDisabled: true } : {})
125
126
  };
126
127
  }
127
128
  }
@@ -158,7 +159,8 @@ function expandRoutingTable(routingSource, aliasIndex) {
158
159
  id: pool.id,
159
160
  priority: pool.priority,
160
161
  backup: pool.backup,
161
- targets: expandedTargets
162
+ targets: expandedTargets,
163
+ ...(pool.force ? { force: true } : {})
162
164
  });
163
165
  }
164
166
  }
@@ -194,7 +196,8 @@ function buildProviderProfiles(targetKeys, runtimeEntries) {
194
196
  processMode: runtime.processMode || 'chat',
195
197
  responsesConfig: runtime.responsesConfig,
196
198
  streaming: streamingPref,
197
- maxContextTokens: contextTokens
199
+ maxContextTokens: contextTokens,
200
+ ...(runtime.serverToolsDisabled ? { serverToolsDisabled: true } : {})
198
201
  };
199
202
  targetRuntime[targetKey] = {
200
203
  ...runtime,
@@ -274,12 +277,15 @@ function normalizeRoutePoolEntry(routeName, entry, index, total) {
274
277
  (typeof record.type === 'string' && record.type.toLowerCase() === 'backup');
275
278
  const priority = normalizePriorityValue(record.priority, total - index);
276
279
  const targets = normalizeRouteTargets(record);
280
+ const force = record.force === true ||
281
+ (typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true');
277
282
  return targets.length
278
283
  ? {
279
284
  id,
280
285
  priority,
281
286
  backup,
282
- targets
287
+ targets,
288
+ ...(force ? { force: true } : {})
283
289
  }
284
290
  : null;
285
291
  }
@@ -386,6 +392,12 @@ function normalizeProvider(providerId, raw) {
386
392
  const streaming = resolveProviderStreamingPreference(provider, responsesNode);
387
393
  const modelStreaming = normalizeModelStreaming(provider);
388
394
  const { modelContextTokens, defaultContextTokens } = normalizeModelContextTokens(provider);
395
+ const serverToolsDisabled = provider.serverToolsDisabled === true ||
396
+ (typeof provider.serverToolsDisabled === 'string' &&
397
+ provider.serverToolsDisabled.trim().toLowerCase() === 'true') ||
398
+ (provider.serverTools &&
399
+ typeof provider.serverTools === 'object' &&
400
+ provider.serverTools.enabled === false);
389
401
  return {
390
402
  providerId,
391
403
  providerType,
@@ -398,7 +410,8 @@ function normalizeProvider(providerId, raw) {
398
410
  streaming,
399
411
  modelStreaming,
400
412
  modelContextTokens,
401
- defaultContextTokens
413
+ defaultContextTokens,
414
+ ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
402
415
  };
403
416
  }
404
417
  function normalizeModelStreaming(provider) {
@@ -539,9 +552,9 @@ function validateWebSearchRouting(webSearch, routingSource) {
539
552
  if (!webSearch) {
540
553
  return;
541
554
  }
542
- const routePools = routingSource['web_search'];
555
+ const routePools = routingSource['web_search'] ?? routingSource['search'];
543
556
  if (!Array.isArray(routePools) || !routePools.length) {
544
- throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
557
+ throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search (or search) route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
545
558
  }
546
559
  const targets = new Set();
547
560
  for (const pool of routePools) {
@@ -556,11 +569,11 @@ function validateWebSearchRouting(webSearch, routingSource) {
556
569
  }
557
570
  for (const engine of webSearch.engines) {
558
571
  if (!targets.has(engine.providerKey)) {
559
- throw new VirtualRouterError(`Virtual Router webSearch engine "${engine.id}" references providerKey "${engine.providerKey}" which is not present in routing.web_search`, VirtualRouterErrorCode.CONFIG_ERROR);
572
+ throw new VirtualRouterError(`Virtual Router webSearch engine "${engine.id}" references providerKey "${engine.providerKey}" which is not present in routing.web_search/search`, VirtualRouterErrorCode.CONFIG_ERROR);
560
573
  }
561
574
  }
562
575
  }
563
- function normalizeWebSearch(input) {
576
+ function normalizeWebSearch(input, routingSource) {
564
577
  if (!input || typeof input !== 'object') {
565
578
  return undefined;
566
579
  }
@@ -588,6 +601,12 @@ function normalizeWebSearch(input) {
588
601
  : undefined;
589
602
  const isDefault = node.default === true ||
590
603
  (typeof node.default === 'string' && node.default.trim().toLowerCase() === 'true');
604
+ const serverToolsDisabled = node.serverToolsDisabled === true ||
605
+ (typeof node.serverToolsDisabled === 'string' &&
606
+ node.serverToolsDisabled.trim().toLowerCase() === 'true') ||
607
+ (node.serverTools &&
608
+ typeof node.serverTools === 'object' &&
609
+ node.serverTools.enabled === false);
591
610
  // Deduplicate by id; first wins, subsequent are ignored.
592
611
  if (engines.some((engine) => engine.id === id)) {
593
612
  continue;
@@ -596,13 +615,15 @@ function normalizeWebSearch(input) {
596
615
  id,
597
616
  providerKey,
598
617
  description,
599
- default: isDefault
618
+ default: isDefault,
619
+ ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
600
620
  });
601
621
  }
602
622
  if (!engines.length) {
603
623
  return undefined;
604
624
  }
605
625
  let injectPolicy;
626
+ let force;
606
627
  const rawPolicy = record.injectPolicy ?? record?.inject_policy;
607
628
  if (typeof rawPolicy === 'string') {
608
629
  const normalized = rawPolicy.trim().toLowerCase();
@@ -610,9 +631,20 @@ function normalizeWebSearch(input) {
610
631
  injectPolicy = normalized;
611
632
  }
612
633
  }
634
+ if (record.force === true ||
635
+ (typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true')) {
636
+ force = true;
637
+ }
638
+ else {
639
+ const webSearchPools = routingSource['web_search'] ?? routingSource['search'] ?? [];
640
+ if (Array.isArray(webSearchPools) && webSearchPools.some((pool) => pool.force)) {
641
+ force = true;
642
+ }
643
+ }
613
644
  return {
614
645
  engines,
615
- injectPolicy: injectPolicy ?? 'selective'
646
+ injectPolicy: injectPolicy ?? 'selective',
647
+ ...(force ? { force } : {})
616
648
  };
617
649
  }
618
650
  function extractProviderAuthEntries(providerId, raw) {
@@ -17,34 +17,30 @@ export class RoutingClassifier {
17
17
  const thinkingContinuation = lastToolCategory === 'read';
18
18
  const searchContinuation = lastToolCategory === 'search';
19
19
  const toolsContinuation = lastToolCategory === 'other';
20
- if (latestMessageFromUser) {
21
- const reasoning = 'thinking:user-input';
22
- const evaluations = {
23
- thinking: { triggered: true, reason: reasoning }
24
- };
25
- const candidates = this.ensureDefaultCandidate(['thinking']);
26
- return this.buildResult('thinking', reasoning, evaluations, candidates);
27
- }
28
20
  const evaluationMap = {
29
21
  vision: {
30
- triggered: features.hasVisionTool && features.hasImageAttachment,
31
- reason: 'vision:requires-tool+image'
22
+ triggered: features.hasImageAttachment,
23
+ reason: 'vision:image-detected'
24
+ },
25
+ thinking: {
26
+ triggered: latestMessageFromUser,
27
+ reason: 'thinking:user-input'
32
28
  },
33
29
  longcontext: {
34
30
  triggered: reachedLongContext,
35
31
  reason: 'longcontext:token-threshold'
36
32
  },
37
- websearch: {
38
- triggered: features.hasWebTool || searchContinuation,
39
- reason: searchContinuation ? 'websearch:last-tool-search' : 'websearch:web-tools-detected'
40
- },
41
33
  coding: {
42
34
  triggered: codingContinuation,
43
35
  reason: 'coding:last-tool-write'
44
36
  },
45
- thinking: {
46
- triggered: thinkingContinuation || latestMessageFromUser,
47
- reason: thinkingContinuation ? 'thinking:last-tool-read' : 'thinking:user-input'
37
+ thinking_continuation: {
38
+ triggered: thinkingContinuation,
39
+ reason: 'thinking:last-tool-read'
40
+ },
41
+ search: {
42
+ triggered: searchContinuation,
43
+ reason: 'search:last-tool-search'
48
44
  },
49
45
  tools: {
50
46
  triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
@@ -12,6 +12,8 @@ export declare class VirtualRouterEngine {
12
12
  private readonly debug;
13
13
  private healthConfig;
14
14
  private readonly statsCenter;
15
+ private webSearchForce;
16
+ private routingInstructionState;
15
17
  initialize(config: VirtualRouterConfig): void;
16
18
  route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
17
19
  target: TargetMetadata;
@@ -28,6 +30,11 @@ export declare class VirtualRouterEngine {
28
30
  }>;
29
31
  health: import("./types.js").ProviderHealthState[];
30
32
  };
33
+ /**
34
+ * 将分类器产生的逻辑路由名直接归一化为配置中的路由键。
35
+ * 不再维护 "websearch" 之类的别名,调用方应显式使用 "web_search" 或 "search" 等实际路由名。
36
+ */
37
+ private normalizeRouteAlias;
31
38
  private validateConfig;
32
39
  private selectProvider;
33
40
  private trySelectFromTier;
@@ -37,16 +44,48 @@ export declare class VirtualRouterEngine {
37
44
  private buildContextCandidatePools;
38
45
  private describeAttempt;
39
46
  private resolveStickyKey;
47
+ private resolveSessionScope;
48
+ private getRoutingInstructionState;
49
+ private buildMetadataInstructions;
50
+ private parseMetadataDisableDescriptor;
51
+ private resolveRoutingMode;
52
+ private resolveInstructionTarget;
53
+ private filterCandidatesByRoutingState;
54
+ private selectFromCandidates;
55
+ private extractProviderId;
56
+ /**
57
+ * 在已有候选路由集合上,筛选出真正挂载了 sticky 池内 providerKey 的路由,
58
+ * 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
59
+ * sticky 模式,就不会再命中独立的 tools 池(例如 glm/qwen 工具模型)。
60
+ * 若候选集合中完全没有挂载 sticky key 的路由,则尝试在 default 路由上兜底。
61
+ */
62
+ private buildStickyRouteCandidatesFromFiltered;
63
+ /**
64
+ * 在 sticky 模式下,仅在 sticky 池内选择 Provider:
65
+ * - stickyKeySet 表示已经解析并通过健康检查的 providerKey 集合;
66
+ * - 不再依赖 routing[*].targets 中是否挂载这些 key,避免「未初始化路由池」导致 sticky 池为空;
67
+ * - 仍然尊重 allowed/disabledProviders、disabledKeys、disabledModels 以及上下文长度。
68
+ */
69
+ private selectFromStickyPool;
70
+ private extractKeyAlias;
71
+ private normalizeAliasDescriptor;
72
+ private extractKeyIndex;
73
+ private getProviderModelId;
40
74
  private mapProviderError;
41
75
  private deriveReason;
42
76
  private buildRouteCandidates;
77
+ private reorderForInlineVision;
78
+ private routeSupportsInlineVision;
43
79
  private sortByPriority;
44
80
  private routeWeight;
81
+ private routeHasForceFlag;
45
82
  private routeHasTargets;
46
83
  private hasPrimaryPool;
47
84
  private sortRoutePools;
48
85
  private flattenPoolTargets;
49
86
  private buildHitReason;
87
+ private isRoutingStateEmpty;
88
+ private persistRoutingInstructionState;
50
89
  private decorateWithDetail;
51
90
  private formatVirtualRouterHit;
52
91
  private resolveRouteColor;