@jsonstudio/llms 0.6.567 → 0.6.586

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  5. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -2
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +72 -81
  8. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
  9. package/dist/conversion/hub/process/chat-process.js +68 -24
  10. package/dist/conversion/hub/response/provider-response.js +0 -8
  11. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +22 -3
  12. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +267 -14
  13. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  14. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
  15. package/dist/conversion/responses/responses-openai-bridge.js +1 -13
  16. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  17. package/dist/conversion/shared/args-mapping.js +11 -3
  18. package/dist/conversion/shared/responses-output-builder.js +42 -21
  19. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  20. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  21. package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
  22. package/dist/conversion/shared/text-markup-normalizer.js +118 -31
  23. package/dist/conversion/shared/tool-filter-pipeline.js +56 -30
  24. package/dist/conversion/shared/tool-harvester.js +43 -12
  25. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  26. package/dist/conversion/shared/tool-mapping.js +33 -19
  27. package/dist/filters/index.d.ts +1 -0
  28. package/dist/filters/index.js +1 -0
  29. package/dist/filters/special/request-tools-normalize.js +14 -4
  30. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  31. package/dist/filters/special/response-apply-patch-toon-decode.js +117 -0
  32. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  33. package/dist/filters/special/response-tool-arguments-toon-decode.js +154 -26
  34. package/dist/guidance/index.js +71 -42
  35. package/dist/router/virtual-router/bootstrap.js +10 -5
  36. package/dist/router/virtual-router/classifier.js +16 -7
  37. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  38. package/dist/router/virtual-router/engine-health.js +217 -4
  39. package/dist/router/virtual-router/engine-logging.d.ts +2 -1
  40. package/dist/router/virtual-router/engine-logging.js +35 -3
  41. package/dist/router/virtual-router/engine.d.ts +17 -1
  42. package/dist/router/virtual-router/engine.js +184 -6
  43. package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-instructions.js +19 -1
  45. package/dist/router/virtual-router/tool-signals.d.ts +2 -1
  46. package/dist/router/virtual-router/tool-signals.js +324 -119
  47. package/dist/router/virtual-router/types.d.ts +31 -1
  48. package/dist/router/virtual-router/types.js +2 -2
  49. package/dist/servertool/engine.js +3 -0
  50. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  51. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  52. package/dist/servertool/handlers/stop-message-auto.js +61 -4
  53. package/dist/servertool/server-side-tools.d.ts +1 -0
  54. package/dist/servertool/server-side-tools.js +27 -0
  55. package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
  56. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +23 -3
  57. package/dist/tools/apply-patch-structured.d.ts +20 -0
  58. package/dist/tools/apply-patch-structured.js +240 -0
  59. package/dist/tools/tool-description-utils.d.ts +5 -0
  60. package/dist/tools/tool-description-utils.js +50 -0
  61. package/dist/tools/tool-registry.js +11 -193
  62. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,117 @@
1
+ import { buildStructuredPatch, isStructuredApplyPatchPayload, StructuredApplyPatchError } from '../../tools/apply-patch-structured.js';
2
+ function envEnabled() {
3
+ const v = String(process?.env?.RCC_TOON_ENABLE ||
4
+ process?.env?.ROUTECODEX_TOON_ENABLE ||
5
+ '').toLowerCase();
6
+ if (!v)
7
+ return true;
8
+ return !(v === '0' || v === 'false' || v === 'off');
9
+ }
10
+ function isObject(v) {
11
+ return !!v && typeof v === 'object' && !Array.isArray(v);
12
+ }
13
+ /**
14
+ * Response-side apply_patch arguments 规范化(TOON + 结构化 JSON → 统一 diff 文本)。
15
+ *
16
+ * 目标:
17
+ * - 对上游模型:仍然可以使用结构化 JSON(changes 数组)或 toon 字段输出补丁;
18
+ * - 对下游客户端(Codex CLI 等):始终看到 { input, patch } 形式的统一 diff 文本,
19
+ * 与本地 apply_patch 工具的旧语义完全一致(对客户端透明)。
20
+ *
21
+ * 支持两种入参形态(arguments 反序列化后):
22
+ * 1) { toon: "<*** Begin Patch ... *** End Patch>" }
23
+ * 2) StructuredApplyPatchPayload(含 changes 数组等字段)
24
+ *
25
+ * 输出:
26
+ * - arguments 统一替换为 JSON 字符串:{ input: patchText, patch: patchText }。
27
+ *
28
+ * Stage: response_pre(在 arguments stringify 之前运行)。
29
+ */
30
+ export class ResponseApplyPatchToonDecodeFilter {
31
+ name = 'response_apply_patch_toon_decode';
32
+ stage = 'response_pre';
33
+ apply(input) {
34
+ if (!envEnabled())
35
+ return { ok: true, data: input };
36
+ try {
37
+ const out = JSON.parse(JSON.stringify(input || {}));
38
+ const choices = Array.isArray(out.choices) ? out.choices : [];
39
+ for (const ch of choices) {
40
+ const msg = ch && ch.message ? ch.message : undefined;
41
+ const tcs = msg && Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
42
+ for (const tc of tcs) {
43
+ try {
44
+ const fn = tc && tc.function ? tc.function : undefined;
45
+ if (!fn || typeof fn !== 'object')
46
+ continue;
47
+ const nameRaw = fn.name;
48
+ if (typeof nameRaw !== 'string' || nameRaw.trim().toLowerCase() !== 'apply_patch') {
49
+ continue;
50
+ }
51
+ const argIn = fn.arguments;
52
+ let parsed;
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
+ }
63
+ }
64
+ else if (isObject(argIn)) {
65
+ parsed = argIn;
66
+ }
67
+ else {
68
+ continue;
69
+ }
70
+ if (!isObject(parsed))
71
+ continue;
72
+ // 优先处理 toon: "<patch text>" 形态(兼容旧 TOON 协议)
73
+ const toon = parsed.toon;
74
+ let patchText;
75
+ if (typeof toon === 'string' && toon.trim()) {
76
+ if (toon.includes('*** Begin Patch') && toon.includes('*** End Patch')) {
77
+ patchText = toon;
78
+ }
79
+ }
80
+ // 否则尝试结构化 JSON(changes 数组 → 统一 diff)
81
+ if (!patchText && isStructuredApplyPatchPayload(parsed)) {
82
+ try {
83
+ patchText = buildStructuredPatch(parsed);
84
+ }
85
+ catch (error) {
86
+ if (error instanceof StructuredApplyPatchError) {
87
+ // 结构化 payload 无法构建补丁时,保留原始 arguments,
88
+ // 由下游工具或客户端根据自身策略报错;这里不吞掉错误。
89
+ continue;
90
+ }
91
+ continue;
92
+ }
93
+ }
94
+ if (!patchText) {
95
+ continue;
96
+ }
97
+ const normalized = { input: patchText, patch: patchText };
98
+ try {
99
+ fn.arguments = JSON.stringify(normalized);
100
+ }
101
+ catch {
102
+ // stringify 失败时保留原始 arguments
103
+ }
104
+ }
105
+ catch {
106
+ // 针对单个 tool_call 的 best-effort,不影响其他工具
107
+ }
108
+ }
109
+ }
110
+ out.choices = choices;
111
+ return { ok: true, data: out };
112
+ }
113
+ catch {
114
+ return { ok: true, data: input };
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * Decode arguments.toon to standard JSON ({command, workdir?}) and map tool name 'shell_toon' → 'shell'.
4
+ * Stage: response_pre (before arguments stringify and invariants).
5
+ */
6
+ export declare class ResponseToolArgumentsToonDecodeFilter implements Filter<JsonObject> {
7
+ readonly name = "response_tool_arguments_toon_decode";
8
+ readonly stage: FilterContext['stage'];
9
+ apply(input: JsonObject, ctx: FilterContext): FilterResult<JsonObject>;
10
+ }
@@ -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();
@@ -10,23 +11,67 @@ function decodeToonPairs(toon) {
10
11
  try {
11
12
  const out = {};
12
13
  const lines = String(toon).split(/\r?\n/);
14
+ let currentKey = null;
15
+ let currentVal = '';
16
+ const flush = () => {
17
+ if (currentKey) {
18
+ out[currentKey] = currentVal;
19
+ }
20
+ currentKey = null;
21
+ currentVal = '';
22
+ };
13
23
  for (const raw of lines) {
14
24
  const line = raw.trim();
15
25
  if (!line)
16
26
  continue;
17
27
  const m = line.match(/^([A-Za-z0-9_\-]+)\s*:\s*(.*)$/);
18
- if (!m)
19
- return null; // fail fast for non key: value
20
- const key = m[1];
21
- const val = m[2];
22
- out[key] = val;
28
+ if (m) {
29
+ // 新的 key: value 行,先提交上一段,再开始累积新 key 的值
30
+ flush();
31
+ currentKey = m[1];
32
+ currentVal = m[2] ?? '';
33
+ }
34
+ else {
35
+ // 非 key: value 行视为上一 key 的续行(例如多行脚本)
36
+ if (!currentKey) {
37
+ // 如果一开始就遇到无法识别的行,认为整个 TOON 不是我们支持的形态
38
+ return null;
39
+ }
40
+ currentVal += (currentVal ? '\n' : '') + raw;
41
+ }
23
42
  }
24
- return out;
43
+ flush();
44
+ return Object.keys(out).length ? out : null;
25
45
  }
26
46
  catch {
27
47
  return null;
28
48
  }
29
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
+ }
30
75
  /**
31
76
  * Decode arguments.toon to standard JSON ({command, workdir?}) and map tool name 'shell_toon' → 'shell'.
32
77
  * Stage: response_pre (before arguments stringify and invariants).
@@ -34,11 +79,12 @@ function decodeToonPairs(toon) {
34
79
  export class ResponseToolArgumentsToonDecodeFilter {
35
80
  name = 'response_tool_arguments_toon_decode';
36
81
  stage = 'response_pre';
37
- apply(input) {
82
+ apply(input, ctx) {
38
83
  if (!envEnabled())
39
84
  return { ok: true, data: input };
40
85
  try {
41
86
  const out = JSON.parse(JSON.stringify(input || {}));
87
+ const warnings = [];
42
88
  const choices = Array.isArray(out.choices) ? out.choices : [];
43
89
  for (const ch of choices) {
44
90
  const msg = ch && ch.message ? ch.message : undefined;
@@ -48,15 +94,23 @@ export class ResponseToolArgumentsToonDecodeFilter {
48
94
  const fn = tc && tc.function ? tc.function : undefined;
49
95
  if (!fn || typeof fn !== 'object')
50
96
  continue;
51
- const argStr = fn.arguments;
52
- if (typeof argStr !== 'string')
53
- continue;
54
- let parsed;
55
- try {
56
- 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
+ }
57
111
  }
58
- catch {
59
- continue;
112
+ else if (isObject(argIn)) {
113
+ parsed = argIn;
60
114
  }
61
115
  if (!isObject(parsed))
62
116
  continue;
@@ -64,20 +118,94 @@ export class ResponseToolArgumentsToonDecodeFilter {
64
118
  if (typeof toon !== 'string' || !toon.trim())
65
119
  continue;
66
120
  const kv = decodeToonPairs(toon);
67
- if (!kv)
121
+ if (!kv) {
122
+ const preview = toon.split(/\r?\n/).slice(0, 5).join('\n');
123
+ const warnMsg = `response_tool_arguments_toon_decode: failed to decode TOON arguments for tool "${fn.name ?? 'unknown'}"`;
124
+ warnings.push(warnMsg);
125
+ if (ctx?.debug?.emit) {
126
+ ctx.debug.emit('tool_toon_decode_error', {
127
+ requestId: ctx.requestId,
128
+ model: ctx.model,
129
+ endpoint: ctx.endpoint,
130
+ stage: ctx.stage,
131
+ provider: ctx.provider,
132
+ toolName: fn.name ?? 'unknown',
133
+ message: warnMsg,
134
+ toonPreview: preview
135
+ });
136
+ }
68
137
  continue; // keep original if decode fails
69
- const command = kv['command'];
70
- const workdir = kv['workdir'];
71
- if (typeof command === 'string' && command.trim()) {
72
- const merged = { command };
73
- if (typeof workdir === 'string' && workdir.trim())
74
- merged.workdir = workdir;
138
+ }
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);
203
+ }
75
204
  try {
76
205
  fn.arguments = JSON.stringify(merged);
77
206
  }
78
- catch { /* keep original */ }
79
- if (typeof fn.name === 'string' && fn.name === 'shell_toon') {
80
- fn.name = 'shell';
207
+ catch {
208
+ // keep original on stringify failure
81
209
  }
82
210
  }
83
211
  }
@@ -85,7 +213,7 @@ export class ResponseToolArgumentsToonDecodeFilter {
85
213
  }
86
214
  }
87
215
  out.choices = choices;
88
- return { ok: true, data: out };
216
+ return warnings.length ? { ok: true, data: out, warnings } : { ok: true, data: out };
89
217
  }
90
218
  catch {
91
219
  return { ok: true, data: input };
@@ -62,36 +62,48 @@ function augmentApplyPatch(fn) {
62
62
  const marker = '[Codex ApplyPatch Guidance]';
63
63
  const guidance = [
64
64
  marker,
65
- 'Edit files by applying a unified diff patch. Return ONLY the patch text with *** Begin Patch/*** End Patch blocks and file sections starting with "*** Add/Update/Delete File: {path}".',
66
- 'Populate the `paths` array with every relative file path touched by this patch (e.g., ["src/foo.ts"]); paths are preferred over any filenames embedded in the patch body.',
67
- 'Do NOT use git-style headers ("--- a/file", "+++ b/file") or merge markers ("<<<<<<<", "=======", ">>>>>>>"); hunks must use only " ", "+", "-" prefixes.',
68
- 'Paths resolve relative to the active workspace root. Use forward-slash paths (e.g., packages/foo/file.ts).',
69
- '路径一律相对于当前工作区根目录解析;请写 packages/foo/... 这样的相对路径。',
70
- 'Example:',
71
- '*** Begin Patch',
72
- '*** Update File: path/to/file.ts',
73
- '@@',
74
- '- old line',
75
- '+ new line',
76
- '*** End Patch'
65
+ 'Provide structured edits instead of raw diff text. Always send JSON arguments with a `changes` array.',
66
+ 'Each change describes one operation, e.g. `{ "file": "src/foo.ts", "kind": "insert_after", "anchor": "const foo = 1;", "lines": ["const bar = 2;"] }`.',
67
+ 'Supported kinds: insert_after, insert_before, replace, delete, create_file, delete_file.',
68
+ 'Paths must stay relative to the workspace root (no leading "/" or drive letters).',
69
+ 'Insert operations require `anchor` text; replace/delete require exact `target` snippets; `lines` omit "+/-" prefixes.'
77
70
  ].join('\n');
78
71
  const params = ensureObjectSchema(fn.parameters);
79
72
  const props = params.properties;
80
- if (isObject(props)) {
81
- delete props.input;
82
- }
83
- props.patch = { type: 'string', description: 'Unified diff patch text' };
84
- props.paths = {
73
+ props.file = { type: 'string', description: 'Optional default file path (relative) for changes' };
74
+ props.instructions = { type: 'string', description: 'Optional human-readable summary' };
75
+ props.changes = {
85
76
  type: 'array',
86
- description: 'Explicit list of relative file paths affected by this patch (e.g., ["src/foo.ts"]).',
87
- items: { type: 'string' }
77
+ minItems: 1,
78
+ items: {
79
+ type: 'object',
80
+ additionalProperties: false,
81
+ required: ['kind'],
82
+ properties: {
83
+ file: { type: 'string', description: 'Relative path for this change (overrides top-level file)' },
84
+ kind: {
85
+ type: 'string',
86
+ description: 'insert_after | insert_before | replace | delete | create_file | delete_file'
87
+ },
88
+ anchor: { type: 'string', description: 'Required for insert_before / insert_after' },
89
+ target: { type: 'string', description: 'Snippet to replace/delete' },
90
+ lines: {
91
+ description: 'New content for insert/replace/create operations',
92
+ oneOf: [
93
+ { type: 'string' },
94
+ { type: 'array', items: { type: 'string' } }
95
+ ]
96
+ },
97
+ use_anchor_indent: {
98
+ type: 'boolean',
99
+ description: 'Reapply the indentation of the anchor snippet to inserted lines'
100
+ }
101
+ }
102
+ }
88
103
  };
89
- params.properties = props;
90
- if (!Array.isArray(params.required)) {
91
- params.required = [];
92
- }
93
- if (!params.required.includes('patch')) {
94
- params.required.push('patch');
104
+ params.required = Array.isArray(params.required) ? params.required : [];
105
+ if (!params.required.includes('changes')) {
106
+ params.required.push('changes');
95
107
  }
96
108
  params.additionalProperties = false;
97
109
  fn.parameters = params;
@@ -180,28 +192,45 @@ export function augmentAnthropicTools(tools) {
180
192
  const n = name.trim();
181
193
  try {
182
194
  if (n === 'apply_patch') {
183
- const props = ensureObjectSchema(schema.properties);
184
- delete props.input;
185
- props.patch = { type: 'string', description: 'Unified diff patch text' };
186
- props.paths = {
187
- type: 'array',
188
- description: 'Explicit list of relative file paths affected by this patch (e.g., ["src/foo.ts"]).',
189
- items: { type: 'string' }
190
- };
191
- schema.properties = props;
195
+ if (!isObject(schema.properties?.changes)) {
196
+ schema.properties.file = { type: 'string', description: 'Optional default file path' };
197
+ schema.properties.instructions = { type: 'string', description: 'Optional summary' };
198
+ schema.properties.changes = {
199
+ type: 'array',
200
+ minItems: 1,
201
+ items: {
202
+ type: 'object',
203
+ additionalProperties: false,
204
+ required: ['kind'],
205
+ properties: {
206
+ file: { type: 'string' },
207
+ kind: { type: 'string' },
208
+ anchor: { type: 'string' },
209
+ target: { type: 'string' },
210
+ lines: {
211
+ oneOf: [
212
+ { type: 'string' },
213
+ { type: 'array', items: { type: 'string' } }
214
+ ]
215
+ },
216
+ use_anchor_indent: { type: 'boolean' }
217
+ }
218
+ }
219
+ };
220
+ }
192
221
  if (!Array.isArray(schema.required))
193
222
  schema.required = [];
194
- if (!schema.required.includes('patch'))
195
- schema.required.push('patch');
223
+ if (!schema.required.includes('changes'))
224
+ schema.required.push('changes');
196
225
  schema.additionalProperties = false;
197
226
  copy.input_schema = schema;
198
227
  const marker = '[Codex ApplyPatch Guidance]';
199
228
  const guidance = [
200
229
  marker,
201
- 'Use unified diff patch with *** Begin Patch/End Patch. Return only the patch body.',
202
- 'Populate the `paths` array with every relative workspace path touched by this patch.',
203
- 'All file paths must stay relative to the workspace root; never emit leading \'/\' or drive letters.',
204
- '所有文件路径都必须相对当前工作区根目录,禁止输出以 / 或盘符开头的绝对路径。'
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.',
231
+ 'Provide structured changes (insert_after / insert_before / replace / delete / create_file / delete_file) instead of raw patch text.',
232
+ 'Each change must include the target file (relative path) plus anchor/target snippets and the replacement lines.',
233
+ '所有路径必须相对工作区根目录,禁止输出以 / 或盘符开头的绝对路径。'
205
234
  ].join('\n');
206
235
  copy.description = appendOnce(desc, guidance, marker);
207
236
  }
@@ -236,8 +265,8 @@ export function buildSystemToolGuidance() {
236
265
  lines.push(bullet('function.arguments must be a single JSON string. / arguments 必须是单个 JSON 字符串。'));
237
266
  lines.push(bullet('shell: Place ALL intent into the command argv array only; do not invent extra keys. / shell 所有意图写入 command 数组,不要添加额外键名。'));
238
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。'));
239
- lines.push(bullet('apply_patch: Provide a unified diff patch with *** Begin Patch/*** End Patch only, and keep file paths relative to the workspace (no leading / or drive letters). / 仅输出统一 diff 补丁,且文件路径必须是相对路径(禁止以 / 或盘符开头)。'));
240
- lines.push(bullet('apply_patch example / 示例:\n*** Begin Patch\n*** Update File: path/to/file.ts\n@@\n- old line\n+ new line\n*** End 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 在写入前必须先通过合适的工具读取目标文件最新内容,并基于该内容生成变更。'));
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。'));
241
270
  lines.push(bullet('update_plan: Keep exactly one step in_progress; others pending/completed. / 仅一个 in_progress 步骤。'));
242
271
  lines.push(bullet('view_image: Path must be an image file (.png .jpg .jpeg .gif .webp .bmp .svg). / 仅图片路径。'));
243
272
  lines.push(bullet('Do NOT use view_image for text files (.md/.ts/.js/.json). Use shell: {"command":["cat","<path>"]}. / 文本文件请用 shell: cat。'));
@@ -552,9 +552,10 @@ function validateWebSearchRouting(webSearch, routingSource) {
552
552
  if (!webSearch) {
553
553
  return;
554
554
  }
555
- const routePools = routingSource['web_search'] ?? routingSource['search'];
555
+ // webSearch search 路由独立配置:只允许从 routing.web_search 中解析搜索后端。
556
+ const routePools = routingSource['web_search'];
556
557
  if (!Array.isArray(routePools) || !routePools.length) {
557
- throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search (or search) route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
558
+ throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
558
559
  }
559
560
  const targets = new Set();
560
561
  for (const pool of routePools) {
@@ -636,7 +637,8 @@ function normalizeWebSearch(input, routingSource) {
636
637
  force = true;
637
638
  }
638
639
  else {
639
- const webSearchPools = routingSource['web_search'] ?? routingSource['search'] ?? [];
640
+ // 仅从 routing.web_search 推导 force 标记,避免与 routing.search 的行为混淆。
641
+ const webSearchPools = routingSource['web_search'] ?? [];
640
642
  if (Array.isArray(webSearchPools) && webSearchPools.some((pool) => pool.force)) {
641
643
  force = true;
642
644
  }
@@ -777,8 +779,9 @@ function extractProviderAuthEntries(providerId, raw) {
777
779
  else if (typeof apiKeyField === 'string' && apiKeyField.trim()) {
778
780
  pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: apiKeyField.trim() }));
779
781
  }
782
+ const hasExplicitEntries = entries.length > 0;
780
783
  // 自动多 token 扫描:仅在未显式声明多 key、且为受支持的 OAuth 提供方时触发
781
- if (baseType === 'oauth') {
784
+ if (baseType === 'oauth' && !hasExplicitEntries) {
782
785
  const scanCandidates = new Set();
783
786
  const pushCandidate = (value) => {
784
787
  if (typeof value === 'string' && value.trim()) {
@@ -826,7 +829,8 @@ function extractProviderAuthEntries(providerId, raw) {
826
829
  authorizationUrl: readOptionalString(auth?.authorizationUrl ?? auth?.authorization_url ?? auth?.authUrl),
827
830
  userInfoUrl: readOptionalString(auth?.userInfoUrl ?? auth?.user_info_url),
828
831
  refreshUrl: readOptionalString(auth?.refreshUrl ?? auth?.refresh_url),
829
- scopes: normalizeScopeList(auth?.scopes ?? auth?.scope)
832
+ scopes: normalizeScopeList(auth?.scopes ?? auth?.scope),
833
+ cookieFile: readOptionalString(auth?.cookieFile)
830
834
  };
831
835
  const fallbackHasData = Boolean(fallbackExtras.value ||
832
836
  fallbackExtras.secretRef ||
@@ -835,6 +839,7 @@ function extractProviderAuthEntries(providerId, raw) {
835
839
  fallbackExtras.deviceCodeUrl ||
836
840
  fallbackExtras.clientId ||
837
841
  fallbackExtras.clientSecret ||
842
+ fallbackExtras.cookieFile ||
838
843
  (fallbackExtras.scopes &&
839
844
  Array.isArray(fallbackExtras.scopes) &&
840
845
  fallbackExtras.scopes.length));