@jsonstudio/llms 0.6.568 → 0.6.626
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/profiles/chat-gemini.json +15 -15
- 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 +9 -10
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +0 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +68 -69
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
- package/dist/conversion/hub/process/chat-process.js +37 -16
- package/dist/conversion/hub/response/provider-response.js +0 -8
- package/dist/conversion/hub/response/response-runtime.js +47 -1
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +59 -4
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +8 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +93 -12
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +208 -31
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +280 -14
- package/dist/conversion/hub/standardized-bridge.js +11 -2
- package/dist/conversion/hub/types/chat-envelope.d.ts +10 -0
- package/dist/conversion/hub/types/standardized.d.ts +2 -1
- package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
- package/dist/conversion/responses/responses-openai-bridge.js +1 -13
- package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
- package/dist/conversion/shared/text-markup-normalizer.js +84 -5
- package/dist/conversion/shared/tool-filter-pipeline.d.ts +1 -1
- package/dist/conversion/shared/tool-filter-pipeline.js +54 -29
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/special/response-apply-patch-toon-decode.js +15 -7
- package/dist/filters/special/response-tool-arguments-toon-decode.js +108 -22
- package/dist/guidance/index.js +2 -0
- package/dist/router/virtual-router/classifier.js +16 -12
- package/dist/router/virtual-router/engine.js +45 -4
- package/dist/router/virtual-router/tool-signals.d.ts +2 -1
- package/dist/router/virtual-router/tool-signals.js +293 -134
- package/dist/router/virtual-router/types.d.ts +1 -1
- package/dist/router/virtual-router/types.js +1 -1
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +28 -4
- package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +7 -3
- package/dist/tools/apply-patch-structured.js +4 -3
- package/package.json +2 -2
|
@@ -134,42 +134,67 @@ function applyLocalToolGovernance(chatRequest, rawPayload) {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
function detectImageHint(messages, rawPayload) {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
if (typeof value
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
const patterns = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
|
|
138
|
+
const hasImageExt = (value) => {
|
|
139
|
+
if (typeof value !== 'string' || !value)
|
|
140
|
+
return false;
|
|
141
|
+
const lower = value.toLowerCase();
|
|
142
|
+
return patterns.some(ext => lower.includes(ext));
|
|
142
143
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
144
|
+
const hasImageInMessage = (msg) => {
|
|
145
|
+
if (!msg || typeof msg !== 'object')
|
|
146
|
+
return false;
|
|
147
|
+
const m = msg;
|
|
148
|
+
const content = m.content;
|
|
149
|
+
if (typeof content === 'string') {
|
|
150
|
+
if (hasImageExt(content))
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
else if (Array.isArray(content)) {
|
|
154
|
+
for (const part of content) {
|
|
155
|
+
if (!part || typeof part !== 'object')
|
|
156
|
+
continue;
|
|
157
|
+
const p = part;
|
|
158
|
+
const t = String(p.type || '').toLowerCase();
|
|
159
|
+
if (t.includes('image')) {
|
|
160
|
+
return true;
|
|
156
161
|
}
|
|
162
|
+
if (hasImageExt(p.text))
|
|
163
|
+
return true;
|
|
164
|
+
const imageUrl = typeof p.image_url === 'string'
|
|
165
|
+
? p.image_url
|
|
166
|
+
: p.image_url && typeof p.image_url.url === 'string'
|
|
167
|
+
? p.image_url.url
|
|
168
|
+
: typeof p.url === 'string'
|
|
169
|
+
? p.url
|
|
170
|
+
: undefined;
|
|
171
|
+
if (hasImageExt(imageUrl))
|
|
172
|
+
return true;
|
|
173
|
+
if (hasImageExt(p.path))
|
|
174
|
+
return true;
|
|
157
175
|
}
|
|
158
176
|
}
|
|
159
|
-
}
|
|
160
|
-
if (rawPayload && typeof rawPayload === 'object') {
|
|
161
|
-
collect(rawPayload.content);
|
|
162
|
-
}
|
|
163
|
-
if (!candidates.length) {
|
|
164
177
|
return false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
178
|
+
};
|
|
179
|
+
// 仅考虑“当前这一轮”的用户输入是否包含图片链接或图片负载,避免因为历史上下文中曾经出现过图片而在后续轮次持续暴露 view_image。
|
|
180
|
+
if (Array.isArray(messages)) {
|
|
181
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
182
|
+
const msg = messages[i];
|
|
183
|
+
if (!msg || typeof msg !== 'object')
|
|
184
|
+
continue;
|
|
185
|
+
const role = String(msg.role || '').toLowerCase();
|
|
186
|
+
if (role !== 'user')
|
|
187
|
+
continue;
|
|
188
|
+
if (hasImageInMessage(msg)) {
|
|
171
189
|
return true;
|
|
172
190
|
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (rawPayload && typeof rawPayload === 'object') {
|
|
195
|
+
const text = rawPayload.content;
|
|
196
|
+
if (hasImageExt(text)) {
|
|
197
|
+
return true;
|
|
173
198
|
}
|
|
174
199
|
}
|
|
175
200
|
return false;
|
package/dist/filters/index.d.ts
CHANGED
|
@@ -48,15 +48,23 @@ export class ResponseApplyPatchToonDecodeFilter {
|
|
|
48
48
|
if (typeof nameRaw !== 'string' || nameRaw.trim().toLowerCase() !== 'apply_patch') {
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
|
-
const
|
|
52
|
-
if (typeof argStr !== 'string' || !argStr.trim())
|
|
53
|
-
continue;
|
|
51
|
+
const argIn = fn.arguments;
|
|
54
52
|
let parsed;
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (typeof argIn === 'string') {
|
|
54
|
+
if (!argIn.trim())
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(argIn);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// 如果 arguments 不是 JSON 字符串,则保持原样交给下游处理
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
else if (isObject(argIn)) {
|
|
65
|
+
parsed = argIn;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
60
68
|
continue;
|
|
61
69
|
}
|
|
62
70
|
if (!isObject(parsed))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isShellToolName, normalizeToolName } from '../../tools/tool-description-utils.js';
|
|
1
2
|
function envEnabled() {
|
|
2
3
|
// Default ON. Allow disabling via env RCC_TOON_ENABLE/ROUTECODEX_TOON_ENABLE = 0|false|off
|
|
3
4
|
const v = String(process?.env?.RCC_TOON_ENABLE || process?.env?.ROUTECODEX_TOON_ENABLE || '').toLowerCase();
|
|
@@ -46,6 +47,31 @@ function decodeToonPairs(toon) {
|
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
function coerceToPrimitive(value) {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (!trimmed)
|
|
53
|
+
return '';
|
|
54
|
+
const lower = trimmed.toLowerCase();
|
|
55
|
+
if (lower === 'true')
|
|
56
|
+
return true;
|
|
57
|
+
if (lower === 'false')
|
|
58
|
+
return false;
|
|
59
|
+
if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
|
|
60
|
+
const num = Number(trimmed);
|
|
61
|
+
if (Number.isFinite(num))
|
|
62
|
+
return num;
|
|
63
|
+
}
|
|
64
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
65
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(trimmed);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// fall through
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
49
75
|
/**
|
|
50
76
|
* Decode arguments.toon to standard JSON ({command, workdir?}) and map tool name 'shell_toon' → 'shell'.
|
|
51
77
|
* Stage: response_pre (before arguments stringify and invariants).
|
|
@@ -68,15 +94,23 @@ export class ResponseToolArgumentsToonDecodeFilter {
|
|
|
68
94
|
const fn = tc && tc.function ? tc.function : undefined;
|
|
69
95
|
if (!fn || typeof fn !== 'object')
|
|
70
96
|
continue;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
const rawName = fn.name;
|
|
98
|
+
const toolName = typeof rawName === 'string' ? rawName : '';
|
|
99
|
+
const normalizedName = normalizeToolName(toolName);
|
|
100
|
+
const isShellLike = isShellToolName(toolName);
|
|
101
|
+
const isApplyPatch = normalizedName === 'apply_patch';
|
|
102
|
+
const argIn = fn.arguments;
|
|
103
|
+
let parsed = undefined;
|
|
104
|
+
if (typeof argIn === 'string') {
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(argIn);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
parsed = undefined;
|
|
110
|
+
}
|
|
77
111
|
}
|
|
78
|
-
|
|
79
|
-
|
|
112
|
+
else if (isObject(argIn)) {
|
|
113
|
+
parsed = argIn;
|
|
80
114
|
}
|
|
81
115
|
if (!isObject(parsed))
|
|
82
116
|
continue;
|
|
@@ -102,24 +136,76 @@ export class ResponseToolArgumentsToonDecodeFilter {
|
|
|
102
136
|
}
|
|
103
137
|
continue; // keep original if decode fails
|
|
104
138
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
139
|
+
// apply_patch 的 toon 由专门的 ResponseApplyPatchToonDecodeFilter 处理,这里跳过,避免覆盖。
|
|
140
|
+
if (isApplyPatch) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (isShellLike) {
|
|
144
|
+
const commandRaw = (typeof kv['command'] === 'string' && kv['command'].trim()
|
|
145
|
+
? kv['command']
|
|
146
|
+
: typeof kv['cmd'] === 'string' && kv['cmd'].trim()
|
|
147
|
+
? kv['cmd']
|
|
148
|
+
: undefined) ?? '';
|
|
149
|
+
const workdirRaw = (typeof kv['workdir'] === 'string' && kv['workdir'].trim()
|
|
150
|
+
? kv['workdir']
|
|
151
|
+
: typeof kv['cwd'] === 'string' && kv['cwd'].trim()
|
|
152
|
+
? kv['cwd']
|
|
153
|
+
: undefined) ?? '';
|
|
154
|
+
const timeoutRaw = typeof kv['timeout_ms'] === 'string' ? kv['timeout_ms'] : undefined;
|
|
155
|
+
const escalatedRaw = typeof kv['with_escalated_permissions'] === 'string'
|
|
156
|
+
? kv['with_escalated_permissions']
|
|
157
|
+
: undefined;
|
|
158
|
+
const justificationRaw = typeof kv['justification'] === 'string' ? kv['justification'] : undefined;
|
|
159
|
+
const command = commandRaw.trim();
|
|
160
|
+
if (command) {
|
|
161
|
+
const merged = {
|
|
162
|
+
cmd: command,
|
|
163
|
+
command
|
|
164
|
+
};
|
|
165
|
+
const workdir = workdirRaw.trim();
|
|
166
|
+
if (workdir) {
|
|
167
|
+
merged.workdir = workdir;
|
|
168
|
+
}
|
|
169
|
+
if (timeoutRaw) {
|
|
170
|
+
const timeoutNum = Number(timeoutRaw);
|
|
171
|
+
if (Number.isFinite(timeoutNum)) {
|
|
172
|
+
merged.timeout_ms = timeoutNum;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (escalatedRaw) {
|
|
176
|
+
const escLower = escalatedRaw.trim().toLowerCase();
|
|
177
|
+
if (escLower === 'true' || escLower === 'yes' || escLower === '1') {
|
|
178
|
+
merged.with_escalated_permissions = true;
|
|
179
|
+
}
|
|
180
|
+
else if (escLower === 'false' || escLower === 'no' || escLower === '0') {
|
|
181
|
+
merged.with_escalated_permissions = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (justificationRaw && justificationRaw.trim()) {
|
|
185
|
+
merged.justification = justificationRaw;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
fn.arguments = JSON.stringify(merged);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* keep original */
|
|
192
|
+
}
|
|
193
|
+
if (typeof fn.name === 'string' && fn.name === 'shell_toon') {
|
|
194
|
+
fn.name = 'shell';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// 通用 TOON → JSON 解码:除 shell / apply_patch 以外的工具,将 key: value 对映射为普通 JSON 字段。
|
|
200
|
+
const merged = {};
|
|
201
|
+
for (const [key, value] of Object.entries(kv)) {
|
|
202
|
+
merged[key] = coerceToPrimitive(value);
|
|
115
203
|
}
|
|
116
|
-
merged.command = command;
|
|
117
204
|
try {
|
|
118
205
|
fn.arguments = JSON.stringify(merged);
|
|
119
206
|
}
|
|
120
|
-
catch {
|
|
121
|
-
|
|
122
|
-
fn.name = 'shell';
|
|
207
|
+
catch {
|
|
208
|
+
// keep original on stringify failure
|
|
123
209
|
}
|
|
124
210
|
}
|
|
125
211
|
}
|
package/dist/guidance/index.js
CHANGED
|
@@ -227,6 +227,7 @@ export function augmentAnthropicTools(tools) {
|
|
|
227
227
|
const marker = '[Codex ApplyPatch Guidance]';
|
|
228
228
|
const guidance = [
|
|
229
229
|
marker,
|
|
230
|
+
'Before using apply_patch, always read the latest content of the target file (via shell or another tool) and base your changes on that content.',
|
|
230
231
|
'Provide structured changes (insert_after / insert_before / replace / delete / create_file / delete_file) instead of raw patch text.',
|
|
231
232
|
'Each change must include the target file (relative path) plus anchor/target snippets and the replacement lines.',
|
|
232
233
|
'所有路径必须相对工作区根目录,禁止输出以 / 或盘符开头的绝对路径。'
|
|
@@ -264,6 +265,7 @@ export function buildSystemToolGuidance() {
|
|
|
264
265
|
lines.push(bullet('function.arguments must be a single JSON string. / arguments 必须是单个 JSON 字符串。'));
|
|
265
266
|
lines.push(bullet('shell: Place ALL intent into the command argv array only; do not invent extra keys. / shell 所有意图写入 command 数组,不要添加额外键名。'));
|
|
266
267
|
lines.push(bullet('File writes are FORBIDDEN via shell (no redirection, no here-doc, no sed -i, no ed -s, no tee). Use apply_patch ONLY. / 通过 shell 写文件一律禁止(不得使用重定向、heredoc、sed -i、ed -s、tee);必须使用 apply_patch。'));
|
|
268
|
+
lines.push(bullet('apply_patch: Before writing, always read the target file first and compute changes against the latest content using appropriate tools. / apply_patch 在写入前必须先通过合适的工具读取目标文件最新内容,并基于该内容生成变更。'));
|
|
267
269
|
lines.push(bullet('apply_patch: Provide structured JSON arguments with a `changes` array (insert_after / insert_before / replace / delete / create_file / delete_file); omit "+/-" prefixes in `lines`; file paths必须是相对路径。 / apply_patch 仅接受结构化 JSON。'));
|
|
268
270
|
lines.push(bullet('update_plan: Keep exactly one step in_progress; others pending/completed. / 仅一个 in_progress 步骤。'));
|
|
269
271
|
lines.push(bullet('view_image: Path must be an image file (.png .jpg .jpeg .gif .webp .bmp .svg). / 仅图片路径。'));
|
|
@@ -11,22 +11,23 @@ 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;
|
|
16
14
|
const reachedLongContext = features.estimatedTokens >= (this.config.longContextThresholdTokens ?? DEFAULT_LONG_CONTEXT_THRESHOLD);
|
|
17
15
|
const latestMessageFromUser = features.latestMessageFromUser === true;
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
16
|
+
const thinkingContinuation = lastToolCategory === 'read';
|
|
17
|
+
const thinkingFromUser = latestMessageFromUser;
|
|
18
|
+
const thinkingFromRead = !thinkingFromUser && thinkingContinuation;
|
|
19
|
+
const codingContinuation = lastToolCategory === 'write';
|
|
20
|
+
const searchContinuation = lastToolCategory === 'search';
|
|
21
|
+
const toolsContinuation = lastToolCategory === 'other';
|
|
22
|
+
const hasToolActivity = features.hasTools || features.hasToolCallResponses;
|
|
22
23
|
const evaluationMap = {
|
|
23
24
|
vision: {
|
|
24
25
|
triggered: features.hasImageAttachment,
|
|
25
26
|
reason: 'vision:image-detected'
|
|
26
27
|
},
|
|
27
28
|
thinking: {
|
|
28
|
-
triggered:
|
|
29
|
-
reason: 'thinking:user-input'
|
|
29
|
+
triggered: thinkingFromUser || thinkingFromRead,
|
|
30
|
+
reason: thinkingFromUser ? 'thinking:user-input' : 'thinking:last-tool-read'
|
|
30
31
|
},
|
|
31
32
|
longcontext: {
|
|
32
33
|
triggered: reachedLongContext,
|
|
@@ -36,9 +37,11 @@ export class RoutingClassifier {
|
|
|
36
37
|
triggered: codingContinuation,
|
|
37
38
|
reason: 'coding:last-tool-write'
|
|
38
39
|
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
web_search: {
|
|
41
|
+
// web_search 路由不再基于上一轮工具分类或本轮是否声明 web_search 工具自动触发,
|
|
42
|
+
// 仅保留为显式路由指令/未来扩展的占位,默认不命中。
|
|
43
|
+
triggered: false,
|
|
44
|
+
reason: 'web_search:disabled'
|
|
42
45
|
},
|
|
43
46
|
search: {
|
|
44
47
|
// search 路由:仅在上一轮 assistant 使用 search 类工具时继续命中,
|
|
@@ -48,7 +51,8 @@ export class RoutingClassifier {
|
|
|
48
51
|
},
|
|
49
52
|
tools: {
|
|
50
53
|
// tools 路由:通用工具分支,包括首次声明的 web/search 工具。
|
|
51
|
-
|
|
54
|
+
// 若上一轮已明确归类为 search,则优先命中 search 路由,tools 仅作为兜底。
|
|
55
|
+
triggered: toolsContinuation || (!searchContinuation && hasToolActivity),
|
|
52
56
|
reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
|
|
53
57
|
},
|
|
54
58
|
background: {
|
|
@@ -130,6 +130,37 @@ export class VirtualRouterEngine {
|
|
|
130
130
|
this.debug?.log?.('[virtual-router-hit]', selection.routeUsed, selection.providerKey, target.modelId || '', hitReason ? `reason=${hitReason}` : '');
|
|
131
131
|
}
|
|
132
132
|
const didFallback = selection.routeUsed !== requestedRoute;
|
|
133
|
+
// 自动 sticky:对需要上下文 save/restore 的 Responses 会话,强制同一个 provider.key.model。
|
|
134
|
+
// 其它协议不启用粘滞,仅显式 routing 指令才会写入 stickyTarget。
|
|
135
|
+
const providerProtocol = metadata?.providerProtocol;
|
|
136
|
+
const serverToolRequired = metadata?.serverToolRequired === true;
|
|
137
|
+
const disableSticky = metadata &&
|
|
138
|
+
typeof metadata === 'object' &&
|
|
139
|
+
metadata.disableStickyRoutes === true;
|
|
140
|
+
const shouldAutoStickyForResponses = providerProtocol === 'openai-responses' && serverToolRequired && !disableSticky;
|
|
141
|
+
if (shouldAutoStickyForResponses) {
|
|
142
|
+
const stickyKeyForState = this.resolveStickyKey(metadata);
|
|
143
|
+
if (stickyKeyForState) {
|
|
144
|
+
const stateKey = stickyKeyForState;
|
|
145
|
+
const state = this.getRoutingInstructionState(stateKey);
|
|
146
|
+
if (!state.stickyTarget) {
|
|
147
|
+
const providerId = this.extractProviderId(selection.providerKey);
|
|
148
|
+
if (providerId) {
|
|
149
|
+
const parts = selection.providerKey.split('.');
|
|
150
|
+
const keyAlias = parts.length >= 3 ? parts[1] : undefined;
|
|
151
|
+
const modelId = target.modelId;
|
|
152
|
+
state.stickyTarget = {
|
|
153
|
+
provider: providerId,
|
|
154
|
+
keyAlias,
|
|
155
|
+
model: modelId,
|
|
156
|
+
// pathLength=3 表示 provider.key.model 形式,对应 alias 显式 sticky;
|
|
157
|
+
// 若缺少别名或模型,则退化为更短 pathLength。
|
|
158
|
+
pathLength: keyAlias && modelId ? 3 : keyAlias ? 2 : 1
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
133
164
|
return {
|
|
134
165
|
target,
|
|
135
166
|
decision: {
|
|
@@ -263,14 +294,24 @@ export class VirtualRouterEngine {
|
|
|
263
294
|
return this.healthManager.getConfig();
|
|
264
295
|
}
|
|
265
296
|
resolveStickyKey(metadata) {
|
|
297
|
+
const providerProtocol = metadata.providerProtocol;
|
|
298
|
+
// 对 Responses 协议的自动粘滞,仅在“单次会话链路”内生效:
|
|
299
|
+
// - Resume/submit 调用:stickyKey = previousRequestId(指向首轮请求);
|
|
300
|
+
// - 普通 /v1/responses 调用:stickyKey = 本次 requestId;
|
|
301
|
+
// 这样不会把 Responses 的自动粘滞扩散到整个 session,仅在需要 save/restore
|
|
302
|
+
// 的请求链路中复用 provider.key.model。
|
|
303
|
+
if (providerProtocol === 'openai-responses') {
|
|
304
|
+
const resume = metadata.responsesResume;
|
|
305
|
+
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
306
|
+
return resume.previousRequestId.trim();
|
|
307
|
+
}
|
|
308
|
+
return metadata.requestId;
|
|
309
|
+
}
|
|
310
|
+
// 其它协议沿用会话级 sticky 语义:sessionId / conversationId → requestId。
|
|
266
311
|
const sessionScope = this.resolveSessionScope(metadata);
|
|
267
312
|
if (sessionScope) {
|
|
268
313
|
return sessionScope;
|
|
269
314
|
}
|
|
270
|
-
const resume = metadata.responsesResume;
|
|
271
|
-
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
272
|
-
return resume.previousRequestId.trim();
|
|
273
|
-
}
|
|
274
315
|
return metadata.requestId;
|
|
275
316
|
}
|
|
276
317
|
resolveSessionScope(metadata) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StandardizedMessage, StandardizedRequest } from '../../conversion/hub/types/standardized.js';
|
|
2
|
-
export type ToolCategory = 'read' | 'write' | 'search' | 'other';
|
|
2
|
+
export type ToolCategory = 'read' | 'write' | 'search' | 'websearch' | 'other';
|
|
3
3
|
export type ToolClassification = {
|
|
4
4
|
category: ToolCategory;
|
|
5
5
|
name: string;
|
|
@@ -10,4 +10,5 @@ export declare function detectCodingTool(request: StandardizedRequest): boolean;
|
|
|
10
10
|
export declare function detectWebTool(request: StandardizedRequest): boolean;
|
|
11
11
|
export declare function extractMeaningfulDeclaredToolNames(tools: StandardizedRequest['tools'] | undefined): string[];
|
|
12
12
|
export declare function detectLastAssistantToolCategory(messages: StandardizedMessage[]): ToolClassification | undefined;
|
|
13
|
+
export declare function classifyToolCallForReport(call: StandardizedMessage['tool_calls'][number]): ToolClassification | undefined;
|
|
13
14
|
export declare function canonicalizeToolName(rawName: string): string;
|