@jsonstudio/llms 0.6.473 → 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.
- 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/claude-thinking-tools.d.ts +15 -0
- package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
- 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/compat/profiles/chat-gemini.json +15 -14
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
- package/dist/conversion/hub/process/chat-process.js +107 -26
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
- package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
- package/dist/conversion/hub/tool-session-compat.js +299 -0
- package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
- package/dist/conversion/responses/responses-openai-bridge.js +0 -71
- package/dist/conversion/shared/anthropic-message-utils.js +54 -0
- package/dist/conversion/shared/args-mapping.js +11 -3
- package/dist/conversion/shared/gemini-tool-utils.js +8 -0
- package/dist/conversion/shared/responses-output-builder.js +47 -88
- 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.js +42 -27
- package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
- package/dist/conversion/shared/tool-governor.js +75 -4
- 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 -13
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/filters/special/request-toolcalls-stringify.js +5 -55
- package/dist/filters/special/request-tools-normalize.js +14 -23
- package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
- package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
- package/dist/guidance/index.js +70 -27
- package/dist/router/virtual-router/bootstrap.js +10 -5
- package/dist/router/virtual-router/classifier.js +9 -4
- package/dist/router/virtual-router/engine-health.d.ts +22 -0
- package/dist/router/virtual-router/engine-health.js +423 -0
- package/dist/router/virtual-router/engine-logging.d.ts +20 -0
- package/dist/router/virtual-router/engine-logging.js +197 -0
- package/dist/router/virtual-router/engine-selection.d.ts +32 -0
- package/dist/router/virtual-router/engine-selection.js +649 -0
- package/dist/router/virtual-router/engine.d.ts +21 -14
- package/dist/router/virtual-router/engine.js +200 -523
- package/dist/router/virtual-router/message-utils.js +22 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
- package/dist/router/virtual-router/routing-instructions.js +137 -3
- package/dist/router/virtual-router/tool-signals.js +57 -11
- package/dist/router/virtual-router/types.d.ts +30 -0
- package/dist/router/virtual-router/types.js +1 -1
- package/dist/servertool/engine.js +3 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -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.d.ts +1 -0
- package/dist/servertool/handlers/stop-message-auto.js +204 -0
- package/dist/servertool/handlers/vision.js +105 -7
- package/dist/servertool/server-side-tools.d.ts +3 -0
- package/dist/servertool/server-side-tools.js +29 -0
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
- package/dist/tools/apply-patch-structured.d.ts +20 -0
- package/dist/tools/apply-patch-structured.js +239 -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 +14 -5
- package/package.json +2 -2
|
@@ -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,27 +107,10 @@ 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
|
-
else if (name === 'apply_patch') {
|
|
105
|
-
dst.function.parameters = {
|
|
106
|
-
type: 'object',
|
|
107
|
-
properties: {
|
|
108
|
-
input: {
|
|
109
|
-
type: 'string',
|
|
110
|
-
description: 'Unified diff patch body between *** Begin Patch/*** End Patch.'
|
|
111
|
-
},
|
|
112
|
-
patch: {
|
|
113
|
-
type: 'string',
|
|
114
|
-
description: 'Alias of input for backwards compatibility.'
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
required: ['input'],
|
|
118
|
-
additionalProperties: false
|
|
119
|
-
};
|
|
120
|
-
dst.function.description =
|
|
121
|
-
'Use apply_patch to edit files. Provide the diff via the input field (*** Begin Patch ... *** End Patch).';
|
|
122
|
-
}
|
|
123
114
|
}
|
|
124
115
|
catch { /* ignore */ }
|
|
125
116
|
finalTools.push(dst);
|
|
@@ -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,109 @@
|
|
|
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 argStr = fn.arguments;
|
|
52
|
+
if (typeof argStr !== 'string' || !argStr.trim())
|
|
53
|
+
continue;
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(argStr);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// 如果 arguments 不是 JSON 字符串,则保持原样交给下游处理
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isObject(parsed))
|
|
63
|
+
continue;
|
|
64
|
+
// 优先处理 toon: "<patch text>" 形态(兼容旧 TOON 协议)
|
|
65
|
+
const toon = parsed.toon;
|
|
66
|
+
let patchText;
|
|
67
|
+
if (typeof toon === 'string' && toon.trim()) {
|
|
68
|
+
if (toon.includes('*** Begin Patch') && toon.includes('*** End Patch')) {
|
|
69
|
+
patchText = toon;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 否则尝试结构化 JSON(changes 数组 → 统一 diff)
|
|
73
|
+
if (!patchText && isStructuredApplyPatchPayload(parsed)) {
|
|
74
|
+
try {
|
|
75
|
+
patchText = buildStructuredPatch(parsed);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof StructuredApplyPatchError) {
|
|
79
|
+
// 结构化 payload 无法构建补丁时,保留原始 arguments,
|
|
80
|
+
// 由下游工具或客户端根据自身策略报错;这里不吞掉错误。
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!patchText) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const normalized = { input: patchText, patch: patchText };
|
|
90
|
+
try {
|
|
91
|
+
fn.arguments = JSON.stringify(normalized);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// stringify 失败时保留原始 arguments
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// 针对单个 tool_call 的 best-effort,不影响其他工具
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
out.choices = choices;
|
|
103
|
+
return { ok: true, data: out };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { ok: true, data: input };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -10,18 +10,37 @@ function decodeToonPairs(toon) {
|
|
|
10
10
|
try {
|
|
11
11
|
const out = {};
|
|
12
12
|
const lines = String(toon).split(/\r?\n/);
|
|
13
|
+
let currentKey = null;
|
|
14
|
+
let currentVal = '';
|
|
15
|
+
const flush = () => {
|
|
16
|
+
if (currentKey) {
|
|
17
|
+
out[currentKey] = currentVal;
|
|
18
|
+
}
|
|
19
|
+
currentKey = null;
|
|
20
|
+
currentVal = '';
|
|
21
|
+
};
|
|
13
22
|
for (const raw of lines) {
|
|
14
23
|
const line = raw.trim();
|
|
15
24
|
if (!line)
|
|
16
25
|
continue;
|
|
17
26
|
const m = line.match(/^([A-Za-z0-9_\-]+)\s*:\s*(.*)$/);
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
if (m) {
|
|
28
|
+
// 新的 key: value 行,先提交上一段,再开始累积新 key 的值
|
|
29
|
+
flush();
|
|
30
|
+
currentKey = m[1];
|
|
31
|
+
currentVal = m[2] ?? '';
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// 非 key: value 行视为上一 key 的续行(例如多行脚本)
|
|
35
|
+
if (!currentKey) {
|
|
36
|
+
// 如果一开始就遇到无法识别的行,认为整个 TOON 不是我们支持的形态
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
currentVal += (currentVal ? '\n' : '') + raw;
|
|
40
|
+
}
|
|
23
41
|
}
|
|
24
|
-
|
|
42
|
+
flush();
|
|
43
|
+
return Object.keys(out).length ? out : null;
|
|
25
44
|
}
|
|
26
45
|
catch {
|
|
27
46
|
return null;
|
|
@@ -34,11 +53,12 @@ function decodeToonPairs(toon) {
|
|
|
34
53
|
export class ResponseToolArgumentsToonDecodeFilter {
|
|
35
54
|
name = 'response_tool_arguments_toon_decode';
|
|
36
55
|
stage = 'response_pre';
|
|
37
|
-
apply(input) {
|
|
56
|
+
apply(input, ctx) {
|
|
38
57
|
if (!envEnabled())
|
|
39
58
|
return { ok: true, data: input };
|
|
40
59
|
try {
|
|
41
60
|
const out = JSON.parse(JSON.stringify(input || {}));
|
|
61
|
+
const warnings = [];
|
|
42
62
|
const choices = Array.isArray(out.choices) ? out.choices : [];
|
|
43
63
|
for (const ch of choices) {
|
|
44
64
|
const msg = ch && ch.message ? ch.message : undefined;
|
|
@@ -64,14 +84,36 @@ export class ResponseToolArgumentsToonDecodeFilter {
|
|
|
64
84
|
if (typeof toon !== 'string' || !toon.trim())
|
|
65
85
|
continue;
|
|
66
86
|
const kv = decodeToonPairs(toon);
|
|
67
|
-
if (!kv)
|
|
87
|
+
if (!kv) {
|
|
88
|
+
const preview = toon.split(/\r?\n/).slice(0, 5).join('\n');
|
|
89
|
+
const warnMsg = `response_tool_arguments_toon_decode: failed to decode TOON arguments for tool "${fn.name ?? 'unknown'}"`;
|
|
90
|
+
warnings.push(warnMsg);
|
|
91
|
+
if (ctx?.debug?.emit) {
|
|
92
|
+
ctx.debug.emit('tool_toon_decode_error', {
|
|
93
|
+
requestId: ctx.requestId,
|
|
94
|
+
model: ctx.model,
|
|
95
|
+
endpoint: ctx.endpoint,
|
|
96
|
+
stage: ctx.stage,
|
|
97
|
+
provider: ctx.provider,
|
|
98
|
+
toolName: fn.name ?? 'unknown',
|
|
99
|
+
message: warnMsg,
|
|
100
|
+
toonPreview: preview
|
|
101
|
+
});
|
|
102
|
+
}
|
|
68
103
|
continue; // keep original if decode fails
|
|
69
|
-
|
|
70
|
-
const
|
|
104
|
+
}
|
|
105
|
+
const command = typeof kv['command'] === 'string' && kv['command'].trim()
|
|
106
|
+
? kv['command']
|
|
107
|
+
: (typeof kv['cmd'] === 'string' && kv['cmd'].trim() ? kv['cmd'] : undefined);
|
|
108
|
+
const workdir = typeof kv['workdir'] === 'string' && kv['workdir'].trim()
|
|
109
|
+
? kv['workdir']
|
|
110
|
+
: (typeof kv['cwd'] === 'string' && kv['cwd'].trim() ? kv['cwd'] : undefined);
|
|
71
111
|
if (typeof command === 'string' && command.trim()) {
|
|
72
|
-
const merged = { command };
|
|
73
|
-
if (typeof workdir === 'string' && workdir.trim())
|
|
112
|
+
const merged = { cmd: command };
|
|
113
|
+
if (typeof workdir === 'string' && workdir.trim()) {
|
|
74
114
|
merged.workdir = workdir;
|
|
115
|
+
}
|
|
116
|
+
merged.command = command;
|
|
75
117
|
try {
|
|
76
118
|
fn.arguments = JSON.stringify(merged);
|
|
77
119
|
}
|
|
@@ -85,7 +127,7 @@ export class ResponseToolArgumentsToonDecodeFilter {
|
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
129
|
out.choices = choices;
|
|
88
|
-
return { ok: true, data: out };
|
|
130
|
+
return warnings.length ? { ok: true, data: out, warnings } : { ok: true, data: out };
|
|
89
131
|
}
|
|
90
132
|
catch {
|
|
91
133
|
return { ok: true, data: input };
|
package/dist/guidance/index.js
CHANGED
|
@@ -62,27 +62,48 @@ function augmentApplyPatch(fn) {
|
|
|
62
62
|
const marker = '[Codex ApplyPatch Guidance]';
|
|
63
63
|
const guidance = [
|
|
64
64
|
marker,
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'*** Update File: path/to/file.ts',
|
|
71
|
-
'@@',
|
|
72
|
-
'- old line',
|
|
73
|
-
'+ new line',
|
|
74
|
-
'*** 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.'
|
|
75
70
|
].join('\n');
|
|
76
71
|
const params = ensureObjectSchema(fn.parameters);
|
|
77
72
|
const props = params.properties;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 = {
|
|
76
|
+
type: 'array',
|
|
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
|
+
}
|
|
103
|
+
};
|
|
104
|
+
params.required = Array.isArray(params.required) ? params.required : [];
|
|
105
|
+
if (!params.required.includes('changes')) {
|
|
106
|
+
params.required.push('changes');
|
|
86
107
|
}
|
|
87
108
|
params.additionalProperties = false;
|
|
88
109
|
fn.parameters = params;
|
|
@@ -171,21 +192,44 @@ export function augmentAnthropicTools(tools) {
|
|
|
171
192
|
const n = name.trim();
|
|
172
193
|
try {
|
|
173
194
|
if (n === 'apply_patch') {
|
|
174
|
-
if (!isObject(schema.properties?.
|
|
175
|
-
schema.properties.
|
|
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
|
+
};
|
|
176
220
|
}
|
|
177
221
|
if (!Array.isArray(schema.required))
|
|
178
222
|
schema.required = [];
|
|
179
|
-
if (!schema.required.includes('
|
|
180
|
-
schema.required.push('
|
|
223
|
+
if (!schema.required.includes('changes'))
|
|
224
|
+
schema.required.push('changes');
|
|
181
225
|
schema.additionalProperties = false;
|
|
182
226
|
copy.input_schema = schema;
|
|
183
227
|
const marker = '[Codex ApplyPatch Guidance]';
|
|
184
228
|
const guidance = [
|
|
185
229
|
marker,
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
'
|
|
230
|
+
'Provide structured changes (insert_after / insert_before / replace / delete / create_file / delete_file) instead of raw patch text.',
|
|
231
|
+
'Each change must include the target file (relative path) plus anchor/target snippets and the replacement lines.',
|
|
232
|
+
'所有路径必须相对工作区根目录,禁止输出以 / 或盘符开头的绝对路径。'
|
|
189
233
|
].join('\n');
|
|
190
234
|
copy.description = appendOnce(desc, guidance, marker);
|
|
191
235
|
}
|
|
@@ -220,8 +264,7 @@ export function buildSystemToolGuidance() {
|
|
|
220
264
|
lines.push(bullet('function.arguments must be a single JSON string. / arguments 必须是单个 JSON 字符串。'));
|
|
221
265
|
lines.push(bullet('shell: Place ALL intent into the command argv array only; do not invent extra keys. / shell 所有意图写入 command 数组,不要添加额外键名。'));
|
|
222
266
|
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。'));
|
|
223
|
-
lines.push(bullet('apply_patch: Provide
|
|
224
|
-
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'));
|
|
267
|
+
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。'));
|
|
225
268
|
lines.push(bullet('update_plan: Keep exactly one step in_progress; others pending/completed. / 仅一个 in_progress 步骤。'));
|
|
226
269
|
lines.push(bullet('view_image: Path must be an image file (.png .jpg .jpeg .gif .webp .bmp .svg). / 仅图片路径。'));
|
|
227
270
|
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));
|
|
@@ -11,12 +11,14 @@ export class RoutingClassifier {
|
|
|
11
11
|
}
|
|
12
12
|
classify(features) {
|
|
13
13
|
const lastToolCategory = features.lastAssistantToolCategory;
|
|
14
|
+
const userInterruptsToolContinuation = features.latestMessageFromUser === true && !features.hasTools && !features.hasToolCallResponses;
|
|
15
|
+
const allowContinuation = !userInterruptsToolContinuation;
|
|
14
16
|
const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
|
|
15
17
|
const latestMessageFromUser = features.latestMessageFromUser === true;
|
|
16
|
-
const codingContinuation = lastToolCategory === 'write';
|
|
17
|
-
const thinkingContinuation = lastToolCategory === 'read';
|
|
18
|
-
const searchContinuation = lastToolCategory === 'search';
|
|
19
|
-
const toolsContinuation = lastToolCategory === 'other';
|
|
18
|
+
const codingContinuation = allowContinuation && lastToolCategory === 'write';
|
|
19
|
+
const thinkingContinuation = allowContinuation && lastToolCategory === 'read';
|
|
20
|
+
const searchContinuation = allowContinuation && lastToolCategory === 'search';
|
|
21
|
+
const toolsContinuation = allowContinuation && lastToolCategory === 'other';
|
|
20
22
|
const evaluationMap = {
|
|
21
23
|
vision: {
|
|
22
24
|
triggered: features.hasImageAttachment,
|
|
@@ -39,10 +41,13 @@ export class RoutingClassifier {
|
|
|
39
41
|
reason: 'thinking:last-tool-read'
|
|
40
42
|
},
|
|
41
43
|
search: {
|
|
44
|
+
// search 路由:仅在上一轮 assistant 使用 search 类工具时继续命中,
|
|
45
|
+
// 不因本轮是否声明 web_search 工具而改变路由。
|
|
42
46
|
triggered: searchContinuation,
|
|
43
47
|
reason: 'search:last-tool-search'
|
|
44
48
|
},
|
|
45
49
|
tools: {
|
|
50
|
+
// tools 路由:通用工具分支,包括首次声明的 web/search 工具。
|
|
46
51
|
triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
|
|
47
52
|
reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
|
|
48
53
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ProviderHealthManager } from './health-manager.js';
|
|
2
|
+
import { ProviderRegistry } from './provider-registry.js';
|
|
3
|
+
import type { ProviderErrorEvent, ProviderFailureEvent, ProviderHealthConfig } from './types.js';
|
|
4
|
+
type DebugLike = {
|
|
5
|
+
log?: (...args: unknown[]) => void;
|
|
6
|
+
} | Console | undefined;
|
|
7
|
+
export declare function resetRateLimitBackoffForProvider(providerKey: string): void;
|
|
8
|
+
export declare function handleProviderFailureImpl(event: ProviderFailureEvent, healthManager: ProviderHealthManager, healthConfig: Required<ProviderHealthConfig>, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void): void;
|
|
9
|
+
export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthConfig: Required<ProviderHealthConfig>): ProviderFailureEvent | null;
|
|
10
|
+
export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
|
|
11
|
+
/**
|
|
12
|
+
* 处理来自 Host 侧的配额恢复事件:
|
|
13
|
+
* - 清除指定 providerKey 在健康管理器中的熔断/冷却状态;
|
|
14
|
+
* - 清理对应的速率退避计数;
|
|
15
|
+
* - 调用调用方提供的 clearProviderCooldown 回调移除显式 cooldown TTL。
|
|
16
|
+
*
|
|
17
|
+
* 返回值表示是否已处理(true=已处理且后续应跳过常规错误映射逻辑)。
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyQuotaRecoveryImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, clearProviderCooldown: (providerKey: string) => void, debug?: DebugLike): boolean;
|
|
20
|
+
export declare function applyQuotaDepletedImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): boolean;
|
|
21
|
+
export declare function deriveReason(code: string, stage: string, statusCode?: number): string;
|
|
22
|
+
export {};
|