@jsonstudio/llms 0.6.568 → 0.6.626

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 (42) hide show
  1. package/dist/conversion/compat/profiles/chat-gemini.json +15 -15
  2. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  3. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  4. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  5. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  6. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  7. package/dist/conversion/compat/profiles/responses-output2choices-test.json +9 -10
  8. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +0 -1
  9. package/dist/conversion/hub/pipeline/hub-pipeline.js +68 -69
  10. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
  11. package/dist/conversion/hub/process/chat-process.js +37 -16
  12. package/dist/conversion/hub/response/provider-response.js +0 -8
  13. package/dist/conversion/hub/response/response-runtime.js +47 -1
  14. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +59 -4
  15. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +8 -0
  16. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +93 -12
  17. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +208 -31
  18. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +280 -14
  19. package/dist/conversion/hub/standardized-bridge.js +11 -2
  20. package/dist/conversion/hub/types/chat-envelope.d.ts +10 -0
  21. package/dist/conversion/hub/types/standardized.d.ts +2 -1
  22. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
  23. package/dist/conversion/responses/responses-openai-bridge.js +1 -13
  24. package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
  25. package/dist/conversion/shared/text-markup-normalizer.js +84 -5
  26. package/dist/conversion/shared/tool-filter-pipeline.d.ts +1 -1
  27. package/dist/conversion/shared/tool-filter-pipeline.js +54 -29
  28. package/dist/filters/index.d.ts +1 -0
  29. package/dist/filters/special/response-apply-patch-toon-decode.js +15 -7
  30. package/dist/filters/special/response-tool-arguments-toon-decode.js +108 -22
  31. package/dist/guidance/index.js +2 -0
  32. package/dist/router/virtual-router/classifier.js +16 -12
  33. package/dist/router/virtual-router/engine.js +45 -4
  34. package/dist/router/virtual-router/tool-signals.d.ts +2 -1
  35. package/dist/router/virtual-router/tool-signals.js +293 -134
  36. package/dist/router/virtual-router/types.d.ts +1 -1
  37. package/dist/router/virtual-router/types.js +1 -1
  38. package/dist/servertool/handlers/gemini-empty-reply-continue.js +28 -4
  39. package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
  40. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +7 -3
  41. package/dist/tools/apply-patch-structured.js +4 -3
  42. package/package.json +2 -2
@@ -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,101 +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
- 'sed -n',
106
- 'sed --quiet',
107
- 'sed ',
108
- 'rg ',
109
- ' ripgrep',
110
- 'grep ',
111
- 'egrep ',
112
- 'fgrep ',
113
- 'ag ',
114
- 'ack ',
115
- 'find ',
116
- 'nl ',
117
- 'less',
118
- 'more',
119
- 'awk ',
120
- 'perl -ne',
121
- 'perl -pe',
122
- 'strings '
123
- ];
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]+/;
124
77
  export function detectVisionTool(request) {
125
78
  if (!Array.isArray(request.tools)) {
126
79
  return false;
@@ -178,31 +131,46 @@ export function extractMeaningfulDeclaredToolNames(tools) {
178
131
  }
179
132
  return names;
180
133
  }
134
+ const TOOL_CATEGORY_PRIORITY = {
135
+ websearch: 4,
136
+ read: 3,
137
+ write: 2,
138
+ search: 1,
139
+ other: 0
140
+ };
181
141
  export function detectLastAssistantToolCategory(messages) {
182
142
  for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
183
143
  const msg = messages[idx];
184
144
  if (!msg || !Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) {
185
145
  continue;
186
146
  }
187
- let fallback;
147
+ const candidates = [];
188
148
  for (const call of msg.tool_calls) {
189
149
  const classification = classifyToolCall(call);
190
- if (!classification) {
191
- continue;
192
- }
193
- if (!fallback) {
194
- fallback = classification;
195
- }
196
- if (classification.category !== 'other') {
197
- return classification;
150
+ if (classification) {
151
+ candidates.push(classification);
198
152
  }
199
153
  }
200
- if (fallback) {
201
- 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
+ }
202
166
  }
167
+ return best;
203
168
  }
204
169
  return undefined;
205
170
  }
171
+ export function classifyToolCallForReport(call) {
172
+ return classifyToolCall(call);
173
+ }
206
174
  function classifyToolCall(call) {
207
175
  if (!call || typeof call !== 'object') {
208
176
  return undefined;
@@ -218,9 +186,8 @@ function classifyToolCall(call) {
218
186
  const snippet = buildCommandSnippet(commandText);
219
187
  const normalizedName = functionName.toLowerCase();
220
188
  const normalizedCmd = commandText.toLowerCase();
221
- // 1) Web search 优先:函数名或命令文本中命中 web 搜索关键字时,一律归类为 search,优先级最高。
222
- const isWebSearch = WEB_TOOL_KEYWORDS.some((keyword) => normalizedName.includes(keyword)) ||
223
- WEB_TOOL_KEYWORDS.some((keyword) => normalizedCmd.includes(keyword));
189
+ // 1) Web search 优先:函数名命中 web 搜索关键字时,一律归类为 websearch,优先级最高。
190
+ const isWebSearch = WEB_TOOL_KEYWORDS.some((keyword) => normalizedName.includes(keyword));
224
191
  // 2) 基于工具名的初步分类(read / write / search / other)
225
192
  const nameCategory = categorizeToolName(functionName);
226
193
  // 3) shell_command / exec_command 根据内部命令判断读写性质
@@ -236,7 +203,7 @@ function classifyToolCall(call) {
236
203
  // 5. 其它工具
237
204
  // Priority 1: Web search
238
205
  if (isWebSearch) {
239
- return { category: 'search', name: functionName, commandSnippet: snippet };
206
+ return { category: 'websearch', name: functionName, commandSnippet: snippet };
240
207
  }
241
208
  // Priority 2: Write (写文件) — 名称或内部命令任一判断为写,都按写处理
242
209
  if (nameCategory === 'write' || shellCategory === 'write') {
@@ -247,13 +214,13 @@ function classifyToolCall(call) {
247
214
  return { category: 'read', name: functionName, commandSnippet: snippet };
248
215
  }
249
216
  // Priority 4: 其他 search 类工具(非 web search)
250
- if (nameCategory === 'search') {
217
+ if (nameCategory === 'search' || shellCategory === 'search') {
251
218
  return { category: 'search', name: functionName, commandSnippet: snippet };
252
219
  }
253
- // Priority 5: 兜底用命令文本再判断一次 shell 风格读写(非 shell/exec_command 的工具)
220
+ // Priority 5: 兜底用命令文本再判断一次 shell 风格读写/搜索(非 shell/exec_command 的工具)
254
221
  if (!SHELL_TOOL_NAMES.has(functionName) && functionName !== 'exec_command' && commandText) {
255
222
  const derivedCategory = classifyShellCommand(commandText);
256
- if (derivedCategory === 'write' || derivedCategory === 'read') {
223
+ if (derivedCategory === 'write' || derivedCategory === 'read' || derivedCategory === 'search') {
257
224
  return { category: derivedCategory, name: functionName, commandSnippet: snippet };
258
225
  }
259
226
  }
@@ -371,12 +338,10 @@ function categorizeToolName(name) {
371
338
  SEARCH_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
372
339
  return 'search';
373
340
  }
374
- if (READ_TOOL_EXACT.has(normalized) ||
375
- READ_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
341
+ if (READ_TOOL_EXACT.has(normalized)) {
376
342
  return 'read';
377
343
  }
378
- if (WRITE_TOOL_EXACT.has(normalized) ||
379
- WRITE_TOOL_KEYWORDS.some((keyword) => normalized.includes(keyword.toLowerCase()))) {
344
+ if (WRITE_TOOL_EXACT.has(normalized)) {
380
345
  return 'write';
381
346
  }
382
347
  return 'other';
@@ -388,28 +353,225 @@ function classifyShellCommand(command) {
388
353
  if (SHELL_HEREDOC_PATTERN.test(command)) {
389
354
  return 'write';
390
355
  }
391
- const segments = splitCommandSegments(command).map(stripShellWrapper);
392
- if (segments.some((segment) => matchesAnyPattern(segment, SHELL_WRITE_PATTERNS))) {
393
- 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
+ }
394
382
  }
395
- if (segments.some((segment) => matchesAnyPattern(segment, SHELL_READ_PATTERNS))) {
383
+ if (sawRead) {
396
384
  return 'read';
397
385
  }
398
- const stripped = stripShellWrapper(command);
399
- if (matchesAnyPattern(stripped, SHELL_WRITE_PATTERNS)) {
400
- return 'write';
401
- }
402
- if (matchesAnyPattern(stripped, SHELL_READ_PATTERNS)) {
403
- return 'read';
386
+ if (sawSearch) {
387
+ return 'search';
404
388
  }
405
389
  return 'other';
406
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
+ }
407
558
  function splitCommandSegments(command) {
408
559
  return command
409
560
  .split(/(?:\r?\n|&&|\|\||;)/)
410
561
  .map((segment) => segment.trim())
411
562
  .filter(Boolean);
412
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
+ }
413
575
  function stripShellWrapper(command) {
414
576
  if (!command) {
415
577
  return '';
@@ -422,6 +584,3 @@ function stripShellWrapper(command) {
422
584
  }
423
585
  return command.trim();
424
586
  }
425
- function matchesAnyPattern(command, patterns) {
426
- return patterns.some((pattern) => command.includes(pattern));
427
- }
@@ -233,7 +233,7 @@ export interface RoutingFeatures {
233
233
  hasCodingTool: boolean;
234
234
  hasThinkingKeyword: boolean;
235
235
  estimatedTokens: number;
236
- lastAssistantToolCategory?: 'read' | 'write' | 'search' | 'other';
236
+ lastAssistantToolCategory?: 'read' | 'write' | 'search' | 'websearch' | 'other';
237
237
  lastAssistantToolSnippet?: string;
238
238
  lastAssistantToolLabel?: string;
239
239
  latestMessageFromUser?: boolean;
@@ -7,9 +7,9 @@ export const ROUTE_PRIORITY = [
7
7
  'vision',
8
8
  'longcontext',
9
9
  'web_search',
10
- 'search',
11
10
  'thinking',
12
11
  'coding',
12
+ 'search',
13
13
  'tools',
14
14
  'background',
15
15
  DEFAULT_ROUTE
@@ -49,9 +49,7 @@ const handler = async (ctx) => {
49
49
  return null;
50
50
  }
51
51
  const contentRaw = message.content;
52
- const contentText = typeof contentRaw === 'string'
53
- ? contentRaw.trim()
54
- : '';
52
+ const contentText = typeof contentRaw === 'string' ? contentRaw.trim() : '';
55
53
  if (contentText.length > 0) {
56
54
  return null;
57
55
  }
@@ -59,10 +57,35 @@ const handler = async (ctx) => {
59
57
  if (toolCalls.length > 0) {
60
58
  return null;
61
59
  }
60
+ // 统计连续空回复次数,超过上限后不再自动续写,而是返回一个可重试错误。
61
+ const previousCountRaw = adapterRecord.geminiEmptyReplyCount;
62
+ const previousCount = typeof previousCountRaw === 'number' && Number.isFinite(previousCountRaw) && previousCountRaw >= 0
63
+ ? previousCountRaw
64
+ : 0;
65
+ const nextCount = previousCount + 1;
62
66
  const captured = getCapturedRequest(ctx.adapterContext);
63
67
  if (!captured) {
64
68
  return null;
65
69
  }
70
+ // 超过最多 3 次空回复:返回一个 HTTP_HANDLER_ERROR 形状的错误,交由上层错误中心处理。
71
+ if (nextCount > 3) {
72
+ const errorChat = {
73
+ id: base.id,
74
+ object: base.object,
75
+ model: base.model,
76
+ error: {
77
+ message: 'fetch failed: gemini_empty_reply_continue exceeded max empty replies',
78
+ code: 'HTTP_HANDLER_ERROR',
79
+ type: 'servertool_empty_reply'
80
+ }
81
+ };
82
+ return {
83
+ chatResponse: errorChat,
84
+ execution: {
85
+ flowId: FLOW_ID
86
+ }
87
+ };
88
+ }
66
89
  const followupPayload = buildContinueFollowupPayload(captured);
67
90
  if (!followupPayload) {
68
91
  return null;
@@ -76,7 +99,8 @@ const handler = async (ctx) => {
76
99
  payload: followupPayload,
77
100
  metadata: {
78
101
  serverToolFollowup: true,
79
- stream: false
102
+ stream: false,
103
+ geminiEmptyReplyCount: nextCount
80
104
  }
81
105
  }
82
106
  }
@@ -81,9 +81,16 @@ function normalizeUsage(usage) {
81
81
  return fallback;
82
82
  }
83
83
  const asAny = usage;
84
- const inputRaw = Number((asAny.input_tokens ?? asAny.prompt_tokens));
84
+ const baseInputRaw = Number((asAny.input_tokens ?? asAny.prompt_tokens));
85
+ const baseInput = Number.isFinite(baseInputRaw) ? baseInputRaw : 0;
86
+ let cachedRaw = Number(asAny.cache_read_input_tokens);
87
+ if (!Number.isFinite(cachedRaw) && asAny.input_tokens_details && typeof asAny.input_tokens_details === 'object') {
88
+ const details = asAny.input_tokens_details;
89
+ cachedRaw = Number(details.cached_tokens);
90
+ }
91
+ const cached = Number.isFinite(cachedRaw) ? cachedRaw : 0;
92
+ const input = baseInput + cached;
85
93
  const outputRaw = Number((asAny.output_tokens ?? asAny.completion_tokens));
86
- const input = Number.isFinite(inputRaw) ? inputRaw : 0;
87
94
  const output = Number.isFinite(outputRaw) ? outputRaw : 0;
88
95
  const totalRaw = Number(asAny.total_tokens);
89
96
  const total = Number.isFinite(totalRaw) ? totalRaw : input + output;
@@ -111,12 +111,16 @@ export function createAnthropicResponseBuilder(options) {
111
111
  break;
112
112
  }
113
113
  case 'message_delta': {
114
- const delta = event.data?.delta;
114
+ const data = event.data ?? {};
115
+ const delta = data?.delta;
115
116
  if (delta?.stop_reason) {
116
117
  state.stopReason = delta.stop_reason;
117
118
  }
118
- if (delta?.usage) {
119
- state.usage = delta.usage;
119
+ // 部分实现将 usage 挂在 delta.usage,部分实现挂在顶层 event.data.usage,
120
+ // 这里统一优先读取 delta.usage,缺失时回退到 data.usage。
121
+ const usageNode = (delta && delta.usage) ?? data.usage;
122
+ if (usageNode) {
123
+ state.usage = usageNode;
120
124
  }
121
125
  break;
122
126
  }
@@ -216,11 +216,12 @@ export function buildStructuredPatch(payload) {
216
216
  else {
217
217
  lines.push(`*** Update File: ${file}`);
218
218
  for (const hunk of section.hunks) {
219
- lines.push('@@');
219
+ // 每个 hunk 仅需一个开头的 "@@" 行,后面直接跟上下文/增删行。
220
220
  for (const entry of hunk) {
221
- lines.push(entry);
221
+ if (!entry.startsWith('@@')) {
222
+ lines.push(entry);
223
+ }
222
224
  }
223
- lines.push('@@');
224
225
  }
225
226
  }
226
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.568",
3
+ "version": "0.6.626",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -62,4 +62,4 @@
62
62
  "dist",
63
63
  "README.md"
64
64
  ]
65
- }
65
+ }