@jsonstudio/llms 0.6.473 → 0.6.567
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/compat/actions/claude-thinking-tools.d.ts +15 -0
- package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -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.js +15 -0
- 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 +44 -17
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +13 -8
- package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
- package/dist/conversion/hub/tool-session-compat.js +299 -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/gemini-tool-utils.js +8 -0
- package/dist/conversion/shared/responses-output-builder.js +6 -68
- package/dist/conversion/shared/tool-governor.js +75 -4
- package/dist/conversion/shared/tool-mapping.js +14 -8
- package/dist/filters/special/request-toolcalls-stringify.js +5 -55
- package/dist/filters/special/request-tools-normalize.js +0 -19
- package/dist/guidance/index.js +25 -9
- package/dist/router/virtual-router/engine-health.d.ts +11 -0
- package/dist/router/virtual-router/engine-health.js +210 -0
- package/dist/router/virtual-router/engine-logging.d.ts +19 -0
- package/dist/router/virtual-router/engine-logging.js +165 -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 +4 -13
- package/dist/router/virtual-router/engine.js +64 -535
- package/dist/router/virtual-router/message-utils.js +22 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -1
- package/dist/router/virtual-router/routing-instructions.js +119 -3
- 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/stop-message-auto.d.ts +1 -0
- package/dist/servertool/handlers/stop-message-auto.js +147 -0
- package/dist/servertool/handlers/vision.js +105 -7
- package/dist/servertool/server-side-tools.d.ts +2 -0
- package/dist/servertool/server-side-tools.js +2 -0
- package/dist/tools/tool-registry.js +195 -4
- package/package.json +1 -1
|
@@ -177,47 +177,6 @@ function normalizeUsage(usageRaw) {
|
|
|
177
177
|
}
|
|
178
178
|
return usageRaw;
|
|
179
179
|
}
|
|
180
|
-
function extractApplyPatchArguments(rawArgs) {
|
|
181
|
-
// Upstream Responses providers may wrap apply_patch arguments in a JSON object
|
|
182
|
-
// (e.g. { patch: '*** Begin Patch...', input: '...' }). For Codex, the tool
|
|
183
|
-
// expects a FREEFORM patch string obeying the unified diff grammar. Here we
|
|
184
|
-
// best-effort extract such a patch string when available.
|
|
185
|
-
const tryExtractFromObject = (obj) => {
|
|
186
|
-
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
187
|
-
return null;
|
|
188
|
-
const record = obj;
|
|
189
|
-
const candidates = ['patch', 'input'];
|
|
190
|
-
for (const key of candidates) {
|
|
191
|
-
const value = record[key];
|
|
192
|
-
if (typeof value !== 'string')
|
|
193
|
-
continue;
|
|
194
|
-
const trimmed = value.trimStart();
|
|
195
|
-
if (trimmed.startsWith('*** Begin Patch')) {
|
|
196
|
-
return trimmed;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return null;
|
|
200
|
-
};
|
|
201
|
-
if (typeof rawArgs === 'string') {
|
|
202
|
-
const trimmed = rawArgs.trimStart();
|
|
203
|
-
if (trimmed.startsWith('*** Begin Patch')) {
|
|
204
|
-
return trimmed;
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
const parsed = JSON.parse(rawArgs);
|
|
208
|
-
const fromObject = tryExtractFromObject(parsed);
|
|
209
|
-
if (fromObject) {
|
|
210
|
-
return fromObject;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// non-JSON string that is not a patch header; leave to caller
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
const fromObject = tryExtractFromObject(rawArgs);
|
|
219
|
-
return fromObject;
|
|
220
|
-
}
|
|
221
180
|
function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, baseCount, offset) {
|
|
222
181
|
try {
|
|
223
182
|
const fn = call?.function || {};
|
|
@@ -228,37 +187,16 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
|
|
|
228
187
|
if (!sanitized || sanitized.toLowerCase() === 'tool')
|
|
229
188
|
return null;
|
|
230
189
|
const rawArgs = fn?.arguments ?? call.arguments ?? {};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (patch != null) {
|
|
235
|
-
argsStr = patch;
|
|
236
|
-
}
|
|
237
|
-
else if (typeof rawArgs === 'string') {
|
|
238
|
-
argsStr = rawArgs;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
190
|
+
const argsStr = typeof rawArgs === 'string'
|
|
191
|
+
? rawArgs
|
|
192
|
+
: (() => {
|
|
241
193
|
try {
|
|
242
|
-
|
|
194
|
+
return JSON.stringify(rawArgs ?? {});
|
|
243
195
|
}
|
|
244
196
|
catch {
|
|
245
|
-
|
|
197
|
+
return '{}';
|
|
246
198
|
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
argsStr =
|
|
251
|
-
typeof rawArgs === 'string'
|
|
252
|
-
? rawArgs
|
|
253
|
-
: (() => {
|
|
254
|
-
try {
|
|
255
|
-
return JSON.stringify(rawArgs ?? {});
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
return '{}';
|
|
259
|
-
}
|
|
260
|
-
})();
|
|
261
|
-
}
|
|
199
|
+
})();
|
|
262
200
|
const originalCallId = typeof call.id === 'string' && call.id.trim().length
|
|
263
201
|
? String(call.id)
|
|
264
202
|
: (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// canonicalizer 按需加载(避免在请求侧仅注入时引入不必要的模块)
|
|
4
4
|
// enforceChatBudget: 为避免在请求侧引入多余依赖,这里提供最小实现(保留形状,不裁剪)。
|
|
5
5
|
import { augmentOpenAITools } from '../../guidance/index.js';
|
|
6
|
+
import { validateToolCall } from '../../tools/tool-registry.js';
|
|
6
7
|
function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
7
8
|
// Note: tool schema strict augmentation removed per alignment
|
|
8
9
|
function enforceChatBudget(chat, _modelId) { return chat; }
|
|
@@ -173,8 +174,7 @@ export function processChatRequestTools(request, opts) {
|
|
|
173
174
|
continue;
|
|
174
175
|
const typeStr = String(t.type || '').toLowerCase();
|
|
175
176
|
const nameStr = typeof fn.name === 'string' ? String(fn.name).toLowerCase() : '';
|
|
176
|
-
const shouldPatch = typeStr === 'function' ||
|
|
177
|
-
nameStr === 'apply_patch';
|
|
177
|
+
const shouldPatch = typeStr === 'function' || nameStr === 'apply_patch';
|
|
178
178
|
if (!shouldPatch)
|
|
179
179
|
continue;
|
|
180
180
|
if (!Object.prototype.hasOwnProperty.call(fn, 'parameters')) {
|
|
@@ -208,7 +208,8 @@ export function processChatRequestTools(request, opts) {
|
|
|
208
208
|
catch { /* ignore */ }
|
|
209
209
|
// 4) Enforce payload budget (context bytes) with minimal loss policy
|
|
210
210
|
const modelId = typeof canonical?.model === 'string' ? String(canonical.model) : 'unknown';
|
|
211
|
-
|
|
211
|
+
const budgeted = enforceChatBudget(canonical, modelId);
|
|
212
|
+
return normalizeApplyPatchToolCallsOnRequest(budgeted);
|
|
212
213
|
}
|
|
213
214
|
catch {
|
|
214
215
|
return out;
|
|
@@ -219,6 +220,75 @@ export function processChatRequestTools(request, opts) {
|
|
|
219
220
|
* - Canonicalize textual tool markup to tool_calls; ensure finish_reason and content=null policy
|
|
220
221
|
*/
|
|
221
222
|
import { repairArgumentsToString, parseLenient } from './jsonish.js';
|
|
223
|
+
function normalizeApplyPatchToolCallsOnResponse(chat) {
|
|
224
|
+
try {
|
|
225
|
+
const out = JSON.parse(JSON.stringify(chat));
|
|
226
|
+
const choices = Array.isArray(out?.choices) ? out.choices : [];
|
|
227
|
+
for (const ch of choices) {
|
|
228
|
+
const msg = ch && ch.message ? ch.message : undefined;
|
|
229
|
+
const tcs = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
|
|
230
|
+
if (!tcs.length)
|
|
231
|
+
continue;
|
|
232
|
+
for (const tc of tcs) {
|
|
233
|
+
try {
|
|
234
|
+
const fn = tc && tc.function ? tc.function : undefined;
|
|
235
|
+
const name = typeof fn?.name === 'string' ? String(fn.name).trim().toLowerCase() : '';
|
|
236
|
+
if (name !== 'apply_patch')
|
|
237
|
+
continue;
|
|
238
|
+
const rawArgs = fn?.arguments;
|
|
239
|
+
const argsStr = repairArgumentsToString(rawArgs);
|
|
240
|
+
const validation = validateToolCall('apply_patch', argsStr);
|
|
241
|
+
if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
|
|
242
|
+
fn.arguments = validation.normalizedArgs;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// best-effort per tool_call
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return chat;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function normalizeApplyPatchToolCallsOnRequest(request) {
|
|
257
|
+
try {
|
|
258
|
+
const out = JSON.parse(JSON.stringify(request));
|
|
259
|
+
const messages = Array.isArray(out?.messages) ? out.messages : [];
|
|
260
|
+
for (const msg of messages) {
|
|
261
|
+
if (!msg || typeof msg !== 'object')
|
|
262
|
+
continue;
|
|
263
|
+
if (msg.role !== 'assistant')
|
|
264
|
+
continue;
|
|
265
|
+
const tcs = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
266
|
+
if (!tcs.length)
|
|
267
|
+
continue;
|
|
268
|
+
for (const tc of tcs) {
|
|
269
|
+
try {
|
|
270
|
+
const fn = tc && tc.function ? tc.function : undefined;
|
|
271
|
+
const name = typeof fn?.name === 'string' ? String(fn.name).trim().toLowerCase() : '';
|
|
272
|
+
if (name !== 'apply_patch')
|
|
273
|
+
continue;
|
|
274
|
+
const rawArgs = fn?.arguments;
|
|
275
|
+
const argsStr = repairArgumentsToString(rawArgs);
|
|
276
|
+
const validation = validateToolCall('apply_patch', argsStr);
|
|
277
|
+
if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
|
|
278
|
+
fn.arguments = validation.normalizedArgs;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// best-effort per tool_call
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return request;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
222
292
|
function enhanceResponseToolArguments(chat) {
|
|
223
293
|
try {
|
|
224
294
|
const enable = String(process?.env?.RCC_TOOL_ENHANCE ?? '1').trim() !== '0';
|
|
@@ -336,7 +406,8 @@ export function processChatResponseTools(resp) {
|
|
|
336
406
|
try {
|
|
337
407
|
const { canonicalizeChatResponseTools } = require('./tool-canonicalizer.js');
|
|
338
408
|
const canon = canonicalizeChatResponseTools(resp);
|
|
339
|
-
|
|
409
|
+
const withPatch = normalizeApplyPatchToolCallsOnResponse(canon);
|
|
410
|
+
return enhanceResponseToolArguments(withPatch);
|
|
340
411
|
}
|
|
341
412
|
catch {
|
|
342
413
|
return resp;
|
|
@@ -29,20 +29,26 @@ function ensureApplyPatchSchema(seed) {
|
|
|
29
29
|
const schema = seed ? { ...seed } : {};
|
|
30
30
|
schema.type = typeof schema.type === 'string' ? schema.type : 'object';
|
|
31
31
|
const properties = isPlainObject(schema.properties) ? { ...schema.properties } : {};
|
|
32
|
-
properties.input
|
|
32
|
+
delete properties.input;
|
|
33
|
+
delete properties.patch;
|
|
34
|
+
properties.patch = {
|
|
33
35
|
type: 'string',
|
|
34
|
-
description: 'Unified diff patch
|
|
36
|
+
description: 'Unified diff patch text. The first line MUST be exactly "*** Begin Patch", followed by one or more file sections starting with ' +
|
|
37
|
+
'"*** Add File: {path}", "*** Update File: {path}", or "*** Delete File: {path}", using hunks with lines prefixed by " ", "+", or "-". ' +
|
|
38
|
+
'The last non-empty line MUST be exactly "*** End Patch". Git-style headers like "--- a/file" / "+++ b/file" and merge markers ' +
|
|
39
|
+
'"<<<<<<<", "=======", ">>>>>>>" are NOT allowed.'
|
|
35
40
|
};
|
|
36
|
-
if (!properties.
|
|
37
|
-
properties.
|
|
38
|
-
type: '
|
|
39
|
-
description: '
|
|
41
|
+
if (!isPlainObject(properties.paths)) {
|
|
42
|
+
properties.paths = {
|
|
43
|
+
type: 'array',
|
|
44
|
+
description: 'Optional explicit list of relative file paths that the patch touches. Each entry must be a relative workspace path (packages/foo/file.ts).',
|
|
45
|
+
items: { type: 'string' }
|
|
40
46
|
};
|
|
41
47
|
}
|
|
42
48
|
schema.properties = properties;
|
|
43
49
|
const requiredList = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === 'string') : [];
|
|
44
|
-
if (!requiredList.includes('
|
|
45
|
-
requiredList.push('
|
|
50
|
+
if (!requiredList.includes('patch')) {
|
|
51
|
+
requiredList.push('patch');
|
|
46
52
|
}
|
|
47
53
|
schema.required = requiredList;
|
|
48
54
|
if (typeof schema.additionalProperties !== 'boolean') {
|
|
@@ -27,12 +27,14 @@ export class RequestToolCallsStringifyFilter {
|
|
|
27
27
|
const fnName = typeof fn.name === 'string' ? fn.name.trim() : '';
|
|
28
28
|
// Case 1: non-string arguments → stringify directly
|
|
29
29
|
if (currentArgs !== undefined && typeof currentArgs !== 'string') {
|
|
30
|
+
let argsJson = '{}';
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
+
argsJson = JSON.stringify(currentArgs ?? {});
|
|
32
33
|
}
|
|
33
34
|
catch {
|
|
34
|
-
|
|
35
|
+
argsJson = '{}';
|
|
35
36
|
}
|
|
37
|
+
fn.arguments = argsJson;
|
|
36
38
|
tc.function = fn;
|
|
37
39
|
continue;
|
|
38
40
|
}
|
|
@@ -61,9 +63,6 @@ export class RequestToolCallsStringifyFilter {
|
|
|
61
63
|
if (fnName === 'shell') {
|
|
62
64
|
fn.arguments = JSON.stringify({ command: currentArgs });
|
|
63
65
|
}
|
|
64
|
-
else if (fnName === 'apply_patch') {
|
|
65
|
-
fn.arguments = JSON.stringify({ patch: currentArgs });
|
|
66
|
-
}
|
|
67
66
|
else {
|
|
68
67
|
fn.arguments = JSON.stringify({ input: currentArgs });
|
|
69
68
|
}
|
|
@@ -74,56 +73,7 @@ export class RequestToolCallsStringifyFilter {
|
|
|
74
73
|
tc.function = fn;
|
|
75
74
|
continue;
|
|
76
75
|
}
|
|
77
|
-
//
|
|
78
|
-
if (parsedOk && fnName === 'apply_patch') {
|
|
79
|
-
try {
|
|
80
|
-
let obj = parsedValue;
|
|
81
|
-
// 1) 若整体是字符串,则视为补丁文本
|
|
82
|
-
if (typeof obj === 'string') {
|
|
83
|
-
obj = { patch: obj };
|
|
84
|
-
}
|
|
85
|
-
if (obj && typeof obj === 'object') {
|
|
86
|
-
const container = obj;
|
|
87
|
-
const rawPatch = container.patch;
|
|
88
|
-
const rawInput = container.input;
|
|
89
|
-
// 2) 若 patch 是形如 JSON 的字符串,尝试解包 {"input": "..."} 或 {"patch": "..."}
|
|
90
|
-
if (typeof rawPatch === 'string') {
|
|
91
|
-
const ptrim = rawPatch.trim();
|
|
92
|
-
if (ptrim.startsWith('{') && ptrim.endsWith('}')) {
|
|
93
|
-
try {
|
|
94
|
-
const inner = JSON.parse(ptrim);
|
|
95
|
-
if (typeof inner.patch === 'string') {
|
|
96
|
-
container.patch = inner.patch;
|
|
97
|
-
}
|
|
98
|
-
else if (typeof inner.input === 'string') {
|
|
99
|
-
container.patch = inner.input;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
container.patch = ptrim;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
container.patch = rawPatch;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
else if (rawPatch === undefined && typeof rawInput === 'string') {
|
|
111
|
-
// 3) 若只有 input 字段,则复制一份到 patch,避免双层包装
|
|
112
|
-
container.patch = rawInput;
|
|
113
|
-
}
|
|
114
|
-
fn.arguments = JSON.stringify(container);
|
|
115
|
-
tc.function = fn;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// 回退到原始字符串形状
|
|
121
|
-
fn.arguments = trimmed;
|
|
122
|
-
tc.function = fn;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// 其它合法 JSON 场景保持原样
|
|
76
|
+
// 合法 JSON 场景保持原样
|
|
127
77
|
fn.arguments = trimmed;
|
|
128
78
|
tc.function = fn;
|
|
129
79
|
}
|
|
@@ -101,25 +101,6 @@ export class RequestOpenAIToolsNormalizeFilter {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
}
|
|
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
104
|
}
|
|
124
105
|
catch { /* ignore */ }
|
|
125
106
|
finalTools.push(dst);
|
package/dist/guidance/index.js
CHANGED
|
@@ -62,9 +62,11 @@ 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.',
|
|
66
|
-
'
|
|
67
|
-
'
|
|
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/... 这样的相对路径。',
|
|
68
70
|
'Example:',
|
|
69
71
|
'*** Begin Patch',
|
|
70
72
|
'*** Update File: path/to/file.ts',
|
|
@@ -75,9 +77,16 @@ function augmentApplyPatch(fn) {
|
|
|
75
77
|
].join('\n');
|
|
76
78
|
const params = ensureObjectSchema(fn.parameters);
|
|
77
79
|
const props = params.properties;
|
|
78
|
-
if (
|
|
79
|
-
props.
|
|
80
|
+
if (isObject(props)) {
|
|
81
|
+
delete props.input;
|
|
80
82
|
}
|
|
83
|
+
props.patch = { type: 'string', description: 'Unified diff patch text' };
|
|
84
|
+
props.paths = {
|
|
85
|
+
type: 'array',
|
|
86
|
+
description: 'Explicit list of relative file paths affected by this patch (e.g., ["src/foo.ts"]).',
|
|
87
|
+
items: { type: 'string' }
|
|
88
|
+
};
|
|
89
|
+
params.properties = props;
|
|
81
90
|
if (!Array.isArray(params.required)) {
|
|
82
91
|
params.required = [];
|
|
83
92
|
}
|
|
@@ -171,9 +180,15 @@ export function augmentAnthropicTools(tools) {
|
|
|
171
180
|
const n = name.trim();
|
|
172
181
|
try {
|
|
173
182
|
if (n === 'apply_patch') {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
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;
|
|
177
192
|
if (!Array.isArray(schema.required))
|
|
178
193
|
schema.required = [];
|
|
179
194
|
if (!schema.required.includes('patch'))
|
|
@@ -183,7 +198,8 @@ export function augmentAnthropicTools(tools) {
|
|
|
183
198
|
const marker = '[Codex ApplyPatch Guidance]';
|
|
184
199
|
const guidance = [
|
|
185
200
|
marker,
|
|
186
|
-
'Use unified diff patch with *** Begin Patch/End Patch. Return only the patch
|
|
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.',
|
|
187
203
|
'All file paths must stay relative to the workspace root; never emit leading \'/\' or drive letters.',
|
|
188
204
|
'所有文件路径都必须相对当前工作区根目录,禁止输出以 / 或盘符开头的绝对路径。'
|
|
189
205
|
].join('\n');
|
|
@@ -0,0 +1,11 @@
|
|
|
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 handleProviderFailureImpl(event: ProviderFailureEvent, healthManager: ProviderHealthManager, healthConfig: Required<ProviderHealthConfig>, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void): void;
|
|
8
|
+
export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthConfig: Required<ProviderHealthConfig>): ProviderFailureEvent | null;
|
|
9
|
+
export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
|
|
10
|
+
export declare function deriveReason(code: string, stage: string, statusCode?: number): string;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const SERIES_COOLDOWN_DETAIL_KEY = 'virtualRouterSeriesCooldown';
|
|
2
|
+
export function handleProviderFailureImpl(event, healthManager, healthConfig, markProviderCooldown) {
|
|
3
|
+
if (!event || !event.providerKey) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
if (event.affectsHealth === false) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (event.fatal) {
|
|
10
|
+
healthManager.tripProvider(event.providerKey, event.reason, event.cooldownOverrideMs);
|
|
11
|
+
}
|
|
12
|
+
else if (event.reason === 'rate_limit' && event.statusCode === 429) {
|
|
13
|
+
healthManager.cooldownProvider(event.providerKey, event.reason, event.cooldownOverrideMs);
|
|
14
|
+
const ttl = event.cooldownOverrideMs ?? healthConfig.cooldownMs;
|
|
15
|
+
markProviderCooldown(event.providerKey, ttl);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
healthManager.recordFailure(event.providerKey, event.reason);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function mapProviderErrorImpl(event, healthConfig) {
|
|
22
|
+
if (!event || !event.runtime) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const runtime = event.runtime;
|
|
26
|
+
const providerKey = runtime.providerKey ||
|
|
27
|
+
(runtime.target && typeof runtime.target === 'object'
|
|
28
|
+
? runtime.target.providerKey
|
|
29
|
+
: undefined);
|
|
30
|
+
if (!providerKey) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const routeName = runtime.routeName;
|
|
34
|
+
const statusCode = event.status;
|
|
35
|
+
const code = event.code?.toUpperCase() ?? 'ERR_UNKNOWN';
|
|
36
|
+
const stage = event.stage?.toLowerCase() ?? 'unknown';
|
|
37
|
+
const recoverable = event.recoverable === true;
|
|
38
|
+
let fatal = !recoverable;
|
|
39
|
+
let reason = deriveReason(code, stage, statusCode);
|
|
40
|
+
let cooldownOverrideMs;
|
|
41
|
+
if (statusCode === 401 || statusCode === 402 || statusCode === 403 || code.includes('AUTH')) {
|
|
42
|
+
fatal = true;
|
|
43
|
+
cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
|
|
44
|
+
reason = 'auth';
|
|
45
|
+
}
|
|
46
|
+
else if (statusCode === 429 && !recoverable) {
|
|
47
|
+
fatal = true;
|
|
48
|
+
cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
|
|
49
|
+
reason = 'rate_limit';
|
|
50
|
+
}
|
|
51
|
+
else if (statusCode && statusCode >= 500) {
|
|
52
|
+
fatal = true;
|
|
53
|
+
cooldownOverrideMs = Math.max(5 * 60_000, healthConfig.fatalCooldownMs ?? 5 * 60_000);
|
|
54
|
+
reason = 'upstream_error';
|
|
55
|
+
}
|
|
56
|
+
else if (stage.includes('compat')) {
|
|
57
|
+
fatal = true;
|
|
58
|
+
cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
|
|
59
|
+
reason = 'compatibility';
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
providerKey,
|
|
63
|
+
routeName,
|
|
64
|
+
reason,
|
|
65
|
+
fatal,
|
|
66
|
+
statusCode,
|
|
67
|
+
errorCode: code,
|
|
68
|
+
retryable: recoverable,
|
|
69
|
+
affectsHealth: event.affectsHealth !== false,
|
|
70
|
+
cooldownOverrideMs,
|
|
71
|
+
metadata: {
|
|
72
|
+
...event.runtime,
|
|
73
|
+
stage,
|
|
74
|
+
eventCode: code,
|
|
75
|
+
originalMessage: event.message,
|
|
76
|
+
statusCode
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function applySeriesCooldownImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
|
|
81
|
+
const seriesDetail = extractSeriesCooldownDetail(event);
|
|
82
|
+
if (!seriesDetail) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const targetKeys = resolveSeriesCooldownTargets(seriesDetail, event, providerRegistry);
|
|
86
|
+
if (targetKeys.length === 0) {
|
|
87
|
+
debug?.log?.('[virtual-router] series cooldown skipped: no targets', {
|
|
88
|
+
providerId: seriesDetail.providerId,
|
|
89
|
+
providerKey: seriesDetail.providerKey,
|
|
90
|
+
series: seriesDetail.series
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const affected = [];
|
|
95
|
+
for (const providerKey of targetKeys) {
|
|
96
|
+
try {
|
|
97
|
+
const profile = providerRegistry.get(providerKey);
|
|
98
|
+
const modelSeries = resolveModelSeries(profile.modelId);
|
|
99
|
+
if (modelSeries !== seriesDetail.series) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
healthManager.tripProvider(providerKey, 'rate_limit', seriesDetail.cooldownMs);
|
|
103
|
+
markProviderCooldown(providerKey, seriesDetail.cooldownMs);
|
|
104
|
+
affected.push(providerKey);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore lookup failures; invalid keys may show up if config drifted
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (affected.length) {
|
|
111
|
+
debug?.log?.('[virtual-router] series cooldown', {
|
|
112
|
+
providerId: seriesDetail.providerId,
|
|
113
|
+
providerKey: seriesDetail.providerKey,
|
|
114
|
+
series: seriesDetail.series,
|
|
115
|
+
cooldownMs: seriesDetail.cooldownMs,
|
|
116
|
+
affected
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function resolveSeriesCooldownTargets(detail, event, providerRegistry) {
|
|
121
|
+
const candidates = new Set();
|
|
122
|
+
const push = (key) => {
|
|
123
|
+
if (typeof key !== 'string') {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const trimmed = key.trim();
|
|
127
|
+
if (!trimmed) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (providerRegistry.has(trimmed)) {
|
|
131
|
+
candidates.add(trimmed);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
push(detail.providerKey);
|
|
135
|
+
const runtimeKey = (event.runtime?.target && typeof event.runtime.target === 'object'
|
|
136
|
+
? event.runtime.target.providerKey
|
|
137
|
+
: undefined) || event.runtime?.providerKey;
|
|
138
|
+
push(runtimeKey);
|
|
139
|
+
return Array.from(candidates);
|
|
140
|
+
}
|
|
141
|
+
function extractSeriesCooldownDetail(event) {
|
|
142
|
+
if (!event || !event.details || typeof event.details !== 'object') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const raw = event.details[SERIES_COOLDOWN_DETAIL_KEY];
|
|
146
|
+
if (!raw || typeof raw !== 'object') {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const record = raw;
|
|
150
|
+
const providerIdRaw = record.providerId;
|
|
151
|
+
const seriesRaw = record.series;
|
|
152
|
+
const providerKeyRaw = record.providerKey;
|
|
153
|
+
const cooldownRaw = record.cooldownMs;
|
|
154
|
+
if (typeof providerIdRaw !== 'string' || !providerIdRaw.trim()) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const normalizedSeries = typeof seriesRaw === 'string' ? seriesRaw.trim().toLowerCase() : '';
|
|
158
|
+
if (normalizedSeries !== 'gemini-pro' && normalizedSeries !== 'gemini-flash' && normalizedSeries !== 'claude') {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const cooldownMs = typeof cooldownRaw === 'number'
|
|
162
|
+
? cooldownRaw
|
|
163
|
+
: typeof cooldownRaw === 'string'
|
|
164
|
+
? Number.parseFloat(cooldownRaw)
|
|
165
|
+
: Number.NaN;
|
|
166
|
+
if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
providerId: providerIdRaw.trim(),
|
|
171
|
+
...(typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length
|
|
172
|
+
? { providerKey: providerKeyRaw.trim() }
|
|
173
|
+
: {}),
|
|
174
|
+
series: normalizedSeries,
|
|
175
|
+
cooldownMs: Math.round(cooldownMs)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function deriveReason(code, stage, statusCode) {
|
|
179
|
+
if (code.includes('RATE') || code.includes('429'))
|
|
180
|
+
return 'rate_limit';
|
|
181
|
+
if (code.includes('AUTH') || statusCode === 401 || statusCode === 403)
|
|
182
|
+
return 'auth';
|
|
183
|
+
if (stage.includes('compat'))
|
|
184
|
+
return 'compatibility';
|
|
185
|
+
if (code.includes('SSE'))
|
|
186
|
+
return 'sse';
|
|
187
|
+
if (code.includes('TIMEOUT') || statusCode === 408 || statusCode === 504)
|
|
188
|
+
return 'timeout';
|
|
189
|
+
if (statusCode && statusCode >= 500)
|
|
190
|
+
return 'upstream_error';
|
|
191
|
+
if (statusCode && statusCode >= 400)
|
|
192
|
+
return 'client_error';
|
|
193
|
+
return 'unknown';
|
|
194
|
+
}
|
|
195
|
+
function resolveModelSeries(modelId) {
|
|
196
|
+
if (!modelId) {
|
|
197
|
+
return 'default';
|
|
198
|
+
}
|
|
199
|
+
const lower = modelId.toLowerCase();
|
|
200
|
+
if (lower.includes('claude') || lower.includes('opus')) {
|
|
201
|
+
return 'claude';
|
|
202
|
+
}
|
|
203
|
+
if (lower.includes('flash')) {
|
|
204
|
+
return 'gemini-flash';
|
|
205
|
+
}
|
|
206
|
+
if (lower.includes('gemini') || lower.includes('pro')) {
|
|
207
|
+
return 'gemini-pro';
|
|
208
|
+
}
|
|
209
|
+
return 'default';
|
|
210
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ClassificationResult, type RoutingFeatures, type RoutingInstructionMode, type VirtualRouterContextRoutingConfig } from './types.js';
|
|
2
|
+
import { ProviderRegistry } from './provider-registry.js';
|
|
3
|
+
type LoggingDeps = {
|
|
4
|
+
providerRegistry: ProviderRegistry;
|
|
5
|
+
contextRouting: VirtualRouterContextRoutingConfig | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare function formatStickyScope(scope?: string): string | undefined;
|
|
8
|
+
export declare function parseProviderKey(providerKey: string): {
|
|
9
|
+
providerId: string;
|
|
10
|
+
keyAlias?: string;
|
|
11
|
+
modelId?: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function describeTargetProvider(providerKey: string, fallbackModelId?: string): {
|
|
14
|
+
providerLabel: string;
|
|
15
|
+
resolvedModel?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildHitReason(routeUsed: string, providerKey: string, classification: ClassificationResult, features: RoutingFeatures, mode: RoutingInstructionMode | undefined, deps: LoggingDeps): string;
|
|
18
|
+
export declare function formatVirtualRouterHit(routeName: string, poolId: string | undefined, providerKey: string, modelId?: string, hitReason?: string, stickyScope?: string): string;
|
|
19
|
+
export {};
|