@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.
- package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
- package/dist/conversion/codecs/openai-openai-codec.js +2 -1
- package/dist/conversion/codecs/responses-openai-codec.js +3 -2
- package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -2
- package/dist/conversion/hub/pipeline/hub-pipeline.js +72 -81
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
- package/dist/conversion/hub/process/chat-process.js +68 -24
- package/dist/conversion/hub/response/provider-response.js +0 -8
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +22 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +267 -14
- package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
- package/dist/conversion/responses/responses-openai-bridge.js +1 -13
- package/dist/conversion/shared/anthropic-message-utils.js +54 -0
- package/dist/conversion/shared/args-mapping.js +11 -3
- package/dist/conversion/shared/responses-output-builder.js +42 -21
- package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
- package/dist/conversion/shared/streaming-text-extractor.js +31 -38
- package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
- package/dist/conversion/shared/text-markup-normalizer.js +118 -31
- package/dist/conversion/shared/tool-filter-pipeline.js +56 -30
- package/dist/conversion/shared/tool-harvester.js +43 -12
- package/dist/conversion/shared/tool-mapping.d.ts +1 -0
- package/dist/conversion/shared/tool-mapping.js +33 -19
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/filters/special/request-tools-normalize.js +14 -4
- package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
- package/dist/filters/special/response-apply-patch-toon-decode.js +117 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.js +154 -26
- package/dist/guidance/index.js +71 -42
- package/dist/router/virtual-router/bootstrap.js +10 -5
- package/dist/router/virtual-router/classifier.js +16 -7
- package/dist/router/virtual-router/engine-health.d.ts +11 -0
- package/dist/router/virtual-router/engine-health.js +217 -4
- package/dist/router/virtual-router/engine-logging.d.ts +2 -1
- package/dist/router/virtual-router/engine-logging.js +35 -3
- package/dist/router/virtual-router/engine.d.ts +17 -1
- package/dist/router/virtual-router/engine.js +184 -6
- package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
- package/dist/router/virtual-router/routing-instructions.js +19 -1
- package/dist/router/virtual-router/tool-signals.d.ts +2 -1
- package/dist/router/virtual-router/tool-signals.js +324 -119
- package/dist/router/virtual-router/types.d.ts +31 -1
- package/dist/router/virtual-router/types.js +2 -2
- package/dist/servertool/engine.js +3 -0
- package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
- package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
- package/dist/servertool/handlers/stop-message-auto.js +61 -4
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +27 -0
- package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +23 -3
- package/dist/tools/apply-patch-structured.d.ts +20 -0
- package/dist/tools/apply-patch-structured.js +240 -0
- package/dist/tools/tool-description-utils.d.ts +5 -0
- package/dist/tools/tool-description-utils.js +50 -0
- package/dist/tools/tool-registry.js +11 -193
- 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
|
|
79
|
-
|
|
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: {
|
|
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
|
-
|
|
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 (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 {
|
|
79
|
-
|
|
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 };
|
package/dist/guidance/index.js
CHANGED
|
@@ -62,36 +62,48 @@ function augmentApplyPatch(fn) {
|
|
|
62
62
|
const marker = '[Codex ApplyPatch Guidance]';
|
|
63
63
|
const guidance = [
|
|
64
64
|
marker,
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'Paths
|
|
69
|
-
'
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
items: {
|
|
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.
|
|
90
|
-
if (!
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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('
|
|
195
|
-
schema.required.push('
|
|
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
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
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:
|
|
240
|
-
lines.push(bullet('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 在写入前必须先通过合适的工具读取目标文件最新内容,并基于该内容生成变更。'));
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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));
|