@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
@@ -2,6 +2,7 @@ import { isJsonObject, jsonClone } from '../types/json.js';
2
2
  import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
3
3
  import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
4
4
  import { captureResponsesContext, buildChatRequestFromResponses, buildResponsesRequestFromChat } from '../../responses/responses-openai-bridge.js';
5
+ import { maybeAugmentApplyPatchErrorContent } from './chat-mapper.js';
5
6
  const RESPONSES_PARAMETER_KEYS = [
6
7
  'model',
7
8
  'temperature',
@@ -19,6 +20,7 @@ const RESPONSES_PARAMETER_KEYS = [
19
20
  'stop_sequences',
20
21
  'modalities'
21
22
  ];
23
+ const RESPONSES_SUBMIT_ENDPOINT = '/v1/responses.submit_tool_outputs';
22
24
  function mapToolOutputs(entries, missing) {
23
25
  if (!entries || !entries.length)
24
26
  return undefined;
@@ -45,10 +47,12 @@ function mapToolOutputs(entries, missing) {
45
47
  content = String(entry.output);
46
48
  }
47
49
  }
50
+ const nameValue = typeof entry.name === 'string' ? entry.name : undefined;
51
+ const augmented = maybeAugmentApplyPatchErrorContent(content, nameValue);
48
52
  outputs.push({
49
53
  tool_call_id: String(callId),
50
- content,
51
- name: typeof entry.name === 'string' ? entry.name : undefined
54
+ content: augmented,
55
+ name: nameValue
52
56
  });
53
57
  });
54
58
  return outputs.length ? outputs : undefined;
@@ -197,6 +201,259 @@ function mergeMetadata(a, b) {
197
201
  const right = jsonClone(b);
198
202
  return { ...left, ...right };
199
203
  }
204
+ function isSubmitToolOutputsEndpoint(ctx) {
205
+ if (!ctx || typeof ctx !== 'object') {
206
+ return false;
207
+ }
208
+ const entry = typeof ctx.entryEndpoint === 'string' ? ctx.entryEndpoint.trim().toLowerCase() : '';
209
+ return entry === RESPONSES_SUBMIT_ENDPOINT;
210
+ }
211
+ function attachResponsesSemantics(existing, context, resume) {
212
+ if (!context && !resume) {
213
+ return existing;
214
+ }
215
+ const next = existing ? { ...existing } : {};
216
+ const currentNode = next.responses && isJsonObject(next.responses) ? { ...next.responses } : {};
217
+ if (context) {
218
+ currentNode.context = jsonClone(context);
219
+ }
220
+ if (resume) {
221
+ currentNode.resume = jsonClone(resume);
222
+ }
223
+ next.responses = currentNode;
224
+ return next;
225
+ }
226
+ function extractResponsesSemanticsNode(chat) {
227
+ if (!chat?.semantics || typeof chat.semantics !== 'object') {
228
+ return undefined;
229
+ }
230
+ const node = chat.semantics.responses;
231
+ return node && isJsonObject(node) ? node : undefined;
232
+ }
233
+ function readResponsesContextFromSemantics(chat) {
234
+ const node = extractResponsesSemanticsNode(chat);
235
+ if (!node) {
236
+ return undefined;
237
+ }
238
+ const contextNode = node.context;
239
+ if (!contextNode || !isJsonObject(contextNode)) {
240
+ return undefined;
241
+ }
242
+ return jsonClone(contextNode);
243
+ }
244
+ function readResponsesResumeFromSemantics(chat) {
245
+ const node = extractResponsesSemanticsNode(chat);
246
+ if (!node) {
247
+ return undefined;
248
+ }
249
+ const resumeNode = node.resume;
250
+ if (!resumeNode || !isJsonObject(resumeNode)) {
251
+ return undefined;
252
+ }
253
+ return jsonClone(resumeNode);
254
+ }
255
+ function selectResponsesContextSnapshot(chat, envelopeMetadata) {
256
+ const semanticsContext = readResponsesContextFromSemantics(chat);
257
+ const metadataContextCandidate = chat.metadata?.responsesContext;
258
+ const metadataContext = metadataContextCandidate && isJsonObject(metadataContextCandidate)
259
+ ? jsonClone(metadataContextCandidate)
260
+ : undefined;
261
+ const context = semanticsContext ??
262
+ metadataContext ??
263
+ {
264
+ metadata: envelopeMetadata
265
+ };
266
+ const mergedMetadata = mergeMetadata(context.metadata ?? undefined, envelopeMetadata);
267
+ if (mergedMetadata) {
268
+ context.metadata = mergedMetadata;
269
+ }
270
+ return context;
271
+ }
272
+ function resolveSubmitResponseId(ctx, responsesContext) {
273
+ const resumeMeta = ctx.responsesResume && typeof ctx.responsesResume === 'object'
274
+ ? ctx.responsesResume
275
+ : undefined;
276
+ const resumeId = typeof resumeMeta?.restoredFromResponseId === 'string'
277
+ ? resumeMeta.restoredFromResponseId.trim()
278
+ : undefined;
279
+ if (resumeId) {
280
+ return resumeId;
281
+ }
282
+ const contextRecord = responsesContext && typeof responsesContext === 'object'
283
+ ? responsesContext
284
+ : undefined;
285
+ const previousId = typeof contextRecord?.previous_response_id === 'string'
286
+ ? contextRecord.previous_response_id.trim()
287
+ : undefined;
288
+ if (previousId) {
289
+ return previousId;
290
+ }
291
+ return undefined;
292
+ }
293
+ function extractSubmitMetadata(responsesContext, resumeMeta) {
294
+ if (responsesContext && responsesContext.metadata && isJsonObject(responsesContext.metadata)) {
295
+ return jsonClone(responsesContext.metadata);
296
+ }
297
+ if (resumeMeta && resumeMeta.metadata && isJsonObject(resumeMeta.metadata)) {
298
+ return jsonClone(resumeMeta.metadata);
299
+ }
300
+ return undefined;
301
+ }
302
+ function coerceOutputText(value) {
303
+ if (typeof value === 'string') {
304
+ return value;
305
+ }
306
+ if (value === undefined || value === null) {
307
+ return '';
308
+ }
309
+ try {
310
+ return JSON.stringify(value);
311
+ }
312
+ catch {
313
+ return String(value);
314
+ }
315
+ }
316
+ function extractCapturedToolOutputs(responsesContext) {
317
+ if (!responsesContext || typeof responsesContext !== 'object') {
318
+ return [];
319
+ }
320
+ const snapshot = responsesContext.__captured_tool_results;
321
+ if (!Array.isArray(snapshot) || !snapshot.length) {
322
+ return [];
323
+ }
324
+ const entries = [];
325
+ snapshot.forEach((entry) => {
326
+ if (!entry || typeof entry !== 'object') {
327
+ return;
328
+ }
329
+ const record = entry;
330
+ const toolId = typeof record.tool_call_id === 'string' && record.tool_call_id.trim().length
331
+ ? record.tool_call_id.trim()
332
+ : typeof record.call_id === 'string' && record.call_id.trim().length
333
+ ? record.call_id.trim()
334
+ : undefined;
335
+ if (!toolId) {
336
+ return;
337
+ }
338
+ entries.push({
339
+ tool_call_id: toolId,
340
+ id: toolId,
341
+ output: typeof record.output === 'string' ? record.output : coerceOutputText(record.output),
342
+ name: typeof record.name === 'string' ? record.name : undefined
343
+ });
344
+ });
345
+ return entries;
346
+ }
347
+ function collectSubmitToolOutputs(chat, ctx, responsesContext) {
348
+ const outputs = [];
349
+ const seen = new Set();
350
+ const append = (idSeed, outputSeed, name) => {
351
+ const trimmed = typeof idSeed === 'string' && idSeed.trim().length ? idSeed.trim() : '';
352
+ const fallbackId = trimmed || `submit_tool_${outputs.length + 1}`;
353
+ if (seen.has(fallbackId)) {
354
+ return;
355
+ }
356
+ seen.add(fallbackId);
357
+ outputs.push({
358
+ tool_call_id: fallbackId,
359
+ id: fallbackId,
360
+ output: coerceOutputText(outputSeed),
361
+ name
362
+ });
363
+ };
364
+ if (Array.isArray(chat.toolOutputs) && chat.toolOutputs.length) {
365
+ chat.toolOutputs.forEach((entry) => {
366
+ append(entry.tool_call_id ?? entry.call_id, entry.content, entry.name);
367
+ });
368
+ }
369
+ if (!outputs.length) {
370
+ const captured = extractCapturedToolOutputs(responsesContext);
371
+ captured.forEach((entry) => append(entry.tool_call_id ?? entry.id, entry.output, entry.name));
372
+ }
373
+ if (!outputs.length) {
374
+ const resume = ctx.responsesResume && typeof ctx.responsesResume === 'object'
375
+ ? ctx.responsesResume
376
+ : undefined;
377
+ const detailed = Array.isArray(resume?.toolOutputsDetailed)
378
+ ? resume?.toolOutputsDetailed
379
+ : undefined;
380
+ if (detailed) {
381
+ detailed.forEach((entry, index) => {
382
+ if (!entry || typeof entry !== 'object') {
383
+ return;
384
+ }
385
+ const callId = typeof entry.callId === 'string' && entry.callId.trim().length
386
+ ? entry.callId.trim()
387
+ : typeof entry.originalId === 'string' && entry.originalId.trim().length
388
+ ? entry.originalId.trim()
389
+ : `resume_tool_${index + 1}`;
390
+ append(callId, typeof entry.outputText === 'string' ? entry.outputText : entry.outputText ?? '');
391
+ });
392
+ }
393
+ }
394
+ return outputs;
395
+ }
396
+ function resolveSubmitStreamFlag(chat, ctx, responsesContext) {
397
+ if (chat.parameters && typeof chat.parameters.stream === 'boolean') {
398
+ return chat.parameters.stream;
399
+ }
400
+ if (responsesContext && typeof responsesContext.stream === 'boolean') {
401
+ return responsesContext.stream;
402
+ }
403
+ if (ctx.streamingHint === 'force') {
404
+ return true;
405
+ }
406
+ if (ctx.streamingHint === 'disable') {
407
+ return false;
408
+ }
409
+ return undefined;
410
+ }
411
+ function resolveSubmitModel(chat, responsesContext) {
412
+ const direct = chat.parameters && typeof chat.parameters.model === 'string'
413
+ ? chat.parameters.model.trim()
414
+ : undefined;
415
+ if (direct) {
416
+ return direct;
417
+ }
418
+ if (responsesContext && typeof responsesContext.parameters === 'object') {
419
+ const params = responsesContext.parameters;
420
+ const model = typeof params.model === 'string' ? params.model.trim() : undefined;
421
+ if (model) {
422
+ return model;
423
+ }
424
+ }
425
+ return undefined;
426
+ }
427
+ function buildSubmitToolOutputsPayload(chat, ctx, responsesContext) {
428
+ const responseId = resolveSubmitResponseId(ctx, responsesContext);
429
+ if (!responseId) {
430
+ throw new Error('Submit tool outputs requires response_id from Responses resume context');
431
+ }
432
+ const toolOutputs = collectSubmitToolOutputs(chat, ctx, responsesContext);
433
+ if (!toolOutputs.length) {
434
+ throw new Error('Submit tool outputs requires at least one tool output entry');
435
+ }
436
+ const resumeMeta = ctx.responsesResume && typeof ctx.responsesResume === 'object'
437
+ ? ctx.responsesResume
438
+ : undefined;
439
+ const payload = {
440
+ response_id: responseId,
441
+ tool_outputs: toolOutputs
442
+ };
443
+ const modelValue = resolveSubmitModel(chat, responsesContext);
444
+ if (modelValue) {
445
+ payload.model = modelValue;
446
+ }
447
+ const streamValue = resolveSubmitStreamFlag(chat, ctx, responsesContext);
448
+ if (typeof streamValue === 'boolean') {
449
+ payload.stream = streamValue;
450
+ }
451
+ const metadata = extractSubmitMetadata(responsesContext, resumeMeta);
452
+ if (metadata) {
453
+ payload.metadata = metadata;
454
+ }
455
+ return payload;
456
+ }
200
457
  export class ResponsesSemanticMapper {
201
458
  async toChat(format, ctx) {
202
459
  const payload = format.payload || {};
@@ -248,15 +505,35 @@ export class ResponsesSemanticMapper {
248
505
  if (responsesContext.responseFormat) {
249
506
  metadata.responseFormat = jsonClone(responsesContext.responseFormat);
250
507
  }
508
+ metadata.responsesContext = jsonClone(responsesContext);
509
+ const resumeNode = ctx.responsesResume && isJsonObject(ctx.responsesResume)
510
+ ? ctx.responsesResume
511
+ : undefined;
512
+ const semantics = attachResponsesSemantics(undefined, responsesContext, resumeNode);
251
513
  return {
252
514
  messages,
253
515
  tools: normalizeTools(toolsNormalized, missingFields),
254
516
  toolOutputs,
255
517
  parameters,
518
+ semantics,
256
519
  metadata
257
520
  };
258
521
  }
259
522
  async fromChat(chat, ctx) {
523
+ const envelopeMetadata = chat.metadata && isJsonObject(chat.metadata) ? chat.metadata : undefined;
524
+ const responsesContext = selectResponsesContextSnapshot(chat, envelopeMetadata);
525
+ if (isSubmitToolOutputsEndpoint(ctx)) {
526
+ const submitPayload = buildSubmitToolOutputsPayload(chat, ctx, responsesContext);
527
+ return {
528
+ protocol: 'openai-responses',
529
+ direction: 'response',
530
+ payload: submitPayload,
531
+ meta: {
532
+ context: ctx,
533
+ submitToolOutputs: true
534
+ }
535
+ };
536
+ }
260
537
  const modelValue = chat.parameters?.model;
261
538
  if (typeof modelValue !== 'string' || !modelValue.trim()) {
262
539
  throw new Error('ChatEnvelope.parameters.model is required for openai-responses outbound conversion');
@@ -271,18 +548,7 @@ export class ResponsesSemanticMapper {
271
548
  .filter((message) => Boolean(message && typeof message === 'object' && message.role === 'system'))
272
549
  .map(message => serializeSystemContent(message))
273
550
  .filter((content) => typeof content === 'string' && content.length > 0);
274
- const capturedContext = chat.metadata?.responsesContext;
275
- const envelopeMetadata = chat.metadata && isJsonObject(chat.metadata) ? chat.metadata : undefined;
276
- const responsesContext = isJsonObject(capturedContext)
277
- ? {
278
- ...capturedContext,
279
- metadata: mergeMetadata(capturedContext.metadata, envelopeMetadata),
280
- originalSystemMessages
281
- }
282
- : {
283
- metadata: envelopeMetadata,
284
- originalSystemMessages
285
- };
551
+ responsesContext.originalSystemMessages = originalSystemMessages;
286
552
  const responsesResult = buildResponsesRequestFromChat(requestShape, responsesContext);
287
553
  const responses = responsesResult.request;
288
554
  if (chat.parameters && chat.parameters.stream !== undefined) {
@@ -5,6 +5,7 @@ export function chatEnvelopeToStandardized(chat, options) {
5
5
  const model = extractModel(parameters);
6
6
  const messages = chat.messages.map((message) => normalizeChatMessage(message));
7
7
  const tools = normalizeTools(chat.tools);
8
+ const semantics = cloneSemantics(chat.semantics);
8
9
  const metadataCaptured = {};
9
10
  const hubState = {};
10
11
  if (Array.isArray(chat.metadata?.missingFields) && chat.metadata?.missingFields.length) {
@@ -35,7 +36,8 @@ export function chatEnvelopeToStandardized(chat, options) {
35
36
  capturedContext: metadataCaptured,
36
37
  requestId: options.requestId,
37
38
  stream: parameters.stream === true
38
- }
39
+ },
40
+ semantics
39
41
  };
40
42
  return standardized;
41
43
  }
@@ -93,7 +95,8 @@ export function standardizedToChatEnvelope(request, options) {
93
95
  messages,
94
96
  tools,
95
97
  parameters,
96
- metadata
98
+ metadata,
99
+ semantics: cloneSemantics(request.semantics)
97
100
  };
98
101
  }
99
102
  function extractModel(parameters) {
@@ -103,6 +106,12 @@ function extractModel(parameters) {
103
106
  }
104
107
  throw new Error('ChatEnvelope parameters must include model string');
105
108
  }
109
+ function cloneSemantics(value) {
110
+ if (!value) {
111
+ return value;
112
+ }
113
+ return jsonClone(value);
114
+ }
106
115
  function normalizeChatMessage(message) {
107
116
  const normalized = {
108
117
  role: message.role,
@@ -56,11 +56,21 @@ export interface AdapterContext {
56
56
  responsesResume?: JsonObject;
57
57
  [key: string]: JsonValue;
58
58
  }
59
+ export interface ChatSemantics extends JsonObject {
60
+ session?: JsonObject;
61
+ system?: JsonObject;
62
+ tools?: JsonObject;
63
+ responses?: JsonObject;
64
+ anthropic?: JsonObject;
65
+ gemini?: JsonObject;
66
+ providerExtras?: JsonObject;
67
+ }
59
68
  export interface ChatEnvelope {
60
69
  messages: ChatMessage[];
61
70
  tools?: ChatToolDefinition[];
62
71
  toolOutputs?: ChatToolOutput[];
63
72
  parameters?: JsonObject;
73
+ semantics?: ChatSemantics;
64
74
  metadata: {
65
75
  context: AdapterContext;
66
76
  missingFields?: MissingField[];
@@ -1,4 +1,4 @@
1
- import type { ChatMessageContentPart } from './chat-envelope.js';
1
+ import type { ChatMessageContentPart, ChatSemantics } from './chat-envelope.js';
2
2
  import type { JsonObject } from './json.js';
3
3
  export type ToolChoice = 'none' | 'auto' | 'required' | {
4
4
  type: 'function';
@@ -74,6 +74,7 @@ export interface StandardizedRequest {
74
74
  tools?: StandardizedTool[];
75
75
  parameters: StandardizedParameters;
76
76
  metadata: StandardizedMetadata;
77
+ semantics?: ChatSemantics;
77
78
  }
78
79
  export interface ProcessedRequest extends StandardizedRequest {
79
80
  processed: {
@@ -1,5 +1,6 @@
1
1
  import type { BridgeInputItem, BridgeToolDefinition } from '../shared/bridge-message-types.js';
2
2
  import type { ChatToolDefinition } from '../hub/types/chat-envelope.js';
3
+ import type { JsonObject, JsonValue } from '../hub/types/json.js';
3
4
  import type { BridgeInputBuildResult } from '../shared/bridge-message-utils.js';
4
5
  import type { ToolCallIdStyle } from '../shared/responses-tool-utils.js';
5
6
  type Unknown = Record<string, unknown>;
@@ -11,8 +12,8 @@ export interface ResponsesRequestContext extends Unknown {
11
12
  store?: unknown;
12
13
  toolChoice?: unknown;
13
14
  parallelToolCalls?: boolean;
14
- metadata?: Record<string, unknown> | undefined;
15
- responseFormat?: unknown;
15
+ metadata?: JsonObject;
16
+ responseFormat?: JsonValue;
16
17
  stream?: boolean;
17
18
  isChatPayload?: boolean;
18
19
  isResponsesPayload?: boolean;
@@ -10,7 +10,6 @@ import { normalizeMessageReasoningTools } from '../shared/reasoning-tool-normali
10
10
  import { createBridgeActionState, runBridgeActionPipeline } from '../shared/bridge-actions.js';
11
11
  import { resolveBridgePolicy, resolvePolicyActions } from '../shared/bridge-policies.js';
12
12
  import { buildResponsesOutputFromChat } from '../shared/responses-output-builder.js';
13
- import { consumeResponsesPayloadSnapshot, consumeResponsesPassthrough } from '../shared/responses-reasoning-registry.js';
14
13
  function isObject(v) {
15
14
  return !!v && typeof v === 'object' && !Array.isArray(v);
16
15
  }
@@ -29,7 +28,7 @@ export function captureResponsesContext(payload, dto) {
29
28
  store: payload.store,
30
29
  toolChoice: payload.tool_choice,
31
30
  parallelToolCalls: typeof payload.parallel_tool_calls === 'boolean' ? payload.parallel_tool_calls : undefined,
32
- metadata: (payload.metadata && typeof payload.metadata === 'object') ? { ...payload.metadata } : undefined,
31
+ metadata: (payload.metadata && typeof payload.metadata === 'object') ? payload.metadata : undefined,
33
32
  responseFormat: payload.response_format,
34
33
  stream: typeof payload.stream === 'boolean' ? payload.stream : undefined,
35
34
  isChatPayload: Array.isArray(payload.messages)
@@ -461,17 +460,6 @@ export function buildResponsesPayloadFromChat(payload, context) {
461
460
  const response = unwrapData(payload);
462
461
  if (!response || typeof response !== 'object')
463
462
  return payload;
464
- const snapshotKey = resolveSnapshotLookupKey(response, context);
465
- if (snapshotKey) {
466
- const passthrough = consumeResponsesPassthrough(snapshotKey);
467
- if (passthrough) {
468
- return passthrough;
469
- }
470
- const snapshot = consumeResponsesPayloadSnapshot(snapshotKey);
471
- if (snapshot) {
472
- return snapshot;
473
- }
474
- }
475
463
  if (response.object === 'response' && Array.isArray(response.output)) {
476
464
  return response;
477
465
  }
@@ -0,0 +1,20 @@
1
+ export type ToolCallLite = {
2
+ id?: string;
3
+ name: string;
4
+ args: string;
5
+ };
6
+ export declare function extractApplyPatchCallsFromText(text: string): ToolCallLite[] | null;
7
+ export declare function extractExecuteBlocksFromText(text: string): ToolCallLite[] | null;
8
+ export declare function extractXMLToolCallsFromText(text: string): ToolCallLite[] | null;
9
+ /**
10
+ * 提取简单 XML 形式的工具调用块,例如:
11
+ *
12
+ * <list_directory>
13
+ * <path>/path/to/dir</path>
14
+ * <recursive>false</recursive>
15
+ * </list_directory>
16
+ *
17
+ * 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
18
+ */
19
+ export declare function extractSimpleXmlToolsFromText(text: string): ToolCallLite[] | null;
20
+ export declare function normalizeAssistantTextToToolCalls(message: Record<string, any>): Record<string, any>;
@@ -11,7 +11,9 @@ const KNOWN_TOOLS = new Set([
11
11
  'view_image',
12
12
  'list_mcp_resources',
13
13
  'read_mcp_resource',
14
- 'list_mcp_resource_templates'
14
+ 'list_mcp_resource_templates',
15
+ // 文件/目录类工具(CLI 侧已有约定;此处只做文本→tool_calls 收割)
16
+ 'list_directory'
15
17
  ]);
16
18
  const ALLOWED_KEYS = {
17
19
  shell: new Set(['command', 'justification', 'timeout_ms', 'with_escalated_permissions', 'workdir']),
@@ -20,7 +22,8 @@ const ALLOWED_KEYS = {
20
22
  view_image: new Set(['path']),
21
23
  list_mcp_resources: new Set(['server', 'cursor', 'filter', 'root']),
22
24
  read_mcp_resource: new Set(['server', 'uri', 'cursor']),
23
- list_mcp_resource_templates: new Set(['server', 'cursor'])
25
+ list_mcp_resource_templates: new Set(['server', 'cursor']),
26
+ list_directory: new Set(['path', 'recursive'])
24
27
  };
25
28
  function normalizeKey(raw) {
26
29
  try {
@@ -259,9 +262,16 @@ export function extractXMLToolCallsFromText(text) {
259
262
  name = cand;
260
263
  }
261
264
  catch { /* ignore */ }
265
+ // 如果前一行抽不到合法函数名,但 arg_key 明显是 toon/command,
266
+ // 视为 CLI 通路下的 exec_command 工具,避免把半截 <tool_call> 当成纯文本丢弃。
267
+ const rawKey = (pm[1] || '').trim();
268
+ const normalizedKey = normalizeKey(rawKey).toLowerCase();
269
+ if (!name && (normalizedKey === 'toon' || normalizedKey === 'command')) {
270
+ name = 'exec_command';
271
+ }
262
272
  if (!name)
263
273
  continue;
264
- const k = normalizeKey((pm[1] || '').trim());
274
+ const k = normalizeKey(rawKey);
265
275
  let vRaw = (pm[2] || '').trim();
266
276
  const argsObj = {};
267
277
  if (k) {
@@ -305,6 +315,74 @@ export function extractXMLToolCallsFromText(text) {
305
315
  return null;
306
316
  }
307
317
  }
318
+ /**
319
+ * 提取简单 XML 形式的工具调用块,例如:
320
+ *
321
+ * <list_directory>
322
+ * <path>/path/to/dir</path>
323
+ * <recursive>false</recursive>
324
+ * </list_directory>
325
+ *
326
+ * 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
327
+ */
328
+ export function extractSimpleXmlToolsFromText(text) {
329
+ try {
330
+ if (typeof text !== 'string' || !text)
331
+ return null;
332
+ const out = [];
333
+ const blockRe = /<\s*([A-Za-z0-9_.-]+)\s*>([\s\S]*?)<\/\s*\1\s*>/gi;
334
+ let bm;
335
+ while ((bm = blockRe.exec(text)) !== null) {
336
+ const rawName = (bm[1] || '').trim();
337
+ const lname = rawName.toLowerCase();
338
+ if (!lname || lname === 'tool_call')
339
+ continue;
340
+ // 目前仅支持 list_directory,后续按需扩展
341
+ if (lname !== 'list_directory')
342
+ continue;
343
+ const inner = bm[2] || '';
344
+ const args = {};
345
+ const argRe = /<\s*([A-Za-z0-9_]+)\s*>([\s\S]*?)<\/\s*\1\s*>/gi;
346
+ let am;
347
+ while ((am = argRe.exec(inner)) !== null) {
348
+ const key = normalizeKey((am[1] || '').trim());
349
+ if (!key)
350
+ continue;
351
+ let rawVal = (am[2] || '').trim();
352
+ let v = rawVal;
353
+ if ((rawVal.startsWith('[') && rawVal.endsWith(']')) || (rawVal.startsWith('{') && rawVal.endsWith('}'))) {
354
+ try {
355
+ v = JSON.parse(rawVal);
356
+ }
357
+ catch {
358
+ v = rawVal;
359
+ }
360
+ }
361
+ else if (rawVal === 'true' || rawVal === 'false') {
362
+ v = rawVal === 'true';
363
+ }
364
+ args[key] = v;
365
+ }
366
+ const filtered = filterArgsForTool(lname, args);
367
+ let argsStr = '{}';
368
+ try {
369
+ argsStr = JSON.stringify(filtered);
370
+ }
371
+ catch {
372
+ argsStr = '{}';
373
+ }
374
+ out.push({
375
+ id: `call_${Math.random().toString(36).slice(2, 10)}`,
376
+ name: lname,
377
+ args: argsStr
378
+ });
379
+ }
380
+ return out.length ? out : null;
381
+ }
382
+ catch {
383
+ return null;
384
+ }
385
+ }
308
386
  export function normalizeAssistantTextToToolCalls(message) {
309
387
  if (!enabled())
310
388
  return message;
@@ -317,10 +395,11 @@ export function normalizeAssistantTextToToolCalls(message) {
317
395
  const text = typeof content === 'string' ? content : null;
318
396
  if (!text)
319
397
  return message;
320
- // Order: xml-like tool_call → apply_patch → execute blocks
398
+ // Order: xml-like <tool_call> → apply_patch → execute blocks → 简单 XML 工具(list_directory 等)
321
399
  const calls = (extractXMLToolCallsFromText(text) ||
322
400
  extractApplyPatchCallsFromText(text) ||
323
- extractExecuteBlocksFromText(text));
401
+ extractExecuteBlocksFromText(text) ||
402
+ extractSimpleXmlToolsFromText(text));
324
403
  if (calls && calls.length) {
325
404
  const toolCalls = calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: c.args } }));
326
405
  const copy = { ...message };
@@ -1,4 +1,4 @@
1
- import { ToolFilterHints } from '../../filters/index.js';
1
+ import type { ToolFilterHints } from '../../filters/index.js';
2
2
  interface RequestFilterOptions {
3
3
  entryEndpoint?: string;
4
4
  requestId?: string;