@agent-proxy/zcode 0.11.3 → 0.12.1
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/index.d.ts +4 -6
- package/dist/index.js +99 -120
- package/dist/request.d.ts +12 -1
- package/dist/request.js +244 -234
- package/dist/resources/models.js +3260 -910
- package/dist/response.d.ts +8 -0
- package/dist/response.js +65 -20
- package/dist/wire.d.ts +2 -4
- package/package.json +5 -5
package/dist/request.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BridgeConversionError, convertRequest } from "@agent-proxy/llm-bridge";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { decodeZcodeThinking, isZcodeRecord } from "./wire.js";
|
|
4
4
|
import { ANALYZE_IMAGE_SYSTEM_NOTE, ANALYZE_IMAGE_TOOL_DECLARATION, ANALYZE_IMAGE_TOOL_NAME, collectZcodeImages } from "./vision.js";
|
|
5
|
+
/**
|
|
6
|
+
* ZCode 请求准备:标准消息/工具/参数统一经 @agent-proxy/llm-bridge 的
|
|
7
|
+
* convertRequest(to, "anthropic");z.ai 私有形状(web_search_20250305 服务端工具、
|
|
8
|
+
* thinking 预算表、zcode-thinking 信封回放、analyze_image 代执行)作为插件前后处理。
|
|
9
|
+
*
|
|
10
|
+
* Responses 源的私有 history 项(reasoning / web_search_call / 其它服务器调用)不能
|
|
11
|
+
* 进标准转换:按项把 input 切成「标准段 + 私有项」,标准段逐段调用公共入口,私有段
|
|
12
|
+
* 由本模块转换,最后按 main 的同角色合并语义拼装。
|
|
13
|
+
*/
|
|
5
14
|
export class ZcodeRequestError extends Error {
|
|
6
|
-
constructor(message) {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "ZcodeRequestError";
|
|
18
|
+
}
|
|
7
19
|
}
|
|
8
20
|
const CUSTOM_INPUT_SCHEMA = {
|
|
9
21
|
type: "object", properties: { input: { type: "string" } }, required: ["input"], additionalProperties: false,
|
|
@@ -11,8 +23,12 @@ const CUSTOM_INPUT_SCHEMA = {
|
|
|
11
23
|
const THINKING_BUDGETS = {
|
|
12
24
|
minimal: 1024, low: 2048, medium: 4096, high: 8192, xhigh: 10240, max: 10240, ultra: 10240,
|
|
13
25
|
};
|
|
14
|
-
function fail(message) {
|
|
15
|
-
|
|
26
|
+
function fail(message) {
|
|
27
|
+
throw new ZcodeRequestError(message);
|
|
28
|
+
}
|
|
29
|
+
function record(value) {
|
|
30
|
+
return isZcodeRecord(value);
|
|
31
|
+
}
|
|
16
32
|
function string(value, field) {
|
|
17
33
|
if (typeof value !== "string")
|
|
18
34
|
fail(`${field} 必须是字符串`);
|
|
@@ -29,6 +45,8 @@ function safeToolName(name) {
|
|
|
29
45
|
const base = name.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+/, "tool_") || "tool";
|
|
30
46
|
return `${base.slice(0, 112)}_${createHash("sha256").update(name).digest("hex").slice(0, 12)}`;
|
|
31
47
|
}
|
|
48
|
+
/** 与声明/历史/choice 共用的 zcode 服务安全名策略(作为 mapToolName 传入桥接)。 */
|
|
49
|
+
const zcodeToolMapper = (identity) => safeToolName(identity.namespace === undefined ? identity.name : JSON.stringify([identity.namespace, identity.name]));
|
|
32
50
|
function addMessage(messages, role, blocks) {
|
|
33
51
|
if (!blocks.length)
|
|
34
52
|
return;
|
|
@@ -38,91 +56,6 @@ function addMessage(messages, role, blocks) {
|
|
|
38
56
|
else
|
|
39
57
|
messages.push({ role, content: blocks });
|
|
40
58
|
}
|
|
41
|
-
function image(part) {
|
|
42
|
-
const source = part.image_url ?? part.url;
|
|
43
|
-
const url = typeof source === "string" ? source : record(source) ? source.url : undefined;
|
|
44
|
-
if (typeof url !== "string" || !url)
|
|
45
|
-
fail("input_image 缺少 image_url");
|
|
46
|
-
const data = /^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i.exec(url);
|
|
47
|
-
return data
|
|
48
|
-
? { type: "image", source: { type: "base64", media_type: data[1], data: data[2] } }
|
|
49
|
-
: { type: "image", source: { type: "url", url } };
|
|
50
|
-
}
|
|
51
|
-
function content(value, field) {
|
|
52
|
-
if (typeof value === "string")
|
|
53
|
-
return [{ type: "text", text: value }];
|
|
54
|
-
if (!Array.isArray(value))
|
|
55
|
-
fail(`${field} 必须是字符串或内容数组`);
|
|
56
|
-
return value.map((raw) => {
|
|
57
|
-
if (!record(raw))
|
|
58
|
-
fail(`${field} 包含无效内容块`);
|
|
59
|
-
if (raw.type === "input_text" || raw.type === "output_text" || raw.type === "text") {
|
|
60
|
-
return { type: "text", text: string(raw.text, `${field}.text`) };
|
|
61
|
-
}
|
|
62
|
-
if (raw.type === "input_image" || raw.type === "image")
|
|
63
|
-
return image(raw);
|
|
64
|
-
fail(`${field} 包含不支持的内容块类型 ${String(raw.type)}`);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
function callInput(value, custom) {
|
|
68
|
-
if (custom)
|
|
69
|
-
return { input: string(value, "custom_tool_call.input") };
|
|
70
|
-
if (record(value))
|
|
71
|
-
return value;
|
|
72
|
-
if (typeof value !== "string")
|
|
73
|
-
fail("function_call 的 arguments 必须是 JSON 对象或 JSON 字符串");
|
|
74
|
-
try {
|
|
75
|
-
const parsed = JSON.parse(value);
|
|
76
|
-
if (!record(parsed))
|
|
77
|
-
fail("function_call 的 arguments 必须解码为 JSON 对象");
|
|
78
|
-
return parsed;
|
|
79
|
-
}
|
|
80
|
-
catch (cause) {
|
|
81
|
-
if (cause instanceof ZcodeRequestError)
|
|
82
|
-
throw cause;
|
|
83
|
-
fail("function_call 的 arguments 不是有效 JSON 对象");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function toolResult(value) {
|
|
87
|
-
if (typeof value === "string")
|
|
88
|
-
return value;
|
|
89
|
-
if (Array.isArray(value)) {
|
|
90
|
-
return value.map((part) => {
|
|
91
|
-
if (record(part) && part.type === "input_image")
|
|
92
|
-
return image(part);
|
|
93
|
-
if (!record(part) || (part.type !== "input_text" && part.type !== "output_text" && part.type !== "text")) {
|
|
94
|
-
fail("function_call_output 包含不支持的内容块");
|
|
95
|
-
}
|
|
96
|
-
return { type: "text", text: string(part.text, "tool 输出文本") };
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return JSON.stringify(value ?? null);
|
|
100
|
-
}
|
|
101
|
-
function bridgeText(input, model, maxTokens) {
|
|
102
|
-
// llm-bridge 是普通 Responses 文本的基础转换器;其遗漏的工具与 custom 语义由下方严格补齐。
|
|
103
|
-
const converted = translateBetweenProviders("openai-responses", "anthropic", {
|
|
104
|
-
model, input, max_output_tokens: maxTokens,
|
|
105
|
-
});
|
|
106
|
-
if (!record(converted) || !Array.isArray(converted.messages))
|
|
107
|
-
fail("llm-bridge 未返回有效 Anthropic 请求");
|
|
108
|
-
return converted.messages.map((raw) => {
|
|
109
|
-
if (!record(raw) || (raw.role !== "user" && raw.role !== "assistant") || !Array.isArray(raw.content)) {
|
|
110
|
-
fail("llm-bridge 返回了无效消息");
|
|
111
|
-
}
|
|
112
|
-
return { role: raw.role, content: raw.content.filter(record) };
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
function toolIdentity(rawName, rawNamespace) {
|
|
116
|
-
const original = string(rawName, "工具 name");
|
|
117
|
-
if (!original)
|
|
118
|
-
fail("工具 name 不能为空");
|
|
119
|
-
const namespace = rawNamespace === undefined || rawNamespace === null ? undefined : string(rawNamespace, "工具 namespace");
|
|
120
|
-
if (namespace === "")
|
|
121
|
-
fail("工具 namespace 不能为空");
|
|
122
|
-
// 数组编码区分 namespace/name 和带点的普通工具名,声明与历史共享稳定映射。
|
|
123
|
-
const name = safeToolName(namespace === undefined ? original : JSON.stringify([namespace, original]));
|
|
124
|
-
return { name, original, ...(namespace === undefined ? {} : { namespace }) };
|
|
125
|
-
}
|
|
126
59
|
/** z.ai 对 web_search_20250305 的实测回放形状:工具名映射为 web_search_prime,query 走 search_query。 */
|
|
127
60
|
function serverToolQuery(input) {
|
|
128
61
|
for (const key of ["search_query", "query"]) {
|
|
@@ -142,11 +75,11 @@ function webSearchServerTool(raw) {
|
|
|
142
75
|
tool.user_location = raw.user_location;
|
|
143
76
|
return tool;
|
|
144
77
|
}
|
|
145
|
-
|
|
146
|
-
|
|
78
|
+
/** Responses 工具声明规划(web_search 服务端工具、custom/namespace 安全名身份表)。 */
|
|
79
|
+
function planResponsesTools(value, map) {
|
|
80
|
+
const declarations = [];
|
|
147
81
|
const serverTools = [];
|
|
148
|
-
const
|
|
149
|
-
const dropped = new Set();
|
|
82
|
+
const dropped = [];
|
|
150
83
|
function visit(entries, namespace, description) {
|
|
151
84
|
if (!Array.isArray(entries))
|
|
152
85
|
fail("tools 必须是数组");
|
|
@@ -159,47 +92,42 @@ function translateTools(value) {
|
|
|
159
92
|
const segment = string(raw.name, "namespace.name");
|
|
160
93
|
if (!segment)
|
|
161
94
|
fail("namespace.name 不能为空");
|
|
162
|
-
visit(raw.tools, segment, typeof raw.description === "string" ? raw.description :
|
|
95
|
+
visit(raw.tools, segment, typeof raw.description === "string" ? raw.description : description);
|
|
163
96
|
continue;
|
|
164
97
|
}
|
|
165
98
|
if (raw.type === "web_search") {
|
|
166
99
|
// 服务器内置工具按上游原生等价物映射;客户端显式禁用外部访问或嵌在 namespace 内时降级剥离。
|
|
167
100
|
if (raw.external_web_access === false || namespace !== undefined)
|
|
168
|
-
dropped.
|
|
101
|
+
dropped.push("web_search");
|
|
169
102
|
else
|
|
170
103
|
serverTools.push(webSearchServerTool(raw));
|
|
171
104
|
continue;
|
|
172
105
|
}
|
|
173
106
|
if (raw.type !== "function" && raw.type !== "custom") {
|
|
174
|
-
dropped.
|
|
107
|
+
dropped.push(typeof raw.type === "string" ? raw.type : "unknown");
|
|
175
108
|
continue;
|
|
176
109
|
}
|
|
177
|
-
const identity = toolIdentity(raw.name, namespace);
|
|
178
|
-
if (map.has(identity.name))
|
|
179
|
-
fail(`工具名映射冲突:${identity.original}`);
|
|
180
110
|
const custom = raw.type === "custom";
|
|
111
|
+
const original = string(raw.name, "工具 name");
|
|
112
|
+
if (!original)
|
|
113
|
+
fail("工具 name 不能为空");
|
|
114
|
+
const ns = namespace === undefined || namespace === null ? undefined : string(namespace, "工具 namespace");
|
|
115
|
+
const mapped = zcodeToolMapper({ name: original, namespace: ns });
|
|
116
|
+
if (map.has(mapped))
|
|
117
|
+
fail(`工具名映射冲突:${original}`);
|
|
181
118
|
if (!custom && !record(raw.parameters))
|
|
182
|
-
fail(`function 工具 ${
|
|
183
|
-
const tool = { name:
|
|
119
|
+
fail(`function 工具 ${original} 缺少对象 parameters`);
|
|
120
|
+
const tool = { name: mapped, input_schema: custom ? CUSTOM_INPUT_SCHEMA : raw.parameters };
|
|
184
121
|
const details = [description, typeof raw.description === "string" ? raw.description : undefined].filter((item) => item !== undefined);
|
|
185
122
|
if (details.length)
|
|
186
123
|
tool.description = details.join("\n\n");
|
|
187
|
-
|
|
188
|
-
map.set(
|
|
124
|
+
declarations.push(tool);
|
|
125
|
+
map.set(mapped, { name: original, custom, ...(ns === undefined ? {} : { namespace: ns }) });
|
|
189
126
|
}
|
|
190
127
|
}
|
|
191
128
|
if (value !== undefined)
|
|
192
129
|
visit(value);
|
|
193
|
-
return {
|
|
194
|
-
}
|
|
195
|
-
function callTool(raw, tools) {
|
|
196
|
-
const identity = toolIdentity(raw.name, raw.namespace);
|
|
197
|
-
const tool = tools.get(identity.name);
|
|
198
|
-
if (!tool || tool.name !== identity.original || tool.namespace !== identity.namespace)
|
|
199
|
-
fail(`工具选择引用了未声明的工具 ${identity.original}`);
|
|
200
|
-
if (tool.custom !== (raw.type === "custom"))
|
|
201
|
-
fail("tool_choice 与工具声明类型不一致");
|
|
202
|
-
return { name: identity.name, custom: tool.custom };
|
|
130
|
+
return { declarations, serverTools, dropped };
|
|
203
131
|
}
|
|
204
132
|
function webSearchCallQuery(item) {
|
|
205
133
|
const action = record(item.action) ? item.action : {};
|
|
@@ -237,117 +165,82 @@ function serverCallSummary(item) {
|
|
|
237
165
|
const detail = query ? `(查询:${query})` : "";
|
|
238
166
|
return { type: "text", text: `[已省略服务器内置工具调用 ${String(item.type)}${detail};本通道不支持该工具]` };
|
|
239
167
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!Array.isArray(
|
|
168
|
+
/** 公共转换结果 → Message[](anthropic 消息形状校验与收口)。 */
|
|
169
|
+
function anthropicMessagesOf(converted) {
|
|
170
|
+
const raw = converted.body.messages;
|
|
171
|
+
if (!Array.isArray(raw))
|
|
172
|
+
fail("convertRequest 未返回有效 Anthropic 消息");
|
|
173
|
+
return raw.map((entry) => {
|
|
174
|
+
if (!record(entry) || (entry.role !== "user" && entry.role !== "assistant") || !Array.isArray(entry.content)) {
|
|
175
|
+
fail("convertRequest 返回了无效消息");
|
|
176
|
+
}
|
|
177
|
+
return { role: entry.role, content: entry.content.filter(record) };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* R4 统一请求前处理:responses 源按私有项切段——thinking 信封、web_search 回放
|
|
182
|
+
* 等私有段在本模块就地转换,标准段交给桥接;其余来源没有 zcode 私有项,整段返回。
|
|
183
|
+
* 主路径对所有来源统一执行一次 convertRequest(to, "anthropic", …);to 表示路由
|
|
184
|
+
* 端点协议(响应方向 anthropic → to 的视角命名,请求方向即桥接的源协议位)。
|
|
185
|
+
*/
|
|
186
|
+
function segmentZcodeRequestLegs(to, body, options) {
|
|
187
|
+
if (to !== "responses")
|
|
188
|
+
return [{ body }];
|
|
189
|
+
const input = body.input;
|
|
190
|
+
if (typeof input !== "string" && !Array.isArray(input))
|
|
244
191
|
fail("input 必须是字符串或数组");
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
192
|
+
const items = typeof input === "string" ? [{ type: "message", role: "user", content: input }] : input;
|
|
193
|
+
const legs = [];
|
|
194
|
+
let run = [];
|
|
195
|
+
const flush = () => {
|
|
196
|
+
if (!run.length)
|
|
197
|
+
return;
|
|
198
|
+
// 标准段沿用源体的其余字段;instructions 由本层统一管理,不重复下发。
|
|
199
|
+
legs.push({ body: { ...body, input: run, instructions: undefined } });
|
|
200
|
+
run = [];
|
|
201
|
+
};
|
|
202
|
+
for (const item of items) {
|
|
250
203
|
if (!record(item))
|
|
251
|
-
fail("input 包含非对象 history 项");
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
if (item.type === "message" || item.type === undefined) {
|
|
260
|
-
if (item.role === "system" || item.role === "developer") {
|
|
261
|
-
for (const block of content(item.content, `${item.role} 消息`)) {
|
|
262
|
-
if (block.type !== "text")
|
|
263
|
-
fail(`${item.role} 消息不支持图片`);
|
|
264
|
-
system.push(string(block.text, `${item.role} 消息文本`));
|
|
265
|
-
}
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (item.role !== "user" && item.role !== "assistant")
|
|
269
|
-
fail(`不支持的消息 role ${String(item.role)}`);
|
|
270
|
-
addMessage(messages, item.role, content(item.content, `${item.role} 消息`));
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
274
|
-
const tool = toolIdentity(item.name, item.namespace);
|
|
275
|
-
const custom = item.type === "custom_tool_call";
|
|
276
|
-
const callId = string(item.call_id ?? item.id, `${item.type}.call_id`);
|
|
277
|
-
if (calls.has(callId))
|
|
278
|
-
fail(`history 中重复的工具调用 ID ${callId}`);
|
|
279
|
-
calls.set(callId, item.type);
|
|
280
|
-
addMessage(messages, "assistant", [{
|
|
281
|
-
type: "tool_use", id: callId, name: tool.name,
|
|
282
|
-
input: callInput(custom ? item.input : item.arguments, custom),
|
|
283
|
-
}]);
|
|
204
|
+
fail("Responses input 包含非对象 history 项");
|
|
205
|
+
const type = typeof item.type === "string" ? item.type : "message";
|
|
206
|
+
const privateKind = type === "reasoning" || type === "web_search_call"
|
|
207
|
+
|| (type.endsWith("_call") && type !== "function_call" && type !== "custom_tool_call")
|
|
208
|
+
|| type === "mcp_list_tools" || type === "mcp_list_resources";
|
|
209
|
+
if (!privateKind) {
|
|
210
|
+
run.push(item);
|
|
284
211
|
continue;
|
|
285
212
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (results.has(callId))
|
|
293
|
-
fail(`history 中重复的工具输出 ID ${callId}`);
|
|
294
|
-
results.add(callId);
|
|
295
|
-
addMessage(messages, "user", [{
|
|
296
|
-
type: "tool_result", tool_use_id: callId, content: toolResult(item.output ?? item.content),
|
|
297
|
-
}]);
|
|
213
|
+
flush();
|
|
214
|
+
if (type === "reasoning") {
|
|
215
|
+
const blocks = decodeZcodeThinking(item.encrypted_content, options.clientModel);
|
|
216
|
+
// 外部或不同模型的私有载荷不能转发;自身匹配的载荷按原块还原。
|
|
217
|
+
if (blocks)
|
|
218
|
+
legs.push({ private: blocks });
|
|
298
219
|
continue;
|
|
299
220
|
}
|
|
300
|
-
if (
|
|
221
|
+
if (type === "web_search_call") {
|
|
301
222
|
const query = webSearchCallQuery(item);
|
|
302
|
-
if (nativeWebSearch) {
|
|
223
|
+
if (options.nativeWebSearch) {
|
|
303
224
|
// 还原为 z.ai 实测回放形状:server_tool_use(web_search_prime/search_query)+ 同轮 tool_result 文本块。
|
|
304
225
|
const id = webSearchUpstreamID(item);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
]);
|
|
226
|
+
legs.push({ private: [
|
|
227
|
+
{ type: "server_tool_use", id, name: "web_search_prime", input: query ? { search_query: query } : {} },
|
|
228
|
+
{ type: "tool_result", tool_use_id: id, content: webSearchResultContent(item) },
|
|
229
|
+
] });
|
|
310
230
|
}
|
|
311
231
|
else {
|
|
312
|
-
|
|
232
|
+
legs.push({ private: [serverCallSummary(item)] });
|
|
313
233
|
}
|
|
314
234
|
continue;
|
|
315
235
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
addMessage(messages, "assistant", [serverCallSummary(item)]);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
fail(`不支持的 Responses history 项类型 ${typeof item.type === "string" ? item.type : "unknown"}`);
|
|
236
|
+
// 其余服务器执行的历史调用(file_search_call、image_generation_call、托管 MCP 等)无法在上游执行,降级为文本摘要。
|
|
237
|
+
legs.push({ private: [serverCallSummary(item)] });
|
|
322
238
|
}
|
|
323
|
-
|
|
239
|
+
flush();
|
|
240
|
+
return legs;
|
|
324
241
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
fail("parallel_tool_calls 必须是布尔值");
|
|
328
|
-
const noParallel = parallel === false;
|
|
329
|
-
if (value === undefined)
|
|
330
|
-
return noParallel ? { type: "auto", disable_parallel_tool_use: true } : undefined;
|
|
331
|
-
let choice;
|
|
332
|
-
if (value === "auto")
|
|
333
|
-
choice = { type: "auto" };
|
|
334
|
-
else if (value === "none")
|
|
335
|
-
choice = { type: "none" };
|
|
336
|
-
else if (value === "required")
|
|
337
|
-
choice = { type: "any" };
|
|
338
|
-
else if (record(value) && (value.type === "function" || value.type === "custom"))
|
|
339
|
-
choice = { type: "tool", name: callTool(value, tools).name };
|
|
340
|
-
else if (record(value) && typeof value.type === "string" && value.type !== "") {
|
|
341
|
-
// 指向服务器内置工具(含被剥离工具)的选择无法跨格式表达,省略让上游回到默认 auto。
|
|
342
|
-
return noParallel ? { type: "auto", disable_parallel_tool_use: true } : undefined;
|
|
343
|
-
}
|
|
344
|
-
else
|
|
345
|
-
fail("不支持的 tool_choice");
|
|
346
|
-
if (noParallel && choice.type !== "none")
|
|
347
|
-
choice.disable_parallel_tool_use = true;
|
|
348
|
-
return choice;
|
|
349
|
-
}
|
|
350
|
-
export function translateZcodeRequest(body, upstreamModel) {
|
|
242
|
+
/** to 为路由端点(客户端入口)协议:请求方向 to → anthropic,响应方向 anthropic → to。 */
|
|
243
|
+
export async function translateZcodeRequest(body, upstreamModel, to = "responses") {
|
|
351
244
|
if (typeof upstreamModel !== "string" || !upstreamModel)
|
|
352
245
|
fail("上游模型不能为空");
|
|
353
246
|
if (body.previous_response_id !== undefined && body.previous_response_id !== null && body.previous_response_id !== "") {
|
|
@@ -364,51 +257,168 @@ export function translateZcodeRequest(body, upstreamModel) {
|
|
|
364
257
|
}
|
|
365
258
|
if (body.instructions !== undefined && typeof body.instructions !== "string")
|
|
366
259
|
fail("instructions 必须是字符串");
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
260
|
+
// 上游 token 上限按来源取参:responses 用 max_output_tokens,chat-completions /
|
|
261
|
+
// anthropic 用 max_tokens,gemini 用 generationConfig.maxOutputTokens;缺省走服务默认 16384。
|
|
262
|
+
const sourceLimit = to === "responses"
|
|
263
|
+
? body.max_output_tokens
|
|
264
|
+
: to === "gemini"
|
|
265
|
+
? (record(body.generationConfig) ? body.generationConfig.maxOutputTokens : undefined)
|
|
266
|
+
: body.max_tokens;
|
|
267
|
+
const maxTokens = sourceLimit === undefined ? 16384 : positiveInteger(sourceLimit, "token 上限");
|
|
268
|
+
const toolMap = new Map();
|
|
269
|
+
const plan = to === "responses" ? planResponsesTools(body.tools, toolMap) : { declarations: [], serverTools: [], dropped: [] };
|
|
270
|
+
const nativeWebSearch = plan.serverTools.length > 0;
|
|
370
271
|
const clientModel = body.model === undefined ? upstreamModel : string(body.model, "model");
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
272
|
+
const identities = new Map();
|
|
273
|
+
const messages = [];
|
|
274
|
+
const system = [];
|
|
275
|
+
if (typeof body.instructions === "string" && body.instructions)
|
|
276
|
+
system.push(body.instructions);
|
|
277
|
+
let passthroughChoice;
|
|
278
|
+
let passthroughStopSequences;
|
|
279
|
+
let passthroughTools;
|
|
280
|
+
try {
|
|
281
|
+
// R4 统一形态:前处理切段(私有段就地转换),主路径对所有来源统一执行一次
|
|
282
|
+
// convertRequest(from, to)——from 是网关识别的真实客户端协议,目标恒为 anthropic。
|
|
283
|
+
for (const leg of segmentZcodeRequestLegs(to, body, { nativeWebSearch, clientModel })) {
|
|
284
|
+
if (leg.private !== undefined) {
|
|
285
|
+
addMessage(messages, "assistant", leg.private);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// 桥接统一形态 convertRequest(from, to, body):from 为上游 endpoint(anthropic),to 为调用方 route 协议。
|
|
289
|
+
const converted = await convertRequest("anthropic", to, leg.body, { model: upstreamModel, mapToolName: zcodeToolMapper });
|
|
290
|
+
for (const [mapped, identity] of converted.tools)
|
|
291
|
+
identities.set(mapped, identity);
|
|
292
|
+
for (const message of anthropicMessagesOf(converted))
|
|
293
|
+
addMessage(messages, message.role, message.content);
|
|
294
|
+
if (typeof converted.body.system === "string" && converted.body.system)
|
|
295
|
+
system.push(converted.body.system);
|
|
296
|
+
passthroughTools = Array.isArray(converted.body.tools) ? converted.body.tools.filter(record) : undefined;
|
|
297
|
+
passthroughChoice = converted.body.tool_choice;
|
|
298
|
+
if (Array.isArray(converted.body.stop_sequences) && converted.body.stop_sequences.every((entry) => typeof entry === "string")) {
|
|
299
|
+
passthroughStopSequences = converted.body.stop_sequences;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error instanceof BridgeConversionError)
|
|
305
|
+
throw new ZcodeRequestError(error.message);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
// chat/gemini/anthropic 源的工具声明来自公共转换(anthropic 为同协议直通的
|
|
309
|
+
// 原名身份表),身份表同步进 zcode 工具表;responses 源的 toolMap 只含声明工具
|
|
310
|
+
// (压缩历史中未声明的调用按需注册,不进入响应校验表)。
|
|
311
|
+
if (to === "chat-completions" || to === "gemini" || to === "anthropic") {
|
|
312
|
+
for (const [mapped, identity] of identities) {
|
|
313
|
+
if (!toolMap.has(mapped))
|
|
314
|
+
toolMap.set(mapped, { ...identity });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const declarations = to === "responses" ? plan.declarations : passthroughTools ?? [];
|
|
318
|
+
const serverTools = plan.serverTools;
|
|
319
|
+
if (plan.dropped.length) {
|
|
374
320
|
// 剥离后必须告知模型,否则模型会声称拥有已被移除的能力。
|
|
375
|
-
system.push(`本通道无法执行以下服务器内置工具,已从本请求移除:${
|
|
321
|
+
system.push(`本通道无法执行以下服务器内置工具,已从本请求移除:${plan.dropped.join("、")}。请勿声称已使用这些工具。`);
|
|
376
322
|
}
|
|
377
323
|
// 请求含图片时声明网关代执行的 analyze_image(GLM 文本模型经此识别图片);
|
|
378
324
|
// 客户端自己声明了同名工具则让位,不注入适配说明避免行为冲突。
|
|
379
|
-
const images = collectZcodeImages(
|
|
380
|
-
const vision = images.length > 0 && !
|
|
325
|
+
const images = collectZcodeImages(messages);
|
|
326
|
+
const vision = images.length > 0 && !toolMap.has(ANALYZE_IMAGE_TOOL_NAME);
|
|
381
327
|
if (vision) {
|
|
382
|
-
|
|
383
|
-
|
|
328
|
+
declarations.push(ANALYZE_IMAGE_TOOL_DECLARATION);
|
|
329
|
+
toolMap.set(ANALYZE_IMAGE_TOOL_NAME, { name: ANALYZE_IMAGE_TOOL_NAME, custom: false, gateway: "analyze_image" });
|
|
384
330
|
system.push(ANALYZE_IMAGE_SYSTEM_NOTE);
|
|
385
331
|
}
|
|
386
|
-
const translated = { model: upstreamModel, stream: true, max_tokens: maxTokens, messages
|
|
332
|
+
const translated = { model: upstreamModel, stream: true, max_tokens: maxTokens, messages };
|
|
387
333
|
if (system.length)
|
|
388
334
|
translated.system = system.join("\n\n");
|
|
389
|
-
if (
|
|
390
|
-
translated.tools = [...
|
|
335
|
+
if (declarations.length || serverTools.length)
|
|
336
|
+
translated.tools = [...declarations, ...serverTools];
|
|
391
337
|
if (body.temperature !== undefined)
|
|
392
338
|
translated.temperature = body.temperature;
|
|
339
|
+
else if (to === "gemini" && record(body.generationConfig) && typeof body.generationConfig.temperature === "number") {
|
|
340
|
+
// gemini 源的采样参数在 generationConfig;不修改调用方原始请求。
|
|
341
|
+
translated.temperature = body.generationConfig.temperature;
|
|
342
|
+
}
|
|
393
343
|
if (body.top_p !== undefined)
|
|
394
344
|
translated.top_p = body.top_p;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
345
|
+
else if (to === "gemini" && record(body.generationConfig) && typeof body.generationConfig.topP === "number") {
|
|
346
|
+
translated.top_p = body.generationConfig.topP;
|
|
347
|
+
}
|
|
348
|
+
if (to === "responses") {
|
|
349
|
+
const choice = translateChoice(body.tool_choice, toolMap, body.parallel_tool_calls);
|
|
350
|
+
if (choice)
|
|
351
|
+
translated.tool_choice = choice;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// chat/anthropic 源的 named choice 已由公共转换映射到同一身份表;服务端搜索与
|
|
355
|
+
// disable_parallel 语义按 anthropic 原生形状透传。
|
|
356
|
+
if (record(passthroughChoice) || typeof passthroughChoice === "string")
|
|
357
|
+
translated.tool_choice = passthroughChoice;
|
|
358
|
+
if (passthroughStopSequences !== undefined)
|
|
359
|
+
translated.stop_sequences = passthroughStopSequences;
|
|
360
|
+
}
|
|
361
|
+
if (to === "responses" || to === "chat-completions") {
|
|
362
|
+
const effort = to === "responses"
|
|
363
|
+
? (record(body.reasoning) && typeof body.reasoning.effort === "string" ? body.reasoning.effort : undefined)
|
|
364
|
+
: (typeof body.reasoning_effort === "string" ? body.reasoning_effort : undefined);
|
|
365
|
+
if (to === "responses" && record(body.reasoning) && body.reasoning.effort === "none") {
|
|
402
366
|
translated.thinking = { type: "disabled" };
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
fail("不支持的 reasoning
|
|
367
|
+
}
|
|
368
|
+
else if (effort !== undefined) {
|
|
369
|
+
if (THINKING_BUDGETS[effort] === undefined)
|
|
370
|
+
fail("不支持的 reasoning 档位");
|
|
407
371
|
const budget = Math.min(THINKING_BUDGETS[effort], maxTokens - 1);
|
|
408
372
|
if (budget < 1024)
|
|
409
373
|
fail("启用 reasoning 至少需要 max_output_tokens 为 1025");
|
|
410
374
|
translated.thinking = { type: "enabled", budget_tokens: budget };
|
|
411
375
|
}
|
|
412
376
|
}
|
|
413
|
-
|
|
377
|
+
if (to === "anthropic" && body.thinking !== undefined && record(body.thinking)) {
|
|
378
|
+
// 原生 Anthropic thinking 输入按同一服务能力验证,不静默覆盖。
|
|
379
|
+
if (body.thinking.type === "enabled") {
|
|
380
|
+
const budget = positiveInteger(body.thinking.budget_tokens, "thinking.budget_tokens");
|
|
381
|
+
if (budget >= maxTokens)
|
|
382
|
+
fail("thinking.budget_tokens 必须小于 max_tokens");
|
|
383
|
+
translated.thinking = { type: "enabled", budget_tokens: budget };
|
|
384
|
+
}
|
|
385
|
+
else if (body.thinking.type === "disabled") {
|
|
386
|
+
translated.thinking = { type: "disabled" };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { body: translated, tools: toolMap, dropped: [...plan.dropped], vision, images };
|
|
390
|
+
}
|
|
391
|
+
function translateChoice(value, tools, parallel) {
|
|
392
|
+
if (parallel !== undefined && typeof parallel !== "boolean")
|
|
393
|
+
fail("parallel_tool_calls 必须是布尔值");
|
|
394
|
+
const noParallel = parallel === false;
|
|
395
|
+
if (value === undefined)
|
|
396
|
+
return noParallel ? { type: "auto", disable_parallel_tool_use: true } : undefined;
|
|
397
|
+
let choice;
|
|
398
|
+
if (value === "auto")
|
|
399
|
+
choice = { type: "auto" };
|
|
400
|
+
else if (value === "none")
|
|
401
|
+
choice = { type: "none" };
|
|
402
|
+
else if (value === "required")
|
|
403
|
+
choice = { type: "any" };
|
|
404
|
+
else if (record(value) && (value.type === "function" || value.type === "custom")) {
|
|
405
|
+
const name = string(value.name, "tool_choice.name");
|
|
406
|
+
const namespace = value.namespace === undefined || value.namespace === null ? undefined : string(value.namespace, "tool_choice.namespace");
|
|
407
|
+
const mapped = zcodeToolMapper({ name, namespace });
|
|
408
|
+
const tool = tools.get(mapped);
|
|
409
|
+
if (!tool || tool.name !== name || tool.namespace !== namespace)
|
|
410
|
+
fail(`工具选择引用了未声明的工具 ${name}`);
|
|
411
|
+
if (tool.custom !== (value.type === "custom"))
|
|
412
|
+
fail("tool_choice 与工具声明类型不一致");
|
|
413
|
+
choice = { type: "tool", name: mapped };
|
|
414
|
+
}
|
|
415
|
+
else if (record(value) && typeof value.type === "string" && value.type !== "") {
|
|
416
|
+
// 指向服务器内置工具(含被剥离工具)的选择无法跨格式表达,省略让上游回到默认 auto。
|
|
417
|
+
return noParallel ? { type: "auto", disable_parallel_tool_use: true } : undefined;
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
fail("不支持的 tool_choice");
|
|
421
|
+
if (noParallel && choice.type !== "none")
|
|
422
|
+
choice.disable_parallel_tool_use = true;
|
|
423
|
+
return choice;
|
|
414
424
|
}
|