@jsonstudio/llms 0.6.567 → 0.6.586

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 (62) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  5. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -2
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +72 -81
  8. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
  9. package/dist/conversion/hub/process/chat-process.js +68 -24
  10. package/dist/conversion/hub/response/provider-response.js +0 -8
  11. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +22 -3
  12. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +267 -14
  13. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  14. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
  15. package/dist/conversion/responses/responses-openai-bridge.js +1 -13
  16. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  17. package/dist/conversion/shared/args-mapping.js +11 -3
  18. package/dist/conversion/shared/responses-output-builder.js +42 -21
  19. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  20. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  21. package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
  22. package/dist/conversion/shared/text-markup-normalizer.js +118 -31
  23. package/dist/conversion/shared/tool-filter-pipeline.js +56 -30
  24. package/dist/conversion/shared/tool-harvester.js +43 -12
  25. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  26. package/dist/conversion/shared/tool-mapping.js +33 -19
  27. package/dist/filters/index.d.ts +1 -0
  28. package/dist/filters/index.js +1 -0
  29. package/dist/filters/special/request-tools-normalize.js +14 -4
  30. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  31. package/dist/filters/special/response-apply-patch-toon-decode.js +117 -0
  32. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  33. package/dist/filters/special/response-tool-arguments-toon-decode.js +154 -26
  34. package/dist/guidance/index.js +71 -42
  35. package/dist/router/virtual-router/bootstrap.js +10 -5
  36. package/dist/router/virtual-router/classifier.js +16 -7
  37. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  38. package/dist/router/virtual-router/engine-health.js +217 -4
  39. package/dist/router/virtual-router/engine-logging.d.ts +2 -1
  40. package/dist/router/virtual-router/engine-logging.js +35 -3
  41. package/dist/router/virtual-router/engine.d.ts +17 -1
  42. package/dist/router/virtual-router/engine.js +184 -6
  43. package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-instructions.js +19 -1
  45. package/dist/router/virtual-router/tool-signals.d.ts +2 -1
  46. package/dist/router/virtual-router/tool-signals.js +324 -119
  47. package/dist/router/virtual-router/types.d.ts +31 -1
  48. package/dist/router/virtual-router/types.js +2 -2
  49. package/dist/servertool/engine.js +3 -0
  50. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  51. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  52. package/dist/servertool/handlers/stop-message-auto.js +61 -4
  53. package/dist/servertool/server-side-tools.d.ts +1 -0
  54. package/dist/servertool/server-side-tools.js +27 -0
  55. package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
  56. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +23 -3
  57. package/dist/tools/apply-patch-structured.d.ts +20 -0
  58. package/dist/tools/apply-patch-structured.js +240 -0
  59. package/dist/tools/tool-description-utils.d.ts +5 -0
  60. package/dist/tools/tool-description-utils.js +50 -0
  61. package/dist/tools/tool-registry.js +11 -193
  62. package/package.json +1 -1
@@ -8,14 +8,7 @@ const READ_TOOL_EXACT = new Set([
8
8
  'open_file',
9
9
  'get_file',
10
10
  'download_file',
11
- 'describe_current_request',
12
- 'list_dir',
13
- 'list_directory',
14
- 'list_files',
15
- 'list_documents',
16
- 'list_resources',
17
- 'search_files',
18
- 'find_files'
11
+ 'describe_current_request'
19
12
  ]);
20
13
  const WRITE_TOOL_EXACT = new Set([
21
14
  'apply_patch',
@@ -26,83 +19,61 @@ const WRITE_TOOL_EXACT = new Set([
26
19
  'update_file',
27
20
  'save_file',
28
21
  'append_file',
29
- 'replace_file',
30
- 'delete_file',
31
- 'remove_file',
32
- 'rename_file',
33
- 'move_file',
34
- 'copy_file',
35
- 'mkdir',
36
- 'rmdir'
22
+ 'replace_file'
23
+ ]);
24
+ const SEARCH_TOOL_EXACT = new Set([
25
+ 'search_files',
26
+ 'find_files',
27
+ 'search_documents',
28
+ 'search_repo',
29
+ 'glob_search',
30
+ 'grep_files',
31
+ 'code_search',
32
+ 'lookup_symbol'
37
33
  ]);
38
- const SEARCH_TOOL_EXACT = new Set(['websearch', 'web_search', 'search_web', 'internet_search', 'webfetch', 'web_fetch']);
39
- const READ_TOOL_KEYWORDS = ['read', 'list', 'view', 'download', 'open', 'show', 'fetch', 'inspect'];
40
- const WRITE_TOOL_KEYWORDS = ['write', 'patch', 'modify', 'edit', 'create', 'update', 'append', 'replace', 'delete', 'remove'];
41
- const SEARCH_TOOL_KEYWORDS = ['search', 'websearch', 'web_fetch', 'webfetch', 'web-request', 'web_request', 'internet'];
34
+ const READ_TOOL_KEYWORDS = ['read', 'view', 'download', 'open', 'show', 'fetch', 'inspect'];
35
+ const WRITE_TOOL_KEYWORDS = ['write', 'patch', 'modify', 'edit', 'create', 'update', 'append', 'replace', 'save'];
36
+ const SEARCH_TOOL_KEYWORDS = ['find', 'grep', 'glob', 'lookup', 'locate'];
42
37
  const SHELL_TOOL_NAMES = new Set(['shell_command', 'shell', 'bash']);
43
38
  const DECLARED_TOOL_IGNORE = new Set(['exec_command']);
44
39
  const SHELL_HEREDOC_PATTERN = /<<\s*['"]?[a-z0-9_-]+/i;
45
- const SHELL_WRITE_PATTERNS = [
46
- 'apply_patch',
47
- 'sed -i',
48
- 'perl -pi',
49
- 'tee ',
50
- 'cat <<',
51
- 'cat >',
52
- 'printf >',
53
- 'touch ',
54
- 'truncate',
55
- 'mkdir',
56
- 'mktemp',
57
- 'rmdir',
58
- 'rm ',
59
- 'rm-',
60
- 'unlink',
61
- 'mv ',
62
- 'cp ',
63
- 'ln -',
64
- 'chmod',
65
- 'chown',
66
- 'chgrp',
67
- 'tar ',
68
- 'git add',
69
- 'git commit',
70
- 'git apply',
71
- 'git am',
72
- 'git rebase',
73
- 'git checkout',
74
- 'git merge',
75
- 'patch <<',
76
- 'npm install',
77
- 'pnpm install',
78
- 'yarn add',
79
- 'yarn install',
80
- 'pip install',
81
- 'pip3 install',
82
- 'brew install',
83
- 'cargo add',
84
- 'cargo install',
85
- 'go install',
86
- 'make install'
87
- ];
88
- const SHELL_READ_PATTERNS = [
89
- 'ls',
90
- 'dir ',
91
- 'pwd',
92
- 'cat ',
93
- 'type ',
94
- 'head ',
95
- 'tail ',
96
- 'stat',
97
- 'tree',
98
- 'wc ',
99
- 'du ',
100
- 'printf "',
101
- 'python - <<',
102
- 'python -c',
103
- 'node - <<',
104
- 'node -e'
105
- ];
40
+ const SHELL_WRITE_COMMANDS = new Set(['apply_patch', 'tee', 'touch', 'truncate', 'patch']);
41
+ const SHELL_READ_COMMANDS = new Set(['cat', 'head', 'tail', 'awk', 'strings', 'less', 'more', 'nl']);
42
+ const SHELL_SEARCH_COMMANDS = new Set([
43
+ 'rg',
44
+ 'ripgrep',
45
+ 'grep',
46
+ 'egrep',
47
+ 'fgrep',
48
+ 'ag',
49
+ 'ack',
50
+ 'find',
51
+ 'fd',
52
+ 'locate',
53
+ 'codesearch'
54
+ ]);
55
+ const SHELL_REDIRECT_WRITE_BINARIES = new Set(['cat', 'printf', 'python', 'node', 'perl', 'ruby', 'php', 'bash', 'sh', 'zsh', 'echo']);
56
+ const SHELL_WRAPPER_COMMANDS = new Set(['sudo', 'env', 'time', 'nice', 'nohup', 'command', 'stdbuf']);
57
+ const COMMAND_ALIASES = new Map([
58
+ ['python3', 'python'],
59
+ ['pip3', 'pip'],
60
+ ['ripgrep', 'rg'],
61
+ ['perl5', 'perl']
62
+ ]);
63
+ const GIT_WRITE_SUBCOMMANDS = new Set(['add', 'commit', 'apply', 'am', 'rebase', 'checkout', 'merge']);
64
+ const PACKAGE_MANAGER_COMMANDS = new Map([
65
+ ['npm', new Set(['install'])],
66
+ ['pnpm', new Set(['install'])],
67
+ ['yarn', new Set(['add', 'install'])],
68
+ ['pip', new Set(['install'])],
69
+ ['pip3', new Set(['install'])],
70
+ ['brew', new Set(['install'])],
71
+ ['cargo', new Set(['add', 'install'])],
72
+ ['go', new Set(['install'])],
73
+ ['make', new Set(['install'])]
74
+ ]);
75
+ const ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
76
+ const OUTPUT_REDIRECT_PATTERN = /(?:^|[\s;|&])>>?\s*(?!&)[^\s]+/;
106
77
  export function detectVisionTool(request) {
107
78
  if (!Array.isArray(request.tools)) {
108
79
  return false;
@@ -160,31 +131,46 @@ export function extractMeaningfulDeclaredToolNames(tools) {
160
131
  }
161
132
  return names;
162
133
  }
134
+ const TOOL_CATEGORY_PRIORITY = {
135
+ websearch: 4,
136
+ read: 3,
137
+ write: 2,
138
+ search: 1,
139
+ other: 0
140
+ };
163
141
  export function detectLastAssistantToolCategory(messages) {
164
142
  for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
165
143
  const msg = messages[idx];
166
144
  if (!msg || !Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) {
167
145
  continue;
168
146
  }
169
- let fallback;
147
+ const candidates = [];
170
148
  for (const call of msg.tool_calls) {
171
149
  const classification = classifyToolCall(call);
172
- if (!classification) {
173
- continue;
174
- }
175
- if (!fallback) {
176
- fallback = classification;
177
- }
178
- if (classification.category !== 'other') {
179
- return classification;
150
+ if (classification) {
151
+ candidates.push(classification);
180
152
  }
181
153
  }
182
- if (fallback) {
183
- return fallback;
154
+ if (!candidates.length) {
155
+ continue;
156
+ }
157
+ let best = candidates[0];
158
+ let bestScore = TOOL_CATEGORY_PRIORITY[best.category] ?? 0;
159
+ for (let i = 1; i < candidates.length; i += 1) {
160
+ const candidate = candidates[i];
161
+ const score = TOOL_CATEGORY_PRIORITY[candidate.category] ?? 0;
162
+ if (score > bestScore) {
163
+ best = candidate;
164
+ bestScore = score;
165
+ }
184
166
  }
167
+ return best;
185
168
  }
186
169
  return undefined;
187
170
  }
171
+ export function classifyToolCallForReport(call) {
172
+ return classifyToolCall(call);
173
+ }
188
174
  function classifyToolCall(call) {
189
175
  if (!call || typeof call !== 'object') {
190
176
  return undefined;
@@ -197,21 +183,48 @@ function classifyToolCall(call) {
197
183
  }
198
184
  const argsObject = parseToolArguments(call?.function?.arguments);
199
185
  const commandText = extractCommandText(argsObject);
200
- const nameCategory = categorizeToolName(functionName);
201
186
  const snippet = buildCommandSnippet(commandText);
202
- if (nameCategory === 'write' || nameCategory === 'read' || nameCategory === 'search') {
203
- return { category: nameCategory, name: functionName, commandSnippet: snippet };
204
- }
205
- if (SHELL_TOOL_NAMES.has(functionName)) {
206
- const shellCategory = classifyShellCommand(commandText);
207
- return { category: shellCategory, name: functionName, commandSnippet: snippet };
208
- }
209
- if (commandText) {
187
+ const normalizedName = functionName.toLowerCase();
188
+ const normalizedCmd = commandText.toLowerCase();
189
+ // 1) Web search 优先:函数名命中 web 搜索关键字时,一律归类为 websearch,优先级最高。
190
+ const isWebSearch = WEB_TOOL_KEYWORDS.some((keyword) => normalizedName.includes(keyword));
191
+ // 2) 基于工具名的初步分类(read / write / search / other)
192
+ const nameCategory = categorizeToolName(functionName);
193
+ // 3) shell_command / exec_command 根据内部命令判断读写性质
194
+ let shellCategory = 'other';
195
+ if (SHELL_TOOL_NAMES.has(functionName) || functionName === 'exec_command') {
196
+ shellCategory = classifyShellCommand(commandText);
197
+ }
198
+ // 按优先级合并分类结果:
199
+ // 1. web search
200
+ // 2. 写文件(任一维度命中写)
201
+ // 3. 读文件(任一维度命中读)
202
+ // 4. 其他搜索(非 web search)
203
+ // 5. 其它工具
204
+ // Priority 1: Web search
205
+ if (isWebSearch) {
206
+ return { category: 'websearch', name: functionName, commandSnippet: snippet };
207
+ }
208
+ // Priority 2: Write (写文件) — 名称或内部命令任一判断为写,都按写处理
209
+ if (nameCategory === 'write' || shellCategory === 'write') {
210
+ return { category: 'write', name: functionName, commandSnippet: snippet };
211
+ }
212
+ // Priority 3: Read (读文件) — 仅在没有写的情况下,再看读
213
+ if (nameCategory === 'read' || shellCategory === 'read') {
214
+ return { category: 'read', name: functionName, commandSnippet: snippet };
215
+ }
216
+ // Priority 4: 其他 search 类工具(非 web search)
217
+ if (nameCategory === 'search' || shellCategory === 'search') {
218
+ return { category: 'search', name: functionName, commandSnippet: snippet };
219
+ }
220
+ // Priority 5: 兜底用命令文本再判断一次 shell 风格读写/搜索(非 shell/exec_command 的工具)
221
+ if (!SHELL_TOOL_NAMES.has(functionName) && functionName !== 'exec_command' && commandText) {
210
222
  const derivedCategory = classifyShellCommand(commandText);
211
- if (derivedCategory !== 'other') {
223
+ if (derivedCategory === 'write' || derivedCategory === 'read' || derivedCategory === 'search') {
212
224
  return { category: derivedCategory, name: functionName, commandSnippet: snippet };
213
225
  }
214
226
  }
227
+ // 最终兜底:other
215
228
  return { category: 'other', name: functionName, commandSnippet: snippet };
216
229
  }
217
230
  function extractToolName(tool) {
@@ -325,12 +338,10 @@ function categorizeToolName(name) {
325
338
  SEARCH_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
326
339
  return 'search';
327
340
  }
328
- if (READ_TOOL_EXACT.has(normalized) ||
329
- READ_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
341
+ if (READ_TOOL_EXACT.has(normalized)) {
330
342
  return 'read';
331
343
  }
332
- if (WRITE_TOOL_EXACT.has(normalized) ||
333
- WRITE_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
344
+ if (WRITE_TOOL_EXACT.has(normalized)) {
334
345
  return 'write';
335
346
  }
336
347
  return 'other';
@@ -342,28 +353,225 @@ function classifyShellCommand(command) {
342
353
  if (SHELL_HEREDOC_PATTERN.test(command)) {
343
354
  return 'write';
344
355
  }
345
- const segments = splitCommandSegments(command).map(stripShellWrapper);
346
- if (segments.some((segment) => matchesAnyPattern(segment, SHELL_WRITE_PATTERNS))) {
347
- return 'write';
356
+ const segments = splitCommandSegments(command);
357
+ let sawRead = false;
358
+ let sawSearch = false;
359
+ for (const segment of segments) {
360
+ const normalized = normalizeShellSegment(segment);
361
+ if (!normalized) {
362
+ continue;
363
+ }
364
+ for (const args of normalized.commands) {
365
+ if (!args.length) {
366
+ continue;
367
+ }
368
+ const [binary, ...rest] = args;
369
+ const normalizedBinary = normalizeBinaryName(binary);
370
+ const alias = COMMAND_ALIASES.get(normalizedBinary) || normalizedBinary;
371
+ if (isWriteBinary(alias, rest, normalized.raw)) {
372
+ return 'write';
373
+ }
374
+ if (isReadBinary(alias, rest)) {
375
+ sawRead = true;
376
+ continue;
377
+ }
378
+ if (isSearchBinary(alias, rest)) {
379
+ sawSearch = true;
380
+ }
381
+ }
348
382
  }
349
- if (segments.some((segment) => matchesAnyPattern(segment, SHELL_READ_PATTERNS))) {
383
+ if (sawRead) {
350
384
  return 'read';
351
385
  }
352
- const stripped = stripShellWrapper(command);
353
- if (matchesAnyPattern(stripped, SHELL_WRITE_PATTERNS)) {
354
- return 'write';
355
- }
356
- if (matchesAnyPattern(stripped, SHELL_READ_PATTERNS)) {
357
- return 'read';
386
+ if (sawSearch) {
387
+ return 'search';
358
388
  }
359
389
  return 'other';
360
390
  }
391
+ function normalizeShellSegment(segment) {
392
+ const trimmed = stripShellWrapper(segment);
393
+ if (!trimmed) {
394
+ return undefined;
395
+ }
396
+ const tokens = splitShellTokens(trimmed);
397
+ if (!tokens.length) {
398
+ return undefined;
399
+ }
400
+ const commands = [];
401
+ let current = [];
402
+ for (const token of tokens) {
403
+ if (token === '|') {
404
+ const cleaned = cleanCommandTokens(current);
405
+ if (cleaned.length) {
406
+ commands.push(cleaned);
407
+ }
408
+ current = [];
409
+ continue;
410
+ }
411
+ current.push(token);
412
+ }
413
+ const cleaned = cleanCommandTokens(current);
414
+ if (cleaned.length) {
415
+ commands.push(cleaned);
416
+ }
417
+ if (!commands.length) {
418
+ return undefined;
419
+ }
420
+ return { raw: trimmed, commands };
421
+ }
422
+ function splitShellTokens(cmd) {
423
+ const tokens = [];
424
+ let current = '';
425
+ let quote = null;
426
+ for (let i = 0; i < cmd.length; i += 1) {
427
+ const ch = cmd[i];
428
+ if (quote) {
429
+ if (ch === quote) {
430
+ quote = null;
431
+ }
432
+ else if (ch === '\\' && quote === '"' && i + 1 < cmd.length) {
433
+ current += cmd[i + 1];
434
+ i += 1;
435
+ }
436
+ else {
437
+ current += ch;
438
+ }
439
+ continue;
440
+ }
441
+ if (ch === '"' || ch === "'") {
442
+ quote = ch;
443
+ continue;
444
+ }
445
+ if (/[\s\t]/.test(ch)) {
446
+ if (current) {
447
+ tokens.push(current);
448
+ current = '';
449
+ }
450
+ continue;
451
+ }
452
+ if (ch === '|') {
453
+ if (current) {
454
+ tokens.push(current);
455
+ current = '';
456
+ }
457
+ tokens.push('|');
458
+ continue;
459
+ }
460
+ if ((ch === '|' || ch === '&') && i + 1 < cmd.length && cmd[i + 1] === ch) {
461
+ if (current) {
462
+ tokens.push(current);
463
+ current = '';
464
+ }
465
+ tokens.push(cmd.slice(i, i + 2));
466
+ i += 1;
467
+ continue;
468
+ }
469
+ current += ch;
470
+ }
471
+ if (current) {
472
+ tokens.push(current);
473
+ }
474
+ return tokens;
475
+ }
476
+ function cleanCommandTokens(tokens) {
477
+ if (!tokens.length) {
478
+ return [];
479
+ }
480
+ const cleaned = [];
481
+ for (const token of tokens) {
482
+ if (!cleaned.length) {
483
+ if (ENV_ASSIGNMENT_PATTERN.test(token)) {
484
+ continue;
485
+ }
486
+ if (SHELL_WRAPPER_COMMANDS.has(token)) {
487
+ continue;
488
+ }
489
+ }
490
+ cleaned.push(token);
491
+ }
492
+ return cleaned;
493
+ }
494
+ function isWriteBinary(binary, args, rawSegment) {
495
+ const normalized = binary.toLowerCase();
496
+ if (SHELL_WRITE_COMMANDS.has(normalized)) {
497
+ return true;
498
+ }
499
+ if (normalized === 'git' && args.length > 0) {
500
+ const sub = args[0].toLowerCase();
501
+ if (GIT_WRITE_SUBCOMMANDS.has(sub)) {
502
+ return true;
503
+ }
504
+ }
505
+ if (PACKAGE_MANAGER_COMMANDS.has(normalized)) {
506
+ const allowed = PACKAGE_MANAGER_COMMANDS.get(normalized);
507
+ if (args.length > 0 && allowed.has(args[0].toLowerCase())) {
508
+ return true;
509
+ }
510
+ }
511
+ if (normalized === 'sed') {
512
+ const joined = args.join(' ').toLowerCase();
513
+ if (joined.includes('-i')) {
514
+ return true;
515
+ }
516
+ }
517
+ if (normalized === 'perl') {
518
+ const joined = args.join(' ').toLowerCase();
519
+ if (joined.includes('-pi')) {
520
+ return true;
521
+ }
522
+ }
523
+ if (normalized === 'printf' && OUTPUT_REDIRECT_PATTERN.test(rawSegment)) {
524
+ return true;
525
+ }
526
+ if (SHELL_REDIRECT_WRITE_BINARIES.has(normalized) && OUTPUT_REDIRECT_PATTERN.test(rawSegment)) {
527
+ return true;
528
+ }
529
+ return false;
530
+ }
531
+ function isReadBinary(binary, args) {
532
+ const normalized = binary.toLowerCase();
533
+ if (SHELL_READ_COMMANDS.has(normalized)) {
534
+ return true;
535
+ }
536
+ if (normalized === 'sed') {
537
+ const joined = args.join(' ').toLowerCase();
538
+ if (joined.includes('-i')) {
539
+ return false;
540
+ }
541
+ return true;
542
+ }
543
+ return false;
544
+ }
545
+ function isSearchBinary(binary, args) {
546
+ const normalized = binary.toLowerCase();
547
+ if (SHELL_SEARCH_COMMANDS.has(normalized)) {
548
+ return true;
549
+ }
550
+ if (normalized === 'git' && args.length > 0) {
551
+ const sub = args[0].toLowerCase();
552
+ if (sub === 'grep') {
553
+ return true;
554
+ }
555
+ }
556
+ return false;
557
+ }
361
558
  function splitCommandSegments(command) {
362
559
  return command
363
560
  .split(/(?:\r?\n|&&|\|\||;)/)
364
561
  .map((segment) => segment.trim())
365
562
  .filter(Boolean);
366
563
  }
564
+ function normalizeBinaryName(binary) {
565
+ if (!binary) {
566
+ return '';
567
+ }
568
+ const lowered = binary.toLowerCase();
569
+ const slashIndex = lowered.lastIndexOf('/');
570
+ if (slashIndex >= 0) {
571
+ return lowered.slice(slashIndex + 1);
572
+ }
573
+ return lowered;
574
+ }
367
575
  function stripShellWrapper(command) {
368
576
  if (!command) {
369
577
  return '';
@@ -376,6 +584,3 @@ function stripShellWrapper(command) {
376
584
  }
377
585
  return command.trim();
378
586
  }
379
- function matchesAnyPattern(command, patterns) {
380
- return patterns.some((pattern) => command.includes(pattern));
381
- }
@@ -172,6 +172,11 @@ export interface RouterMetadataInput {
172
172
  * 强制路由模式,从消息中的 <**...**> 指令解析得出
173
173
  */
174
174
  routingMode?: RoutingInstructionMode;
175
+ /**
176
+ * 当 disableStickyRoutes=true 时,本次请求仍使用 sticky session 状态,
177
+ * 但不继承 sticky target,允许后续路由重新选择 provider。
178
+ */
179
+ disableStickyRoutes?: boolean;
175
180
  /**
176
181
  * 允许的 provider 白名单
177
182
  */
@@ -228,7 +233,7 @@ export interface RoutingFeatures {
228
233
  hasCodingTool: boolean;
229
234
  hasThinkingKeyword: boolean;
230
235
  estimatedTokens: number;
231
- lastAssistantToolCategory?: 'read' | 'write' | 'search' | 'other';
236
+ lastAssistantToolCategory?: 'read' | 'write' | 'search' | 'websearch' | 'other';
232
237
  lastAssistantToolSnippet?: string;
233
238
  lastAssistantToolLabel?: string;
234
239
  latestMessageFromUser?: boolean;
@@ -343,3 +348,28 @@ export interface ProviderErrorEvent {
343
348
  export interface FeatureBuilder {
344
349
  build(request: StandardizedRequest, metadata: RouterMetadataInput): RoutingFeatures;
345
350
  }
351
+ export interface ProviderCooldownState {
352
+ providerKey: string;
353
+ cooldownExpiresAt: number;
354
+ reason?: string;
355
+ }
356
+ export interface VirtualRouterHealthSnapshot {
357
+ providers: ProviderHealthState[];
358
+ cooldowns: ProviderCooldownState[];
359
+ }
360
+ export interface VirtualRouterHealthStore {
361
+ /**
362
+ * 在 VirtualRouterEngine 初始化时提供上一次持久化的健康快照。
363
+ * 调用方应仅返回仍在有效期内的 cooldown/熔断信息,或返回 null 表示无可恢复状态。
364
+ */
365
+ loadInitialSnapshot(): VirtualRouterHealthSnapshot | null;
366
+ /**
367
+ * 当 VirtualRouterEngine 更新 provider 健康状态或 cooldown 时,可选地持久化最新快照。
368
+ * 实现应保证内部吞掉 I/O 错误,不影响路由主流程。
369
+ */
370
+ persistSnapshot?(snapshot: VirtualRouterHealthSnapshot): void;
371
+ /**
372
+ * 可选:记录原始 ProviderErrorEvent,便于后续离线统计与诊断。
373
+ */
374
+ recordProviderError?(event: ProviderErrorEvent): void;
375
+ }
@@ -7,9 +7,9 @@ export const ROUTE_PRIORITY = [
7
7
  'vision',
8
8
  'longcontext',
9
9
  'web_search',
10
- 'search',
11
- 'coding',
12
10
  'thinking',
11
+ 'coding',
12
+ 'search',
13
13
  'tools',
14
14
  'background',
15
15
  DEFAULT_ROUTE
@@ -94,6 +94,9 @@ function resolveRouteHint(adapterContext, flowId) {
94
94
  if (!routeId) {
95
95
  return undefined;
96
96
  }
97
+ if (routeId.toLowerCase() === 'default') {
98
+ return undefined;
99
+ }
97
100
  if (flowId && routeId.toLowerCase() === flowId.toLowerCase()) {
98
101
  return undefined;
99
102
  }