@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
|
@@ -20,6 +20,28 @@ export function extractMessageText(message) {
|
|
|
20
20
|
if (typeof message.content === 'string' && message.content.trim()) {
|
|
21
21
|
return message.content;
|
|
22
22
|
}
|
|
23
|
+
const content = message.content;
|
|
24
|
+
if (Array.isArray(content)) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
for (const entry of content) {
|
|
27
|
+
if (typeof entry === 'string' && entry.trim()) {
|
|
28
|
+
parts.push(entry);
|
|
29
|
+
}
|
|
30
|
+
else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
31
|
+
const record = entry;
|
|
32
|
+
if (typeof record.text === 'string' && record.text.trim()) {
|
|
33
|
+
parts.push(record.text);
|
|
34
|
+
}
|
|
35
|
+
else if (typeof record.content === 'string' && record.content.trim()) {
|
|
36
|
+
parts.push(record.content);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const joined = parts.join('\n').trim();
|
|
41
|
+
if (joined) {
|
|
42
|
+
return joined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
23
45
|
return '';
|
|
24
46
|
}
|
|
25
47
|
export function detectKeyword(text, keywords) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { StandardizedMessage } from '../../conversion/hub/types/standardized.js';
|
|
2
2
|
export interface RoutingInstruction {
|
|
3
|
-
type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow';
|
|
3
|
+
type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow' | 'stopMessageSet' | 'stopMessageClear';
|
|
4
4
|
provider?: string;
|
|
5
5
|
keyAlias?: string;
|
|
6
6
|
keyIndex?: number;
|
|
7
7
|
model?: string;
|
|
8
8
|
pathLength?: number;
|
|
9
|
+
stopMessageText?: string;
|
|
10
|
+
stopMessageMaxRepeats?: number;
|
|
9
11
|
}
|
|
10
12
|
export interface RoutingInstructionState {
|
|
11
13
|
forcedTarget?: {
|
|
@@ -26,6 +28,11 @@ export interface RoutingInstructionState {
|
|
|
26
28
|
disabledProviders: Set<string>;
|
|
27
29
|
disabledKeys: Map<string, Set<string | number>>;
|
|
28
30
|
disabledModels: Map<string, Set<string>>;
|
|
31
|
+
stopMessageText?: string;
|
|
32
|
+
stopMessageMaxRepeats?: number;
|
|
33
|
+
stopMessageUsed?: number;
|
|
34
|
+
stopMessageUpdatedAt?: number;
|
|
35
|
+
stopMessageLastUsedAt?: number;
|
|
29
36
|
}
|
|
30
37
|
export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
|
|
31
38
|
export declare function applyRoutingInstructions(instructions: RoutingInstruction[], currentState: RoutingInstructionState): RoutingInstructionState;
|
|
@@ -49,6 +49,11 @@ function expandInstructionSegments(instruction) {
|
|
|
49
49
|
if (!trimmed) {
|
|
50
50
|
return [];
|
|
51
51
|
}
|
|
52
|
+
// stopMessage 指令需要整体解析,不能按逗号拆分,否则类似
|
|
53
|
+
// "<**stopMessage:\"继续\",3**>" 会被错误拆成 ["stopMessage:\"继续\"", "3"]。
|
|
54
|
+
if (/^stopMessage\s*:/i.test(trimmed)) {
|
|
55
|
+
return [trimmed];
|
|
56
|
+
}
|
|
52
57
|
const prefix = trimmed[0];
|
|
53
58
|
if (prefix === '!' || prefix === '#' || prefix === '@') {
|
|
54
59
|
const tokens = splitInstructionTargets(trimmed.substring(1));
|
|
@@ -69,6 +74,74 @@ function parseSingleInstruction(instruction) {
|
|
|
69
74
|
if (instruction === 'clear') {
|
|
70
75
|
return { type: 'clear' };
|
|
71
76
|
}
|
|
77
|
+
if (/^stopMessage\s*:/i.test(instruction)) {
|
|
78
|
+
const body = instruction.slice('stopMessage'.length + 1).trim();
|
|
79
|
+
if (!body) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (/^clear$/i.test(body)) {
|
|
83
|
+
return { type: 'stopMessageClear' };
|
|
84
|
+
}
|
|
85
|
+
let text = '';
|
|
86
|
+
let maxRepeats = 1;
|
|
87
|
+
let cursor = body;
|
|
88
|
+
if (cursor[0] === '"') {
|
|
89
|
+
let escaped = false;
|
|
90
|
+
let endIndex = -1;
|
|
91
|
+
for (let i = 1; i < cursor.length; i += 1) {
|
|
92
|
+
const ch = cursor[i];
|
|
93
|
+
if (escaped) {
|
|
94
|
+
escaped = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (ch === '\\') {
|
|
98
|
+
escaped = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === '"') {
|
|
102
|
+
endIndex = i;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (endIndex <= 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const rawText = cursor.slice(1, endIndex);
|
|
110
|
+
text = rawText.replace(/\\"/g, '"');
|
|
111
|
+
cursor = cursor.slice(endIndex + 1).trim();
|
|
112
|
+
if (cursor.startsWith(',')) {
|
|
113
|
+
const countRaw = cursor.slice(1).trim();
|
|
114
|
+
if (countRaw) {
|
|
115
|
+
const parsed = Number.parseInt(countRaw, 10);
|
|
116
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
117
|
+
maxRepeats = parsed;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// 支持无引号的简单形式:stopMessage:继续,3
|
|
124
|
+
const parts = cursor.split(',').map((part) => part.trim()).filter(Boolean);
|
|
125
|
+
if (!parts.length) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
text = parts[0];
|
|
129
|
+
if (parts.length > 1) {
|
|
130
|
+
const parsed = Number.parseInt(parts[1], 10);
|
|
131
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
132
|
+
maxRepeats = parsed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!text) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
type: 'stopMessageSet',
|
|
141
|
+
stopMessageText: text,
|
|
142
|
+
stopMessageMaxRepeats: maxRepeats
|
|
143
|
+
};
|
|
144
|
+
}
|
|
72
145
|
if (instruction.startsWith('!')) {
|
|
73
146
|
const target = instruction.substring(1).trim();
|
|
74
147
|
if (!target) {
|
|
@@ -199,7 +272,12 @@ export function applyRoutingInstructions(instructions, currentState) {
|
|
|
199
272
|
allowedProviders: new Set(currentState.allowedProviders),
|
|
200
273
|
disabledProviders: new Set(currentState.disabledProviders),
|
|
201
274
|
disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
|
|
202
|
-
disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)]))
|
|
275
|
+
disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)])),
|
|
276
|
+
stopMessageText: currentState.stopMessageText,
|
|
277
|
+
stopMessageMaxRepeats: currentState.stopMessageMaxRepeats,
|
|
278
|
+
stopMessageUsed: currentState.stopMessageUsed,
|
|
279
|
+
stopMessageUpdatedAt: currentState.stopMessageUpdatedAt,
|
|
280
|
+
stopMessageLastUsedAt: currentState.stopMessageLastUsedAt
|
|
203
281
|
};
|
|
204
282
|
let allowReset = false;
|
|
205
283
|
let disableReset = false;
|
|
@@ -312,6 +390,29 @@ export function applyRoutingInstructions(instructions, currentState) {
|
|
|
312
390
|
newState.disabledKeys.clear();
|
|
313
391
|
newState.disabledModels.clear();
|
|
314
392
|
break;
|
|
393
|
+
case 'stopMessageSet': {
|
|
394
|
+
const text = typeof instruction.stopMessageText === 'string' && instruction.stopMessageText.trim()
|
|
395
|
+
? instruction.stopMessageText.trim()
|
|
396
|
+
: '';
|
|
397
|
+
const maxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
|
|
398
|
+
? Math.floor(instruction.stopMessageMaxRepeats)
|
|
399
|
+
: 0;
|
|
400
|
+
if (text && maxRepeats > 0) {
|
|
401
|
+
newState.stopMessageText = text;
|
|
402
|
+
newState.stopMessageMaxRepeats = maxRepeats;
|
|
403
|
+
newState.stopMessageUsed = 0;
|
|
404
|
+
newState.stopMessageUpdatedAt = Date.now();
|
|
405
|
+
newState.stopMessageLastUsedAt = undefined;
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case 'stopMessageClear':
|
|
410
|
+
newState.stopMessageText = undefined;
|
|
411
|
+
newState.stopMessageMaxRepeats = undefined;
|
|
412
|
+
newState.stopMessageUsed = undefined;
|
|
413
|
+
newState.stopMessageUpdatedAt = undefined;
|
|
414
|
+
newState.stopMessageLastUsedAt = undefined;
|
|
415
|
+
break;
|
|
315
416
|
}
|
|
316
417
|
}
|
|
317
418
|
return newState;
|
|
@@ -351,7 +452,22 @@ export function serializeRoutingInstructionState(state) {
|
|
|
351
452
|
disabledModels: Array.from(state.disabledModels.entries()).map(([provider, models]) => ({
|
|
352
453
|
provider,
|
|
353
454
|
models: Array.from(models)
|
|
354
|
-
}))
|
|
455
|
+
})),
|
|
456
|
+
...(typeof state.stopMessageText === 'string' && state.stopMessageText.trim()
|
|
457
|
+
? { stopMessageText: state.stopMessageText }
|
|
458
|
+
: {}),
|
|
459
|
+
...(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
|
|
460
|
+
? { stopMessageMaxRepeats: state.stopMessageMaxRepeats }
|
|
461
|
+
: {}),
|
|
462
|
+
...(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
|
|
463
|
+
? { stopMessageUsed: state.stopMessageUsed }
|
|
464
|
+
: {}),
|
|
465
|
+
...(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)
|
|
466
|
+
? { stopMessageUpdatedAt: state.stopMessageUpdatedAt }
|
|
467
|
+
: {}),
|
|
468
|
+
...(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
|
|
469
|
+
? { stopMessageLastUsedAt: state.stopMessageLastUsedAt }
|
|
470
|
+
: {})
|
|
355
471
|
};
|
|
356
472
|
}
|
|
357
473
|
export function deserializeRoutingInstructionState(data) {
|
|
@@ -361,7 +477,10 @@ export function deserializeRoutingInstructionState(data) {
|
|
|
361
477
|
allowedProviders: new Set(),
|
|
362
478
|
disabledProviders: new Set(),
|
|
363
479
|
disabledKeys: new Map(),
|
|
364
|
-
disabledModels: new Map()
|
|
480
|
+
disabledModels: new Map(),
|
|
481
|
+
stopMessageText: undefined,
|
|
482
|
+
stopMessageMaxRepeats: undefined,
|
|
483
|
+
stopMessageUsed: undefined
|
|
365
484
|
};
|
|
366
485
|
if (data.forcedTarget && typeof data.forcedTarget === 'object') {
|
|
367
486
|
state.forcedTarget = data.forcedTarget;
|
|
@@ -389,5 +508,20 @@ export function deserializeRoutingInstructionState(data) {
|
|
|
389
508
|
}
|
|
390
509
|
}
|
|
391
510
|
}
|
|
511
|
+
if (typeof data.stopMessageText === 'string' && data.stopMessageText.trim()) {
|
|
512
|
+
state.stopMessageText = data.stopMessageText;
|
|
513
|
+
}
|
|
514
|
+
if (typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats)) {
|
|
515
|
+
state.stopMessageMaxRepeats = Math.floor(data.stopMessageMaxRepeats);
|
|
516
|
+
}
|
|
517
|
+
if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
|
|
518
|
+
state.stopMessageUsed = Math.max(0, Math.floor(data.stopMessageUsed));
|
|
519
|
+
}
|
|
520
|
+
if (typeof data.stopMessageUpdatedAt === 'number' && Number.isFinite(data.stopMessageUpdatedAt)) {
|
|
521
|
+
state.stopMessageUpdatedAt = data.stopMessageUpdatedAt;
|
|
522
|
+
}
|
|
523
|
+
if (typeof data.stopMessageLastUsedAt === 'number' && Number.isFinite(data.stopMessageLastUsedAt)) {
|
|
524
|
+
state.stopMessageLastUsedAt = data.stopMessageLastUsedAt;
|
|
525
|
+
}
|
|
392
526
|
return state;
|
|
393
527
|
}
|
|
@@ -101,7 +101,25 @@ const SHELL_READ_PATTERNS = [
|
|
|
101
101
|
'python - <<',
|
|
102
102
|
'python -c',
|
|
103
103
|
'node - <<',
|
|
104
|
-
'node -e'
|
|
104
|
+
'node -e',
|
|
105
|
+
'sed -n',
|
|
106
|
+
'sed --quiet',
|
|
107
|
+
'sed ',
|
|
108
|
+
'rg ',
|
|
109
|
+
' ripgrep',
|
|
110
|
+
'grep ',
|
|
111
|
+
'egrep ',
|
|
112
|
+
'fgrep ',
|
|
113
|
+
'ag ',
|
|
114
|
+
'ack ',
|
|
115
|
+
'find ',
|
|
116
|
+
'nl ',
|
|
117
|
+
'less',
|
|
118
|
+
'more',
|
|
119
|
+
'awk ',
|
|
120
|
+
'perl -ne',
|
|
121
|
+
'perl -pe',
|
|
122
|
+
'strings '
|
|
105
123
|
];
|
|
106
124
|
export function detectVisionTool(request) {
|
|
107
125
|
if (!Array.isArray(request.tools)) {
|
|
@@ -197,21 +215,49 @@ function classifyToolCall(call) {
|
|
|
197
215
|
}
|
|
198
216
|
const argsObject = parseToolArguments(call?.function?.arguments);
|
|
199
217
|
const commandText = extractCommandText(argsObject);
|
|
200
|
-
const nameCategory = categorizeToolName(functionName);
|
|
201
218
|
const snippet = buildCommandSnippet(commandText);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
const normalizedName = functionName.toLowerCase();
|
|
220
|
+
const normalizedCmd = commandText.toLowerCase();
|
|
221
|
+
// 1) Web search 优先:函数名或命令文本中命中 web 搜索关键字时,一律归类为 search,优先级最高。
|
|
222
|
+
const isWebSearch = WEB_TOOL_KEYWORDS.some((keyword) => normalizedName.includes(keyword)) ||
|
|
223
|
+
WEB_TOOL_KEYWORDS.some((keyword) => normalizedCmd.includes(keyword));
|
|
224
|
+
// 2) 基于工具名的初步分类(read / write / search / other)
|
|
225
|
+
const nameCategory = categorizeToolName(functionName);
|
|
226
|
+
// 3) shell_command / exec_command 根据内部命令判断读写性质
|
|
227
|
+
let shellCategory = 'other';
|
|
228
|
+
if (SHELL_TOOL_NAMES.has(functionName) || functionName === 'exec_command') {
|
|
229
|
+
shellCategory = classifyShellCommand(commandText);
|
|
230
|
+
}
|
|
231
|
+
// 按优先级合并分类结果:
|
|
232
|
+
// 1. web search
|
|
233
|
+
// 2. 写文件(任一维度命中写)
|
|
234
|
+
// 3. 读文件(任一维度命中读)
|
|
235
|
+
// 4. 其他搜索(非 web search)
|
|
236
|
+
// 5. 其它工具
|
|
237
|
+
// Priority 1: Web search
|
|
238
|
+
if (isWebSearch) {
|
|
239
|
+
return { category: 'search', name: functionName, commandSnippet: snippet };
|
|
240
|
+
}
|
|
241
|
+
// Priority 2: Write (写文件) — 名称或内部命令任一判断为写,都按写处理
|
|
242
|
+
if (nameCategory === 'write' || shellCategory === 'write') {
|
|
243
|
+
return { category: 'write', name: functionName, commandSnippet: snippet };
|
|
244
|
+
}
|
|
245
|
+
// Priority 3: Read (读文件) — 仅在没有写的情况下,再看读
|
|
246
|
+
if (nameCategory === 'read' || shellCategory === 'read') {
|
|
247
|
+
return { category: 'read', name: functionName, commandSnippet: snippet };
|
|
248
|
+
}
|
|
249
|
+
// Priority 4: 其他 search 类工具(非 web search)
|
|
250
|
+
if (nameCategory === 'search') {
|
|
251
|
+
return { category: 'search', name: functionName, commandSnippet: snippet };
|
|
252
|
+
}
|
|
253
|
+
// Priority 5: 兜底用命令文本再判断一次 shell 风格读写(非 shell/exec_command 的工具)
|
|
254
|
+
if (!SHELL_TOOL_NAMES.has(functionName) && functionName !== 'exec_command' && commandText) {
|
|
210
255
|
const derivedCategory = classifyShellCommand(commandText);
|
|
211
|
-
if (derivedCategory
|
|
256
|
+
if (derivedCategory === 'write' || derivedCategory === 'read') {
|
|
212
257
|
return { category: derivedCategory, name: functionName, commandSnippet: snippet };
|
|
213
258
|
}
|
|
214
259
|
}
|
|
260
|
+
// 最终兜底:other
|
|
215
261
|
return { category: 'other', name: functionName, commandSnippet: snippet };
|
|
216
262
|
}
|
|
217
263
|
function extractToolName(tool) {
|
|
@@ -172,6 +172,11 @@ export interface RouterMetadataInput {
|
|
|
172
172
|
* 强制路由模式,从消息中的 <**...**> 指令解析得出
|
|
173
173
|
*/
|
|
174
174
|
routingMode?: RoutingInstructionMode;
|
|
175
|
+
/**
|
|
176
|
+
* 当 disableStickyRoutes=true 时,本次请求仍使用 sticky session 状态,
|
|
177
|
+
* 但不继承 sticky target,允许后续路由重新选择 provider。
|
|
178
|
+
*/
|
|
179
|
+
disableStickyRoutes?: boolean;
|
|
175
180
|
/**
|
|
176
181
|
* 允许的 provider 白名单
|
|
177
182
|
*/
|
|
@@ -343,3 +348,28 @@ export interface ProviderErrorEvent {
|
|
|
343
348
|
export interface FeatureBuilder {
|
|
344
349
|
build(request: StandardizedRequest, metadata: RouterMetadataInput): RoutingFeatures;
|
|
345
350
|
}
|
|
351
|
+
export interface ProviderCooldownState {
|
|
352
|
+
providerKey: string;
|
|
353
|
+
cooldownExpiresAt: number;
|
|
354
|
+
reason?: string;
|
|
355
|
+
}
|
|
356
|
+
export interface VirtualRouterHealthSnapshot {
|
|
357
|
+
providers: ProviderHealthState[];
|
|
358
|
+
cooldowns: ProviderCooldownState[];
|
|
359
|
+
}
|
|
360
|
+
export interface VirtualRouterHealthStore {
|
|
361
|
+
/**
|
|
362
|
+
* 在 VirtualRouterEngine 初始化时提供上一次持久化的健康快照。
|
|
363
|
+
* 调用方应仅返回仍在有效期内的 cooldown/熔断信息,或返回 null 表示无可恢复状态。
|
|
364
|
+
*/
|
|
365
|
+
loadInitialSnapshot(): VirtualRouterHealthSnapshot | null;
|
|
366
|
+
/**
|
|
367
|
+
* 当 VirtualRouterEngine 更新 provider 健康状态或 cooldown 时,可选地持久化最新快照。
|
|
368
|
+
* 实现应保证内部吞掉 I/O 错误,不影响路由主流程。
|
|
369
|
+
*/
|
|
370
|
+
persistSnapshot?(snapshot: VirtualRouterHealthSnapshot): void;
|
|
371
|
+
/**
|
|
372
|
+
* 可选:记录原始 ProviderErrorEvent,便于后续离线统计与诊断。
|
|
373
|
+
*/
|
|
374
|
+
recordProviderError?(event: ProviderErrorEvent): void;
|
|
375
|
+
}
|
|
@@ -94,6 +94,9 @@ function resolveRouteHint(adapterContext, flowId) {
|
|
|
94
94
|
if (!routeId) {
|
|
95
95
|
return undefined;
|
|
96
96
|
}
|
|
97
|
+
if (routeId.toLowerCase() === 'default') {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
97
100
|
if (flowId && routeId.toLowerCase() === flowId.toLowerCase()) {
|
|
98
101
|
return undefined;
|
|
99
102
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { registerServerToolHandler } from '../registry.js';
|
|
2
|
+
import { cloneJson } from '../server-side-tools.js';
|
|
3
|
+
const FLOW_ID = 'gemini_empty_reply_continue';
|
|
4
|
+
const handler = async (ctx) => {
|
|
5
|
+
if (!ctx.options.reenterPipeline) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
// 避免在 followup 请求里再次触发,防止循环。
|
|
9
|
+
const adapterRecord = ctx.adapterContext;
|
|
10
|
+
const followupRaw = adapterRecord.serverToolFollowup;
|
|
11
|
+
if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
// 仅针对 gemini-chat 协议 + antigravity.* providerKey 的 /v1/responses 路径启用。
|
|
15
|
+
if (ctx.options.providerProtocol !== 'gemini-chat') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const entryEndpoint = (ctx.options.entryEndpoint || '').toLowerCase();
|
|
19
|
+
if (!entryEndpoint.includes('/v1/responses')) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const providerKey = typeof adapterRecord.providerKey === 'string' && adapterRecord.providerKey.trim()
|
|
23
|
+
? adapterRecord.providerKey.trim().toLowerCase()
|
|
24
|
+
: '';
|
|
25
|
+
if (!providerKey.startsWith('antigravity.')) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// 仅在 finish_reason=stop 且第一条消息内容为空、无 tool_calls 时触发。
|
|
29
|
+
const base = ctx.base;
|
|
30
|
+
const choices = Array.isArray(base.choices) ? base.choices : [];
|
|
31
|
+
if (!choices.length) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const firstRaw = choices[0];
|
|
35
|
+
if (!firstRaw || typeof firstRaw !== 'object') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const first = firstRaw;
|
|
39
|
+
const finishReason = typeof first.finish_reason === 'string' && first.finish_reason.trim()
|
|
40
|
+
? first.finish_reason.trim()
|
|
41
|
+
: '';
|
|
42
|
+
if (finishReason !== 'stop') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const message = first.message && typeof first.message === 'object' && !Array.isArray(first.message)
|
|
46
|
+
? first.message
|
|
47
|
+
: null;
|
|
48
|
+
if (!message) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const contentRaw = message.content;
|
|
52
|
+
const contentText = typeof contentRaw === 'string'
|
|
53
|
+
? contentRaw.trim()
|
|
54
|
+
: '';
|
|
55
|
+
if (contentText.length > 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
59
|
+
if (toolCalls.length > 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const captured = getCapturedRequest(ctx.adapterContext);
|
|
63
|
+
if (!captured) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const followupPayload = buildContinueFollowupPayload(captured);
|
|
67
|
+
if (!followupPayload) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
chatResponse: ctx.base,
|
|
72
|
+
execution: {
|
|
73
|
+
flowId: FLOW_ID,
|
|
74
|
+
followup: {
|
|
75
|
+
requestIdSuffix: ':continue',
|
|
76
|
+
payload: followupPayload,
|
|
77
|
+
metadata: {
|
|
78
|
+
serverToolFollowup: true,
|
|
79
|
+
stream: false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
registerServerToolHandler('gemini_empty_reply_continue', handler, { trigger: 'auto' });
|
|
86
|
+
function getCapturedRequest(adapterContext) {
|
|
87
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const captured = adapterContext.capturedChatRequest;
|
|
91
|
+
if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return captured;
|
|
95
|
+
}
|
|
96
|
+
function buildContinueFollowupPayload(source) {
|
|
97
|
+
if (!source || typeof source !== 'object') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const payload = {};
|
|
101
|
+
if (typeof source.model === 'string' && source.model.trim()) {
|
|
102
|
+
payload.model = source.model.trim();
|
|
103
|
+
}
|
|
104
|
+
const rawMessages = source.messages;
|
|
105
|
+
const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
|
|
106
|
+
messages.push({
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: '继续'
|
|
109
|
+
});
|
|
110
|
+
payload.messages = messages;
|
|
111
|
+
if (Array.isArray(source.tools) && source.tools.length) {
|
|
112
|
+
payload.tools = cloneJson(source.tools);
|
|
113
|
+
}
|
|
114
|
+
const parameters = source.parameters;
|
|
115
|
+
if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
|
|
116
|
+
const params = cloneJson(parameters);
|
|
117
|
+
Object.assign(payload, params);
|
|
118
|
+
}
|
|
119
|
+
return payload;
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { registerServerToolHandler } from '../registry.js';
|
|
2
|
+
import { cloneJson } from '../server-side-tools.js';
|
|
3
|
+
const FLOW_ID = 'iflow_model_error_retry';
|
|
4
|
+
const handler = async (ctx) => {
|
|
5
|
+
if (!ctx.options.reenterPipeline) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const adapterRecord = ctx.adapterContext;
|
|
9
|
+
// 避免在 followup 请求里再次触发,防止循环。
|
|
10
|
+
const followupRaw = adapterRecord.serverToolFollowup;
|
|
11
|
+
if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
// 仅针对 openai-chat 协议 + iflow.* providerKey 的 /v1/responses 路径启用。
|
|
15
|
+
if (ctx.options.providerProtocol !== 'openai-chat') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const entryEndpoint = (ctx.options.entryEndpoint || '').toLowerCase();
|
|
19
|
+
if (!entryEndpoint.includes('/v1/responses')) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const providerKey = typeof adapterRecord.providerKey === 'string' && adapterRecord.providerKey.trim()
|
|
23
|
+
? adapterRecord.providerKey.trim().toLowerCase()
|
|
24
|
+
: '';
|
|
25
|
+
if (!providerKey.startsWith('iflow.')) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// 仅在上游返回 error_code(HTTP 200 + 业务错误)时触发一次自动重试。
|
|
29
|
+
const base = ctx.base;
|
|
30
|
+
const errorCode = base.error_code;
|
|
31
|
+
const msg = base.msg;
|
|
32
|
+
if (typeof errorCode !== 'number' || errorCode === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (typeof msg !== 'string' || !msg.trim().length) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const captured = getCapturedRequest(ctx.adapterContext);
|
|
39
|
+
if (!captured) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const followupPayload = buildRetryFollowupPayload(captured);
|
|
43
|
+
if (!followupPayload) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
chatResponse: ctx.base,
|
|
48
|
+
execution: {
|
|
49
|
+
flowId: FLOW_ID,
|
|
50
|
+
followup: {
|
|
51
|
+
requestIdSuffix: ':retry',
|
|
52
|
+
payload: followupPayload,
|
|
53
|
+
metadata: {
|
|
54
|
+
serverToolFollowup: true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
registerServerToolHandler('iflow_model_error_retry', handler, { trigger: 'auto' });
|
|
61
|
+
function getCapturedRequest(adapterContext) {
|
|
62
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const captured = adapterContext.capturedChatRequest;
|
|
66
|
+
if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return captured;
|
|
70
|
+
}
|
|
71
|
+
function buildRetryFollowupPayload(source) {
|
|
72
|
+
if (!source || typeof source !== 'object') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const payload = {};
|
|
76
|
+
if (typeof source.model === 'string' && source.model.trim()) {
|
|
77
|
+
payload.model = source.model.trim();
|
|
78
|
+
}
|
|
79
|
+
const rawMessages = source.messages;
|
|
80
|
+
if (Array.isArray(rawMessages)) {
|
|
81
|
+
payload.messages = cloneJson(rawMessages);
|
|
82
|
+
}
|
|
83
|
+
const rawTools = source.tools;
|
|
84
|
+
if (Array.isArray(rawTools) && rawTools.length) {
|
|
85
|
+
payload.tools = cloneJson(rawTools);
|
|
86
|
+
}
|
|
87
|
+
const parameters = source.parameters;
|
|
88
|
+
if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
|
|
89
|
+
const params = cloneJson(parameters);
|
|
90
|
+
Object.assign(payload, params);
|
|
91
|
+
}
|
|
92
|
+
return payload;
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|