@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
@@ -134,42 +134,67 @@ function applyLocalToolGovernance(chatRequest, rawPayload) {
134
134
  };
135
135
  }
136
136
  function detectImageHint(messages, rawPayload) {
137
- const candidates = [];
138
- const collect = (value) => {
139
- if (typeof value === 'string' && value) {
140
- candidates.push(value);
141
- }
137
+ const patterns = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
138
+ const hasImageExt = (value) => {
139
+ if (typeof value !== 'string' || !value)
140
+ return false;
141
+ const lower = value.toLowerCase();
142
+ return patterns.some(ext => lower.includes(ext));
142
143
  };
143
- if (Array.isArray(messages)) {
144
- for (const msg of messages) {
145
- if (msg && typeof msg === 'object') {
146
- const text = msg.content;
147
- if (typeof text === 'string') {
148
- collect(text);
149
- }
150
- else if (Array.isArray(text)) {
151
- for (const part of text) {
152
- if (part && typeof part === 'object') {
153
- collect(part.text);
154
- }
155
- }
144
+ const hasImageInMessage = (msg) => {
145
+ if (!msg || typeof msg !== 'object')
146
+ return false;
147
+ const m = msg;
148
+ const content = m.content;
149
+ if (typeof content === 'string') {
150
+ if (hasImageExt(content))
151
+ return true;
152
+ }
153
+ else if (Array.isArray(content)) {
154
+ for (const part of content) {
155
+ if (!part || typeof part !== 'object')
156
+ continue;
157
+ const p = part;
158
+ const t = String(p.type || '').toLowerCase();
159
+ if (t.includes('image')) {
160
+ return true;
156
161
  }
162
+ if (hasImageExt(p.text))
163
+ return true;
164
+ const imageUrl = typeof p.image_url === 'string'
165
+ ? p.image_url
166
+ : p.image_url && typeof p.image_url.url === 'string'
167
+ ? p.image_url.url
168
+ : typeof p.url === 'string'
169
+ ? p.url
170
+ : undefined;
171
+ if (hasImageExt(imageUrl))
172
+ return true;
173
+ if (hasImageExt(p.path))
174
+ return true;
157
175
  }
158
176
  }
159
- }
160
- if (rawPayload && typeof rawPayload === 'object') {
161
- collect(rawPayload.content);
162
- }
163
- if (!candidates.length) {
164
177
  return false;
165
- }
166
- const patterns = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
167
- for (const text of candidates) {
168
- const lower = text.toLowerCase();
169
- for (const ext of patterns) {
170
- if (lower.includes(ext)) {
178
+ };
179
+ // 仅考虑“当前这一轮”的用户输入是否包含图片链接或图片负载,避免因为历史上下文中曾经出现过图片而在后续轮次持续暴露 view_image。
180
+ if (Array.isArray(messages)) {
181
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
182
+ const msg = messages[i];
183
+ if (!msg || typeof msg !== 'object')
184
+ continue;
185
+ const role = String(msg.role || '').toLowerCase();
186
+ if (role !== 'user')
187
+ continue;
188
+ if (hasImageInMessage(msg)) {
171
189
  return true;
172
190
  }
191
+ break;
192
+ }
193
+ }
194
+ if (rawPayload && typeof rawPayload === 'object') {
195
+ const text = rawPayload.content;
196
+ if (hasImageExt(text)) {
197
+ return true;
173
198
  }
174
199
  }
175
200
  return false;
@@ -1,3 +1,4 @@
1
+ export type { ToolFilterHints, ToolFilterDecision, ToolFilterAction } from './types.js';
1
2
  export * from './types.js';
2
3
  export * from './engine.js';
3
4
  export * from './builtin/whitelist-filter.js';
@@ -48,15 +48,23 @@ export class ResponseApplyPatchToonDecodeFilter {
48
48
  if (typeof nameRaw !== 'string' || nameRaw.trim().toLowerCase() !== 'apply_patch') {
49
49
  continue;
50
50
  }
51
- const argStr = fn.arguments;
52
- if (typeof argStr !== 'string' || !argStr.trim())
53
- continue;
51
+ const argIn = fn.arguments;
54
52
  let parsed;
55
- try {
56
- parsed = JSON.parse(argStr);
53
+ if (typeof argIn === 'string') {
54
+ if (!argIn.trim())
55
+ continue;
56
+ try {
57
+ parsed = JSON.parse(argIn);
58
+ }
59
+ catch {
60
+ // 如果 arguments 不是 JSON 字符串,则保持原样交给下游处理
61
+ continue;
62
+ }
57
63
  }
58
- catch {
59
- // 如果 arguments 不是 JSON 字符串,则保持原样交给下游处理
64
+ else if (isObject(argIn)) {
65
+ parsed = argIn;
66
+ }
67
+ else {
60
68
  continue;
61
69
  }
62
70
  if (!isObject(parsed))
@@ -1,3 +1,4 @@
1
+ import { isShellToolName, normalizeToolName } from '../../tools/tool-description-utils.js';
1
2
  function envEnabled() {
2
3
  // Default ON. Allow disabling via env RCC_TOON_ENABLE/ROUTECODEX_TOON_ENABLE = 0|false|off
3
4
  const v = String(process?.env?.RCC_TOON_ENABLE || process?.env?.ROUTECODEX_TOON_ENABLE || '').toLowerCase();
@@ -46,6 +47,31 @@ function decodeToonPairs(toon) {
46
47
  return null;
47
48
  }
48
49
  }
50
+ function coerceToPrimitive(value) {
51
+ const trimmed = value.trim();
52
+ if (!trimmed)
53
+ return '';
54
+ const lower = trimmed.toLowerCase();
55
+ if (lower === 'true')
56
+ return true;
57
+ if (lower === 'false')
58
+ return false;
59
+ if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
60
+ const num = Number(trimmed);
61
+ if (Number.isFinite(num))
62
+ return num;
63
+ }
64
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
65
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
66
+ try {
67
+ return JSON.parse(trimmed);
68
+ }
69
+ catch {
70
+ // fall through
71
+ }
72
+ }
73
+ return value;
74
+ }
49
75
  /**
50
76
  * Decode arguments.toon to standard JSON ({command, workdir?}) and map tool name 'shell_toon' → 'shell'.
51
77
  * Stage: response_pre (before arguments stringify and invariants).
@@ -68,15 +94,23 @@ export class ResponseToolArgumentsToonDecodeFilter {
68
94
  const fn = tc && tc.function ? tc.function : undefined;
69
95
  if (!fn || typeof fn !== 'object')
70
96
  continue;
71
- const argStr = fn.arguments;
72
- if (typeof argStr !== 'string')
73
- continue;
74
- let parsed;
75
- try {
76
- parsed = JSON.parse(argStr);
97
+ const rawName = fn.name;
98
+ const toolName = typeof rawName === 'string' ? rawName : '';
99
+ const normalizedName = normalizeToolName(toolName);
100
+ const isShellLike = isShellToolName(toolName);
101
+ const isApplyPatch = normalizedName === 'apply_patch';
102
+ const argIn = fn.arguments;
103
+ let parsed = undefined;
104
+ if (typeof argIn === 'string') {
105
+ try {
106
+ parsed = JSON.parse(argIn);
107
+ }
108
+ catch {
109
+ parsed = undefined;
110
+ }
77
111
  }
78
- catch {
79
- continue;
112
+ else if (isObject(argIn)) {
113
+ parsed = argIn;
80
114
  }
81
115
  if (!isObject(parsed))
82
116
  continue;
@@ -102,24 +136,76 @@ export class ResponseToolArgumentsToonDecodeFilter {
102
136
  }
103
137
  continue; // keep original if decode fails
104
138
  }
105
- const command = typeof kv['command'] === 'string' && kv['command'].trim()
106
- ? kv['command']
107
- : (typeof kv['cmd'] === 'string' && kv['cmd'].trim() ? kv['cmd'] : undefined);
108
- const workdir = typeof kv['workdir'] === 'string' && kv['workdir'].trim()
109
- ? kv['workdir']
110
- : (typeof kv['cwd'] === 'string' && kv['cwd'].trim() ? kv['cwd'] : undefined);
111
- if (typeof command === 'string' && command.trim()) {
112
- const merged = { cmd: command };
113
- if (typeof workdir === 'string' && workdir.trim()) {
114
- merged.workdir = workdir;
139
+ // apply_patch toon 由专门的 ResponseApplyPatchToonDecodeFilter 处理,这里跳过,避免覆盖。
140
+ if (isApplyPatch) {
141
+ continue;
142
+ }
143
+ if (isShellLike) {
144
+ const commandRaw = (typeof kv['command'] === 'string' && kv['command'].trim()
145
+ ? kv['command']
146
+ : typeof kv['cmd'] === 'string' && kv['cmd'].trim()
147
+ ? kv['cmd']
148
+ : undefined) ?? '';
149
+ const workdirRaw = (typeof kv['workdir'] === 'string' && kv['workdir'].trim()
150
+ ? kv['workdir']
151
+ : typeof kv['cwd'] === 'string' && kv['cwd'].trim()
152
+ ? kv['cwd']
153
+ : undefined) ?? '';
154
+ const timeoutRaw = typeof kv['timeout_ms'] === 'string' ? kv['timeout_ms'] : undefined;
155
+ const escalatedRaw = typeof kv['with_escalated_permissions'] === 'string'
156
+ ? kv['with_escalated_permissions']
157
+ : undefined;
158
+ const justificationRaw = typeof kv['justification'] === 'string' ? kv['justification'] : undefined;
159
+ const command = commandRaw.trim();
160
+ if (command) {
161
+ const merged = {
162
+ cmd: command,
163
+ command
164
+ };
165
+ const workdir = workdirRaw.trim();
166
+ if (workdir) {
167
+ merged.workdir = workdir;
168
+ }
169
+ if (timeoutRaw) {
170
+ const timeoutNum = Number(timeoutRaw);
171
+ if (Number.isFinite(timeoutNum)) {
172
+ merged.timeout_ms = timeoutNum;
173
+ }
174
+ }
175
+ if (escalatedRaw) {
176
+ const escLower = escalatedRaw.trim().toLowerCase();
177
+ if (escLower === 'true' || escLower === 'yes' || escLower === '1') {
178
+ merged.with_escalated_permissions = true;
179
+ }
180
+ else if (escLower === 'false' || escLower === 'no' || escLower === '0') {
181
+ merged.with_escalated_permissions = false;
182
+ }
183
+ }
184
+ if (justificationRaw && justificationRaw.trim()) {
185
+ merged.justification = justificationRaw;
186
+ }
187
+ try {
188
+ fn.arguments = JSON.stringify(merged);
189
+ }
190
+ catch {
191
+ /* keep original */
192
+ }
193
+ if (typeof fn.name === 'string' && fn.name === 'shell_toon') {
194
+ fn.name = 'shell';
195
+ }
196
+ }
197
+ }
198
+ else {
199
+ // 通用 TOON → JSON 解码:除 shell / apply_patch 以外的工具,将 key: value 对映射为普通 JSON 字段。
200
+ const merged = {};
201
+ for (const [key, value] of Object.entries(kv)) {
202
+ merged[key] = coerceToPrimitive(value);
115
203
  }
116
- merged.command = command;
117
204
  try {
118
205
  fn.arguments = JSON.stringify(merged);
119
206
  }
120
- catch { /* keep original */ }
121
- if (typeof fn.name === 'string' && fn.name === 'shell_toon') {
122
- fn.name = 'shell';
207
+ catch {
208
+ // keep original on stringify failure
123
209
  }
124
210
  }
125
211
  }
@@ -227,6 +227,7 @@ export function augmentAnthropicTools(tools) {
227
227
  const marker = '[Codex ApplyPatch Guidance]';
228
228
  const guidance = [
229
229
  marker,
230
+ 'Before using apply_patch, always read the latest content of the target file (via shell or another tool) and base your changes on that content.',
230
231
  'Provide structured changes (insert_after / insert_before / replace / delete / create_file / delete_file) instead of raw patch text.',
231
232
  'Each change must include the target file (relative path) plus anchor/target snippets and the replacement lines.',
232
233
  '所有路径必须相对工作区根目录,禁止输出以 / 或盘符开头的绝对路径。'
@@ -264,6 +265,7 @@ export function buildSystemToolGuidance() {
264
265
  lines.push(bullet('function.arguments must be a single JSON string. / arguments 必须是单个 JSON 字符串。'));
265
266
  lines.push(bullet('shell: Place ALL intent into the command argv array only; do not invent extra keys. / shell 所有意图写入 command 数组,不要添加额外键名。'));
266
267
  lines.push(bullet('File writes are FORBIDDEN via shell (no redirection, no here-doc, no sed -i, no ed -s, no tee). Use apply_patch ONLY. / 通过 shell 写文件一律禁止(不得使用重定向、heredoc、sed -i、ed -s、tee);必须使用 apply_patch。'));
268
+ lines.push(bullet('apply_patch: Before writing, always read the target file first and compute changes against the latest content using appropriate tools. / apply_patch 在写入前必须先通过合适的工具读取目标文件最新内容,并基于该内容生成变更。'));
267
269
  lines.push(bullet('apply_patch: Provide structured JSON arguments with a `changes` array (insert_after / insert_before / replace / delete / create_file / delete_file); omit "+/-" prefixes in `lines`; file paths必须是相对路径。 / apply_patch 仅接受结构化 JSON。'));
268
270
  lines.push(bullet('update_plan: Keep exactly one step in_progress; others pending/completed. / 仅一个 in_progress 步骤。'));
269
271
  lines.push(bullet('view_image: Path must be an image file (.png .jpg .jpeg .gif .webp .bmp .svg). / 仅图片路径。'));
@@ -11,22 +11,23 @@ export class RoutingClassifier {
11
11
  }
12
12
  classify(features) {
13
13
  const lastToolCategory = features.lastAssistantToolCategory;
14
- const userInterruptsToolContinuation = features.latestMessageFromUser === true && !features.hasTools && !features.hasToolCallResponses;
15
- const allowContinuation = !userInterruptsToolContinuation;
16
14
  const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
17
15
  const latestMessageFromUser = features.latestMessageFromUser === true;
18
- const codingContinuation = allowContinuation && lastToolCategory === 'write';
19
- const thinkingContinuation = allowContinuation && lastToolCategory === 'read';
20
- const searchContinuation = allowContinuation && lastToolCategory === 'search';
21
- const toolsContinuation = allowContinuation && lastToolCategory === 'other';
16
+ const thinkingContinuation = lastToolCategory === 'read';
17
+ const thinkingFromUser = latestMessageFromUser;
18
+ const thinkingFromRead = !thinkingFromUser && thinkingContinuation;
19
+ const codingContinuation = lastToolCategory === 'write';
20
+ const searchContinuation = lastToolCategory === 'search';
21
+ const toolsContinuation = lastToolCategory === 'other';
22
+ const hasToolActivity = features.hasTools || features.hasToolCallResponses;
22
23
  const evaluationMap = {
23
24
  vision: {
24
25
  triggered: features.hasImageAttachment,
25
26
  reason: 'vision:image-detected'
26
27
  },
27
28
  thinking: {
28
- triggered: latestMessageFromUser,
29
- reason: 'thinking:user-input'
29
+ triggered: thinkingFromUser || thinkingFromRead,
30
+ reason: thinkingFromUser ? 'thinking:user-input' : 'thinking:last-tool-read'
30
31
  },
31
32
  longcontext: {
32
33
  triggered: reachedLongContext,
@@ -36,9 +37,11 @@ export class RoutingClassifier {
36
37
  triggered: codingContinuation,
37
38
  reason: 'coding:last-tool-write'
38
39
  },
39
- thinking_continuation: {
40
- triggered: thinkingContinuation,
41
- reason: 'thinking:last-tool-read'
40
+ web_search: {
41
+ // web_search 路由不再基于上一轮工具分类或本轮是否声明 web_search 工具自动触发,
42
+ // 仅保留为显式路由指令/未来扩展的占位,默认不命中。
43
+ triggered: false,
44
+ reason: 'web_search:disabled'
42
45
  },
43
46
  search: {
44
47
  // search 路由:仅在上一轮 assistant 使用 search 类工具时继续命中,
@@ -48,7 +51,8 @@ export class RoutingClassifier {
48
51
  },
49
52
  tools: {
50
53
  // tools 路由:通用工具分支,包括首次声明的 web/search 工具。
51
- triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
54
+ // 若上一轮已明确归类为 search,则优先命中 search 路由,tools 仅作为兜底。
55
+ triggered: toolsContinuation || (!searchContinuation && hasToolActivity),
52
56
  reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
53
57
  },
54
58
  background: {
@@ -130,6 +130,37 @@ export class VirtualRouterEngine {
130
130
  this.debug?.log?.('[virtual-router-hit]', selection.routeUsed, selection.providerKey, target.modelId || '', hitReason ? `reason=${hitReason}` : '');
131
131
  }
132
132
  const didFallback = selection.routeUsed !== requestedRoute;
133
+ // 自动 sticky:对需要上下文 save/restore 的 Responses 会话,强制同一个 provider.key.model。
134
+ // 其它协议不启用粘滞,仅显式 routing 指令才会写入 stickyTarget。
135
+ const providerProtocol = metadata?.providerProtocol;
136
+ const serverToolRequired = metadata?.serverToolRequired === true;
137
+ const disableSticky = metadata &&
138
+ typeof metadata === 'object' &&
139
+ metadata.disableStickyRoutes === true;
140
+ const shouldAutoStickyForResponses = providerProtocol === 'openai-responses' && serverToolRequired && !disableSticky;
141
+ if (shouldAutoStickyForResponses) {
142
+ const stickyKeyForState = this.resolveStickyKey(metadata);
143
+ if (stickyKeyForState) {
144
+ const stateKey = stickyKeyForState;
145
+ const state = this.getRoutingInstructionState(stateKey);
146
+ if (!state.stickyTarget) {
147
+ const providerId = this.extractProviderId(selection.providerKey);
148
+ if (providerId) {
149
+ const parts = selection.providerKey.split('.');
150
+ const keyAlias = parts.length >= 3 ? parts[1] : undefined;
151
+ const modelId = target.modelId;
152
+ state.stickyTarget = {
153
+ provider: providerId,
154
+ keyAlias,
155
+ model: modelId,
156
+ // pathLength=3 表示 provider.key.model 形式,对应 alias 显式 sticky;
157
+ // 若缺少别名或模型,则退化为更短 pathLength。
158
+ pathLength: keyAlias && modelId ? 3 : keyAlias ? 2 : 1
159
+ };
160
+ }
161
+ }
162
+ }
163
+ }
133
164
  return {
134
165
  target,
135
166
  decision: {
@@ -263,14 +294,24 @@ export class VirtualRouterEngine {
263
294
  return this.healthManager.getConfig();
264
295
  }
265
296
  resolveStickyKey(metadata) {
297
+ const providerProtocol = metadata.providerProtocol;
298
+ // 对 Responses 协议的自动粘滞,仅在“单次会话链路”内生效:
299
+ // - Resume/submit 调用:stickyKey = previousRequestId(指向首轮请求);
300
+ // - 普通 /v1/responses 调用:stickyKey = 本次 requestId;
301
+ // 这样不会把 Responses 的自动粘滞扩散到整个 session,仅在需要 save/restore
302
+ // 的请求链路中复用 provider.key.model。
303
+ if (providerProtocol === 'openai-responses') {
304
+ const resume = metadata.responsesResume;
305
+ if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
306
+ return resume.previousRequestId.trim();
307
+ }
308
+ return metadata.requestId;
309
+ }
310
+ // 其它协议沿用会话级 sticky 语义:sessionId / conversationId → requestId。
266
311
  const sessionScope = this.resolveSessionScope(metadata);
267
312
  if (sessionScope) {
268
313
  return sessionScope;
269
314
  }
270
- const resume = metadata.responsesResume;
271
- if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
272
- return resume.previousRequestId.trim();
273
- }
274
315
  return metadata.requestId;
275
316
  }
276
317
  resolveSessionScope(metadata) {
@@ -1,5 +1,5 @@
1
1
  import type { StandardizedMessage, StandardizedRequest } from '../../conversion/hub/types/standardized.js';
2
- export type ToolCategory = 'read' | 'write' | 'search' | 'other';
2
+ export type ToolCategory = 'read' | 'write' | 'search' | 'websearch' | 'other';
3
3
  export type ToolClassification = {
4
4
  category: ToolCategory;
5
5
  name: string;
@@ -10,4 +10,5 @@ export declare function detectCodingTool(request: StandardizedRequest): boolean;
10
10
  export declare function detectWebTool(request: StandardizedRequest): boolean;
11
11
  export declare function extractMeaningfulDeclaredToolNames(tools: StandardizedRequest['tools'] | undefined): string[];
12
12
  export declare function detectLastAssistantToolCategory(messages: StandardizedMessage[]): ToolClassification | undefined;
13
+ export declare function classifyToolCallForReport(call: StandardizedMessage['tool_calls'][number]): ToolClassification | undefined;
13
14
  export declare function canonicalizeToolName(rawName: string): string;