@jsonstudio/llms 0.6.567 → 0.6.568

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 (55) 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 -1
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +25 -13
  8. package/dist/conversion/hub/process/chat-process.js +65 -11
  9. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +16 -3
  10. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  11. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  12. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  13. package/dist/conversion/shared/args-mapping.js +11 -3
  14. package/dist/conversion/shared/responses-output-builder.js +42 -21
  15. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  16. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  17. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  18. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  19. package/dist/conversion/shared/tool-harvester.js +43 -12
  20. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  21. package/dist/conversion/shared/tool-mapping.js +33 -19
  22. package/dist/filters/index.d.ts +1 -0
  23. package/dist/filters/index.js +1 -0
  24. package/dist/filters/special/request-tools-normalize.js +14 -4
  25. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  26. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  27. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  28. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  29. package/dist/guidance/index.js +69 -42
  30. package/dist/router/virtual-router/bootstrap.js +10 -5
  31. package/dist/router/virtual-router/classifier.js +9 -4
  32. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  33. package/dist/router/virtual-router/engine-health.js +217 -4
  34. package/dist/router/virtual-router/engine-logging.d.ts +2 -1
  35. package/dist/router/virtual-router/engine-logging.js +35 -3
  36. package/dist/router/virtual-router/engine.d.ts +17 -1
  37. package/dist/router/virtual-router/engine.js +154 -6
  38. package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
  39. package/dist/router/virtual-router/routing-instructions.js +19 -1
  40. package/dist/router/virtual-router/tool-signals.js +57 -11
  41. package/dist/router/virtual-router/types.d.ts +30 -0
  42. package/dist/router/virtual-router/types.js +1 -1
  43. package/dist/servertool/engine.js +3 -0
  44. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  45. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  46. package/dist/servertool/handlers/stop-message-auto.js +61 -4
  47. package/dist/servertool/server-side-tools.d.ts +1 -0
  48. package/dist/servertool/server-side-tools.js +27 -0
  49. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  50. package/dist/tools/apply-patch-structured.d.ts +20 -0
  51. package/dist/tools/apply-patch-structured.js +239 -0
  52. package/dist/tools/tool-description-utils.d.ts +5 -0
  53. package/dist/tools/tool-description-utils.js +50 -0
  54. package/dist/tools/tool-registry.js +11 -193
  55. package/package.json +2 -2
@@ -153,29 +153,50 @@ export function buildResponsesOutputFromChat(options) {
153
153
  };
154
154
  }
155
155
  function normalizeUsage(usageRaw) {
156
- if (usageRaw && typeof usageRaw === 'object') {
157
- const usage = { ...usageRaw };
158
- if (usage.input_tokens != null && usage.prompt_tokens == null) {
159
- usage.prompt_tokens = usage.input_tokens;
160
- }
161
- if (usage.output_tokens != null && usage.completion_tokens == null) {
162
- usage.completion_tokens = usage.output_tokens;
163
- }
164
- if (usage.prompt_tokens != null && usage.completion_tokens != null && usage.total_tokens == null) {
165
- const total = Number(usage.prompt_tokens) + Number(usage.completion_tokens);
166
- if (!Number.isNaN(total))
167
- usage.total_tokens = total;
168
- }
169
- try {
170
- delete usage.input_tokens;
171
- delete usage.output_tokens;
172
- }
173
- catch {
174
- /* ignore */
156
+ if (!usageRaw || typeof usageRaw !== 'object') {
157
+ return usageRaw;
158
+ }
159
+ const usage = { ...usageRaw };
160
+ // 统一 Responses 与 Chat 两种 usage 形态:
161
+ // - Responses: input_tokens / output_tokens / total_tokens
162
+ // - Chat: prompt_tokens / completion_tokens / total_tokens
163
+ const inputTokens = typeof usage.input_tokens === 'number'
164
+ ? usage.input_tokens
165
+ : typeof usage.prompt_tokens === 'number'
166
+ ? usage.prompt_tokens
167
+ : undefined;
168
+ const outputTokens = typeof usage.output_tokens === 'number'
169
+ ? usage.output_tokens
170
+ : typeof usage.completion_tokens === 'number'
171
+ ? usage.completion_tokens
172
+ : undefined;
173
+ let totalTokens = typeof usage.total_tokens === 'number'
174
+ ? usage.total_tokens
175
+ : undefined;
176
+ if (totalTokens === undefined && inputTokens !== undefined && outputTokens !== undefined) {
177
+ const total = Number(inputTokens) + Number(outputTokens);
178
+ if (!Number.isNaN(total)) {
179
+ totalTokens = total;
175
180
  }
176
- return usage;
177
181
  }
178
- return usageRaw;
182
+ // Responses 规范字段:input_tokens / output_tokens / total_tokens
183
+ if (inputTokens !== undefined) {
184
+ usage.input_tokens = inputTokens;
185
+ }
186
+ if (outputTokens !== undefined) {
187
+ usage.output_tokens = outputTokens;
188
+ }
189
+ if (totalTokens !== undefined) {
190
+ usage.total_tokens = totalTokens;
191
+ }
192
+ // 为了兼容内部统计逻辑,保留 prompt_tokens / completion_tokens 映射(如果原本没有)
193
+ if (usage.prompt_tokens == null && inputTokens !== undefined) {
194
+ usage.prompt_tokens = inputTokens;
195
+ }
196
+ if (usage.completion_tokens == null && outputTokens !== undefined) {
197
+ usage.completion_tokens = outputTokens;
198
+ }
199
+ return usage;
179
200
  }
180
201
  function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, baseCount, offset) {
181
202
  try {
@@ -0,0 +1,25 @@
1
+ export interface StreamingToolCall {
2
+ id?: string;
3
+ type: 'function';
4
+ function: {
5
+ name?: string;
6
+ arguments?: string;
7
+ };
8
+ }
9
+ export interface StreamingToolExtractorOptions {
10
+ idPrefix?: string;
11
+ }
12
+ export declare class StreamingTextToolExtractor {
13
+ private opts;
14
+ private buffer;
15
+ private idCounter;
16
+ constructor(opts?: StreamingToolExtractorOptions);
17
+ reset(): void;
18
+ feedText(text: string): StreamingToolCall[];
19
+ private genId;
20
+ private toToolCall;
21
+ private tryExtractStructuredBlocks;
22
+ private tryExtractFunctionExecuteBlocks;
23
+ private splitCommand;
24
+ }
25
+ export declare function createStreamingToolExtractor(opts?: StreamingToolExtractorOptions): StreamingTextToolExtractor;
@@ -1,6 +1,7 @@
1
1
  // Streaming textual tool intent extractor (对齐)
2
- // Detects <function=execute> blocks and unified diff patches
2
+ // Detects <function=execute> blocks and structured apply_patch payloads
3
3
  // and converts them into OpenAI tool_calls incrementally.
4
+ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
4
5
  function isObject(v) {
5
6
  return !!v && typeof v === 'object' && !Array.isArray(v);
6
7
  }
@@ -8,22 +9,20 @@ export class StreamingTextToolExtractor {
8
9
  opts;
9
10
  buffer = '';
10
11
  idCounter = 0;
11
- pendingPatch = { active: false, lines: [] };
12
12
  constructor(opts = {}) {
13
13
  this.opts = opts;
14
14
  }
15
15
  reset() {
16
16
  this.buffer = '';
17
17
  this.idCounter = 0;
18
- this.pendingPatch = { active: false, lines: [] };
19
18
  }
20
19
  feedText(text) {
21
20
  const out = [];
22
21
  if (typeof text !== 'string' || !text)
23
22
  return out;
24
23
  this.buffer += text;
25
- // 1) Unified diff apply_patch block detection (*** Begin Patch ... *** End Patch)
26
- out.push(...this.tryExtractUnifiedDiffBlocks());
24
+ // 1) Structured apply_patch block detection (```json ... ```)
25
+ out.push(...this.tryExtractStructuredBlocks());
27
26
  // 2) <function=execute> compact blocks detection
28
27
  out.push(...this.tryExtractFunctionExecuteBlocks());
29
28
  return out;
@@ -42,42 +41,36 @@ export class StreamingTextToolExtractor {
42
41
  }
43
42
  return { id: this.genId(), type: 'function', function: { name, arguments: argStr } };
44
43
  }
45
- tryExtractUnifiedDiffBlocks() {
44
+ tryExtractStructuredBlocks() {
46
45
  const out = [];
47
- // Stream-friendly: if we see Begin Patch, start accumulating until End Patch
48
- const beginIdx = this.buffer.indexOf('*** Begin Patch');
49
- if (beginIdx >= 0) {
50
- // Start patch if not already active
51
- if (!this.pendingPatch.active) {
52
- this.pendingPatch.active = true;
53
- this.pendingPatch.lines = [];
54
- this.pendingPatch.lines.push(this.buffer.slice(beginIdx));
55
- // trim buffer before begin to reduce size
56
- this.buffer = this.buffer.slice(beginIdx);
57
- }
58
- else {
59
- // append new data
60
- this.pendingPatch.lines.push(this.buffer);
61
- }
62
- }
63
- if (this.pendingPatch.active) {
64
- const joined = this.pendingPatch.lines.join('');
65
- const endIdx = joined.indexOf('*** End Patch');
66
- if (endIdx >= 0) {
67
- const patchEnd = endIdx + '*** End Patch'.length;
68
- const patchText = joined.slice(0, patchEnd);
69
- out.push(this.toToolCall('apply_patch', { patch: patchText }));
70
- // consume used part
71
- const remainder = joined.slice(patchEnd);
72
- this.pendingPatch = { active: false, lines: [] };
73
- this.buffer = remainder;
74
- }
75
- else {
76
- // keep accumulating, but limit memory
77
- if (joined.length > 200000) {
78
- this.pendingPatch.lines = [joined.slice(-100000)];
46
+ let searchIdx = 0;
47
+ while (searchIdx < this.buffer.length) {
48
+ const startIdx = this.buffer.indexOf('```', searchIdx);
49
+ if (startIdx < 0)
50
+ break;
51
+ const headerEnd = this.buffer.indexOf('\n', startIdx + 3);
52
+ if (headerEnd < 0)
53
+ break;
54
+ const language = this.buffer.slice(startIdx + 3, headerEnd).trim().toLowerCase();
55
+ const endIdx = this.buffer.indexOf('```', headerEnd + 1);
56
+ if (endIdx < 0)
57
+ break;
58
+ const body = this.buffer.slice(headerEnd + 1, endIdx);
59
+ if (!language || language === 'json' || language === 'apply_patch' || language === 'toon') {
60
+ try {
61
+ const parsed = JSON.parse(body);
62
+ if (isStructuredApplyPatchPayload(parsed)) {
63
+ out.push(this.toToolCall('apply_patch', parsed));
64
+ this.buffer = this.buffer.slice(0, startIdx) + this.buffer.slice(endIdx + 3);
65
+ searchIdx = 0;
66
+ continue;
67
+ }
68
+ }
69
+ catch {
70
+ /* ignore parse errors */
79
71
  }
80
72
  }
73
+ searchIdx = endIdx + 3;
81
74
  }
82
75
  return out;
83
76
  }
@@ -1,6 +1,7 @@
1
1
  // Normalize textual markup into OpenAI tool_calls shape.
2
2
  // Gated by RCC_TEXT_MARKUP_COMPAT=1 to avoid overreach.
3
3
  import { isImagePath } from './media.js';
4
+ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
4
5
  // Strict allowlist for tool names and their argument keys to avoid picking up
5
6
  // stray markup or free-form text as JSON keys (reduces false positives).
6
7
  const KNOWN_TOOLS = new Set([
@@ -14,7 +15,7 @@ const KNOWN_TOOLS = new Set([
14
15
  ]);
15
16
  const ALLOWED_KEYS = {
16
17
  shell: new Set(['command', 'justification', 'timeout_ms', 'with_escalated_permissions', 'workdir']),
17
- apply_patch: new Set(['patch']),
18
+ apply_patch: new Set(['file', 'instructions', 'changes']),
18
19
  update_plan: new Set(['explanation', 'plan']),
19
20
  view_image: new Set(['path']),
20
21
  list_mcp_resources: new Set(['server', 'cursor', 'filter', 'root']),
@@ -70,40 +71,54 @@ function enabled() {
70
71
  }
71
72
  }
72
73
  // 已移除所有 rcc.tool.v1 相关处理:不再识别或剥离 rcc 封装
73
- export function extractApplyPatchCallsFromText(text) {
74
+ function extractStructuredApplyPatchPayloads(text) {
75
+ const payloads = [];
74
76
  try {
75
- if (typeof text !== 'string' || !text)
76
- return null;
77
- const out = [];
78
- const candidates = [];
79
- const fenceRe = /```(?:patch)?\s*([\s\S]*?)\s*```/gi;
77
+ const fenceRe = /```(?:json|apply_patch|toon)?\s*([\s\S]*?)\s*```/gi;
80
78
  let fm;
81
79
  while ((fm = fenceRe.exec(text)) !== null) {
82
80
  const body = fm[1] || '';
83
- if (/\*\*\*\s+Begin Patch[\s\S]*?\*\*\*\s+End Patch/.test(body))
84
- candidates.push(body);
85
- }
86
- if (/\*\*\*\s+Begin Patch[\s\S]*?\*\*\*\s+End Patch/.test(text))
87
- candidates.push(text);
88
- const genId = () => `call_${Math.random().toString(36).slice(2, 10)}`;
89
- for (const src of candidates) {
90
- const pg = /\*\*\*\s+Begin Patch[\s\S]*?\*\*\*\s+End Patch/gm;
91
- let pm;
92
- while ((pm = pg.exec(src)) !== null) {
93
- const patch = pm[0];
94
- if (!patch || patch.length < 32)
95
- continue;
96
- let argsStr = '{}';
97
- try {
98
- argsStr = JSON.stringify({ patch });
81
+ try {
82
+ const parsed = JSON.parse(body);
83
+ if (isStructuredApplyPatchPayload(parsed)) {
84
+ payloads.push(parsed);
99
85
  }
100
- catch {
101
- argsStr = '{"patch":""}';
86
+ }
87
+ catch { /* ignore invalid JSON */ }
88
+ }
89
+ if (!payloads.length && typeof text === 'string' && text.includes('"changes"')) {
90
+ try {
91
+ const parsed = JSON.parse(text);
92
+ if (isStructuredApplyPatchPayload(parsed)) {
93
+ payloads.push(parsed);
102
94
  }
103
- out.push({ id: genId(), name: 'apply_patch', args: argsStr });
104
95
  }
96
+ catch { /* ignore */ }
105
97
  }
106
- return out.length ? out : null;
98
+ }
99
+ catch { /* ignore */ }
100
+ return payloads;
101
+ }
102
+ export function extractApplyPatchCallsFromText(text) {
103
+ try {
104
+ if (typeof text !== 'string' || !text)
105
+ return null;
106
+ const payloads = extractStructuredApplyPatchPayloads(text);
107
+ if (!payloads.length)
108
+ return null;
109
+ const out = [];
110
+ const genId = () => `call_${Math.random().toString(36).slice(2, 10)}`;
111
+ for (const payload of payloads) {
112
+ let argsStr = '{}';
113
+ try {
114
+ argsStr = JSON.stringify(payload);
115
+ }
116
+ catch {
117
+ argsStr = '{"changes":[]}';
118
+ }
119
+ out.push({ id: genId(), name: 'apply_patch', args: argsStr });
120
+ }
121
+ return out;
107
122
  }
108
123
  catch {
109
124
  return null;
@@ -212,8 +212,9 @@ export async function runChatResponseToolFilters(chatJson, options = {}) {
212
212
  const { ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } = await import('../../filters/index.js');
213
213
  register(new ResponseToolTextCanonicalizeFilter());
214
214
  try {
215
- const { ResponseToolArgumentsToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
215
+ const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
216
216
  register(new ResponseToolArgumentsToonDecodeFilter());
217
+ register(new ResponseApplyPatchToonDecodeFilter());
217
218
  try {
218
219
  register(new ResponseToolArgumentsSchemaConvergeFilter());
219
220
  }
@@ -1,8 +1,9 @@
1
1
  // Unified tool harvesting (对齐, single entry)
2
2
  // - First-time harvesting only (no late-stage repair)
3
- // - Handles textual markers (<function=execute>, unified diff)
3
+ // - Handles textual markers (<function=execute>, structured apply_patch payloads)
4
4
  // - Handles structural shapes (function_call legacy, tool_calls)
5
5
  // - Normalizes arguments (single JSON string), sets finish_reason when applicable
6
+ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
6
7
  function isObject(v) {
7
8
  return !!v && typeof v === 'object' && !Array.isArray(v);
8
9
  }
@@ -48,17 +49,19 @@ function extractFromTextual(content, ctx) {
48
49
  const events = [];
49
50
  if (typeof content !== 'string' || !content)
50
51
  return events;
51
- // 1) unified diff
52
- const beginIdx = content.indexOf('*** Begin Patch');
53
- const endIdx = content.indexOf('*** End Patch');
54
- if (beginIdx >= 0 && endIdx > beginIdx) {
55
- const patchText = content.slice(beginIdx, endIdx + '*** End Patch'.length);
56
- const id = genId(ctx, 0);
57
- const argStr = toJsonString({ patch: patchText });
58
- events.push({ tool_calls: [{ index: 0, id, type: 'function', function: { name: 'apply_patch' } }] });
59
- const parts = chunkString(argStr, Math.max(32, Math.min(1024, ctx?.chunkSize || 256)));
60
- for (const d of parts) {
61
- events.push({ tool_calls: [{ index: 0, id, type: 'function', function: { arguments: d } }] });
52
+ // 1) structured apply_patch payload
53
+ const structuredPayloads = extractStructuredApplyPatchPayloads(content);
54
+ if (structuredPayloads.length) {
55
+ let idx = 0;
56
+ for (const payload of structuredPayloads) {
57
+ const id = genId(ctx, idx);
58
+ const argStr = toJsonString(payload);
59
+ events.push({ tool_calls: [{ index: idx, id, type: 'function', function: { name: 'apply_patch' } }] });
60
+ const parts = chunkString(argStr, Math.max(32, Math.min(1024, ctx?.chunkSize || 256)));
61
+ for (const d of parts) {
62
+ events.push({ tool_calls: [{ index: idx, id, type: 'function', function: { arguments: d } }] });
63
+ }
64
+ idx += 1;
62
65
  }
63
66
  return events;
64
67
  }
@@ -157,6 +160,34 @@ function extractFromTextual(content, ctx) {
157
160
  catch { /* ignore textual tool_call parse errors */ }
158
161
  return events;
159
162
  }
163
+ function extractStructuredApplyPatchPayloads(text) {
164
+ const payloads = [];
165
+ try {
166
+ const fenceRe = /```(?:json|apply_patch|toon)?\s*([\s\S]*?)\s*```/gi;
167
+ let fm;
168
+ while ((fm = fenceRe.exec(text)) !== null) {
169
+ const body = fm[1] || '';
170
+ try {
171
+ const parsed = JSON.parse(body);
172
+ if (isStructuredApplyPatchPayload(parsed)) {
173
+ payloads.push(parsed);
174
+ }
175
+ }
176
+ catch { /* ignore invalid JSON */ }
177
+ }
178
+ if (!payloads.length && typeof text === 'string' && text.includes('"changes"')) {
179
+ try {
180
+ const parsed = JSON.parse(text);
181
+ if (isStructuredApplyPatchPayload(parsed)) {
182
+ payloads.push(parsed);
183
+ }
184
+ }
185
+ catch { /* ignore */ }
186
+ }
187
+ }
188
+ catch { /* ignore */ }
189
+ return payloads;
190
+ }
160
191
  function splitCommand(s) {
161
192
  try {
162
193
  const out = [];
@@ -13,6 +13,7 @@ export declare function stringifyArgs(args: unknown): string;
13
13
  export interface BridgeToolMapOptions {
14
14
  sanitizeName?: (raw: unknown) => string | undefined;
15
15
  }
16
+ export declare function ensureApplyPatchSchema(seed?: Record<string, unknown>): Record<string, unknown>;
16
17
  export declare function bridgeToolToChatDefinition(rawTool: BridgeToolDefinition | Record<string, unknown> | null | undefined, options?: BridgeToolMapOptions): ChatToolDefinition | null;
17
18
  export declare function mapBridgeToolsToChat(rawTools: unknown, options?: BridgeToolMapOptions): ChatToolDefinition[] | undefined;
18
19
  export declare function chatToolToBridgeDefinition(rawTool: ChatToolDefinition | Record<string, unknown> | null | undefined, options?: BridgeToolMapOptions): BridgeToolDefinition | null;
@@ -25,30 +25,44 @@ function asSchema(value) {
25
25
  }
26
26
  return undefined;
27
27
  }
28
- function ensureApplyPatchSchema(seed) {
28
+ export function ensureApplyPatchSchema(seed) {
29
29
  const schema = seed ? { ...seed } : {};
30
30
  schema.type = typeof schema.type === 'string' ? schema.type : 'object';
31
31
  const properties = isPlainObject(schema.properties) ? { ...schema.properties } : {};
32
- delete properties.input;
33
- delete properties.patch;
34
- properties.patch = {
35
- type: 'string',
36
- description: 'Unified diff patch text. The first line MUST be exactly "*** Begin Patch", followed by one or more file sections starting with ' +
37
- '"*** Add File: {path}", "*** Update File: {path}", or "*** Delete File: {path}", using hunks with lines prefixed by " ", "+", or "-". ' +
38
- 'The last non-empty line MUST be exactly "*** End Patch". Git-style headers like "--- a/file" / "+++ b/file" and merge markers ' +
39
- '"<<<<<<<", "=======", ">>>>>>>" are NOT allowed.'
32
+ properties.file = { type: 'string', description: 'Optional default file path for all changes' };
33
+ properties.instructions = { type: 'string', description: 'Optional summary of the edit' };
34
+ properties.changes = {
35
+ type: 'array',
36
+ minItems: 1,
37
+ items: {
38
+ type: 'object',
39
+ additionalProperties: false,
40
+ required: ['kind'],
41
+ properties: {
42
+ file: { type: 'string', description: 'Relative path for this change' },
43
+ kind: {
44
+ type: 'string',
45
+ description: 'insert_after | insert_before | replace | delete | create_file | delete_file'
46
+ },
47
+ anchor: { type: 'string', description: 'Context snippet for insert operations' },
48
+ target: { type: 'string', description: 'Snippet to replace/delete' },
49
+ lines: {
50
+ description: 'New content for insert/replace/create operations',
51
+ oneOf: [
52
+ { type: 'string' },
53
+ { type: 'array', items: { type: 'string' } }
54
+ ]
55
+ },
56
+ use_anchor_indent: { type: 'boolean', description: 'Reuse indentation from the anchor snippet' }
57
+ }
58
+ }
40
59
  };
41
- if (!isPlainObject(properties.paths)) {
42
- properties.paths = {
43
- type: 'array',
44
- description: 'Optional explicit list of relative file paths that the patch touches. Each entry must be a relative workspace path (packages/foo/file.ts).',
45
- items: { type: 'string' }
46
- };
47
- }
48
60
  schema.properties = properties;
49
- const requiredList = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === 'string') : [];
50
- if (!requiredList.includes('patch')) {
51
- requiredList.push('patch');
61
+ const requiredList = Array.isArray(schema.required)
62
+ ? schema.required.filter((entry) => typeof entry === 'string')
63
+ : [];
64
+ if (!requiredList.includes('changes')) {
65
+ requiredList.push('changes');
52
66
  }
53
67
  schema.required = requiredList;
54
68
  if (typeof schema.additionalProperties !== 'boolean') {
@@ -12,6 +12,7 @@ export * from './special/response-tool-arguments-stringify.js';
12
12
  export * from './special/response-finish-invariants.js';
13
13
  export * from './special/request-tools-normalize.js';
14
14
  export * from './special/response-tool-arguments-toon-decode.js';
15
+ export * from './special/response-apply-patch-toon-decode.js';
15
16
  export * from './special/response-tool-arguments-blacklist.js';
16
17
  export * from './special/response-tool-arguments-schema-converge.js';
17
18
  export * from './special/response-tool-arguments-whitelist.js';
@@ -15,6 +15,7 @@ export * from './special/response-finish-invariants.js';
15
15
  // TOON support (default ON via RCC_TOON_ENABLE unless explicitly disabled)
16
16
  export * from './special/request-tools-normalize.js';
17
17
  export * from './special/response-tool-arguments-toon-decode.js';
18
+ export * from './special/response-apply-patch-toon-decode.js';
18
19
  // Arguments policy filters (synced)
19
20
  export * from './special/response-tool-arguments-blacklist.js';
20
21
  export * from './special/response-tool-arguments-schema-converge.js';
@@ -1,3 +1,4 @@
1
+ import { appendApplyPatchReminder, buildShellDescription, hasApplyPatchToolDeclared, isShellToolName, normalizeToolName } from '../../tools/tool-description-utils.js';
1
2
  function isObject(v) {
2
3
  return !!v && typeof v === 'object' && !Array.isArray(v);
3
4
  }
@@ -31,6 +32,7 @@ export class RequestOpenAIToolsNormalizeFilter {
31
32
  const toonEnv = String(process?.env?.RCC_TOON_ENABLE || process?.env?.ROUTECODEX_TOON_ENABLE || '').toLowerCase();
32
33
  const toonEnabled = toonEnv ? !(toonEnv === '0' || toonEnv === 'false' || toonEnv === 'off') : true; // default ON
33
34
  const tools = Array.isArray(out.tools) ? out.tools : [];
35
+ const hasApplyPatchTool = hasApplyPatchToolDeclared(tools);
34
36
  if (!tools.length) {
35
37
  // No tools present: drop tool_choice to avoid provider-side validation errors
36
38
  try {
@@ -75,19 +77,25 @@ export class RequestOpenAIToolsNormalizeFilter {
75
77
  catch { /* ignore */ }
76
78
  // Switch schema for specific built-in tools at unified shaping point
77
79
  try {
78
- const name = String(dst.function.name || '').toLowerCase();
79
- if (name === 'shell') {
80
+ const rawToolName = String(dst.function.name || '');
81
+ const isShell = isShellToolName(rawToolName);
82
+ const isApplyPatch = normalizeToolName(rawToolName) === 'apply_patch';
83
+ if (isShell) {
80
84
  const wantToon = toonEnabled && !looksHeredoc;
81
85
  if (wantToon) {
82
86
  dst.function.parameters = {
83
87
  type: 'object',
84
88
  properties: {
85
- toon: { type: 'string', description: 'TOON-encoded arguments (multi-line key: value). Example: command: bash -lc "echo ok"\\nworkdir: .' }
89
+ toon: {
90
+ type: 'string',
91
+ description: 'TOON-encoded arguments (multi-line key: value). Example: command: bash -lc "echo ok"\\nworkdir: .'
92
+ }
86
93
  },
87
94
  required: ['toon'],
88
95
  additionalProperties: false
89
96
  };
90
- dst.function.description = 'Use TOON: put arguments into arguments.toon as multi-line key: value (e.g., command / workdir).';
97
+ const toonDescription = 'Use TOON: put arguments into arguments.toon as multi-line key: value (e.g., command / workdir).';
98
+ dst.function.description = appendApplyPatchReminder(toonDescription, hasApplyPatchTool);
91
99
  }
92
100
  else {
93
101
  dst.function.parameters = {
@@ -99,6 +107,8 @@ export class RequestOpenAIToolsNormalizeFilter {
99
107
  required: ['command'],
100
108
  additionalProperties: false
101
109
  };
110
+ const label = rawToolName && rawToolName.trim().length > 0 ? rawToolName.trim() : 'shell';
111
+ dst.function.description = buildShellDescription(label, hasApplyPatchTool);
102
112
  }
103
113
  }
104
114
  }
@@ -0,0 +1,23 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * Response-side apply_patch arguments 规范化(TOON + 结构化 JSON → 统一 diff 文本)。
4
+ *
5
+ * 目标:
6
+ * - 对上游模型:仍然可以使用结构化 JSON(changes 数组)或 toon 字段输出补丁;
7
+ * - 对下游客户端(Codex CLI 等):始终看到 { input, patch } 形式的统一 diff 文本,
8
+ * 与本地 apply_patch 工具的旧语义完全一致(对客户端透明)。
9
+ *
10
+ * 支持两种入参形态(arguments 反序列化后):
11
+ * 1) { toon: "<*** Begin Patch ... *** End Patch>" }
12
+ * 2) StructuredApplyPatchPayload(含 changes 数组等字段)
13
+ *
14
+ * 输出:
15
+ * - arguments 统一替换为 JSON 字符串:{ input: patchText, patch: patchText }。
16
+ *
17
+ * Stage: response_pre(在 arguments stringify 之前运行)。
18
+ */
19
+ export declare class ResponseApplyPatchToonDecodeFilter implements Filter<JsonObject> {
20
+ readonly name = "response_apply_patch_toon_decode";
21
+ readonly stage: FilterContext['stage'];
22
+ apply(input: JsonObject): FilterResult<JsonObject>;
23
+ }