@agent-proxy/zcode 0.0.0-stage → 0.11.0

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.
@@ -0,0 +1,414 @@
1
+ import { translateBetweenProviders } from "@agent-proxy/llm-bridge";
2
+ import { createHash } from "node:crypto";
3
+ import { decodeZcodeThinking, isZcodeRecord } from "./wire.js";
4
+ import { ANALYZE_IMAGE_SYSTEM_NOTE, ANALYZE_IMAGE_TOOL_DECLARATION, ANALYZE_IMAGE_TOOL_NAME, collectZcodeImages } from "./vision.js";
5
+ export class ZcodeRequestError extends Error {
6
+ constructor(message) { super(message); this.name = "ZcodeRequestError"; }
7
+ }
8
+ const CUSTOM_INPUT_SCHEMA = {
9
+ type: "object", properties: { input: { type: "string" } }, required: ["input"], additionalProperties: false,
10
+ };
11
+ const THINKING_BUDGETS = {
12
+ minimal: 1024, low: 2048, medium: 4096, high: 8192, xhigh: 10240, max: 10240, ultra: 10240,
13
+ };
14
+ function fail(message) { throw new ZcodeRequestError(message); }
15
+ function record(value) { return isZcodeRecord(value); }
16
+ function string(value, field) {
17
+ if (typeof value !== "string")
18
+ fail(`${field} 必须是字符串`);
19
+ return value;
20
+ }
21
+ function positiveInteger(value, field) {
22
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
23
+ fail(`${field} 必须是正整数`);
24
+ return value;
25
+ }
26
+ function safeToolName(name) {
27
+ if (/^[A-Za-z0-9_-]{1,128}$/.test(name))
28
+ return name;
29
+ const base = name.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+/, "tool_") || "tool";
30
+ return `${base.slice(0, 112)}_${createHash("sha256").update(name).digest("hex").slice(0, 12)}`;
31
+ }
32
+ function addMessage(messages, role, blocks) {
33
+ if (!blocks.length)
34
+ return;
35
+ const previous = messages.at(-1);
36
+ if (previous?.role === role)
37
+ previous.content.push(...blocks);
38
+ else
39
+ messages.push({ role, content: blocks });
40
+ }
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
+ /** z.ai 对 web_search_20250305 的实测回放形状:工具名映射为 web_search_prime,query 走 search_query。 */
127
+ function serverToolQuery(input) {
128
+ for (const key of ["search_query", "query"]) {
129
+ if (typeof input[key] === "string" && input[key])
130
+ return input[key];
131
+ }
132
+ return "";
133
+ }
134
+ function webSearchServerTool(raw) {
135
+ const tool = { type: "web_search_20250305", name: typeof raw.name === "string" && raw.name ? raw.name : "web_search" };
136
+ if (typeof raw.max_uses === "number")
137
+ tool.max_uses = raw.max_uses;
138
+ const filters = record(raw.filters) ? raw.filters : undefined;
139
+ if (filters && Array.isArray(filters.allowed_domains))
140
+ tool.allowed_domains = filters.allowed_domains;
141
+ if (record(raw.user_location))
142
+ tool.user_location = raw.user_location;
143
+ return tool;
144
+ }
145
+ function translateTools(value) {
146
+ const tools = [];
147
+ const serverTools = [];
148
+ const map = new Map();
149
+ const dropped = new Set();
150
+ function visit(entries, namespace, description) {
151
+ if (!Array.isArray(entries))
152
+ fail("tools 必须是数组");
153
+ for (const raw of entries) {
154
+ if (!record(raw))
155
+ fail("tools 包含无效声明");
156
+ if (raw.type === "namespace") {
157
+ if (namespace !== undefined)
158
+ fail("namespace 内仅支持 function 或 custom 工具");
159
+ const segment = string(raw.name, "namespace.name");
160
+ if (!segment)
161
+ fail("namespace.name 不能为空");
162
+ visit(raw.tools, segment, typeof raw.description === "string" ? raw.description : undefined);
163
+ continue;
164
+ }
165
+ if (raw.type === "web_search") {
166
+ // 服务器内置工具按上游原生等价物映射;客户端显式禁用外部访问或嵌在 namespace 内时降级剥离。
167
+ if (raw.external_web_access === false || namespace !== undefined)
168
+ dropped.add("web_search");
169
+ else
170
+ serverTools.push(webSearchServerTool(raw));
171
+ continue;
172
+ }
173
+ if (raw.type !== "function" && raw.type !== "custom") {
174
+ dropped.add(typeof raw.type === "string" ? raw.type : "unknown");
175
+ continue;
176
+ }
177
+ const identity = toolIdentity(raw.name, namespace);
178
+ if (map.has(identity.name))
179
+ fail(`工具名映射冲突:${identity.original}`);
180
+ const custom = raw.type === "custom";
181
+ if (!custom && !record(raw.parameters))
182
+ fail(`function 工具 ${identity.original} 缺少对象 parameters`);
183
+ const tool = { name: identity.name, input_schema: custom ? CUSTOM_INPUT_SCHEMA : raw.parameters };
184
+ const details = [description, typeof raw.description === "string" ? raw.description : undefined].filter((item) => item !== undefined);
185
+ if (details.length)
186
+ tool.description = details.join("\n\n");
187
+ tools.push(tool);
188
+ map.set(identity.name, { name: identity.original, custom, ...(namespace === undefined ? {} : { namespace }) });
189
+ }
190
+ }
191
+ if (value !== undefined)
192
+ visit(value);
193
+ return { tools, map, serverTools, dropped };
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 };
203
+ }
204
+ function webSearchCallQuery(item) {
205
+ const action = record(item.action) ? item.action : {};
206
+ for (const key of ["query", "url"]) {
207
+ if (typeof action[key] === "string" && action[key])
208
+ return action[key];
209
+ }
210
+ if (Array.isArray(action.queries)) {
211
+ const first = action.queries.find((entry) => typeof entry === "string" && entry.length > 0);
212
+ if (first)
213
+ return first;
214
+ }
215
+ return "";
216
+ }
217
+ /** Responses 的 web_search_call id(ws_ 前缀)还原为上游服务端工具 id;空缺时生成同形 id。 */
218
+ function webSearchUpstreamID(item) {
219
+ const raw = typeof item.id === "string" ? item.id.replace(/^ws_/, "").replace(/[^A-Za-z0-9_]/g, "") : "";
220
+ return raw || `call_ws${createHash("sha256").update(JSON.stringify(item)).digest("hex").slice(0, 24)}`;
221
+ }
222
+ function webSearchResultContent(item) {
223
+ if (!Array.isArray(item.results))
224
+ return [];
225
+ const parts = [];
226
+ for (const entry of item.results) {
227
+ if (!record(entry))
228
+ continue;
229
+ const line = [entry.title, entry.url, entry.text].filter((field) => typeof field === "string" && field.length > 0);
230
+ if (line.length)
231
+ parts.push(line.join("\n"));
232
+ }
233
+ return parts.map((text) => ({ type: "text", text }));
234
+ }
235
+ function serverCallSummary(item) {
236
+ const query = item.type === "web_search_call" ? webSearchCallQuery(item) : "";
237
+ const detail = query ? `(查询:${query})` : "";
238
+ return { type: "text", text: `[已省略服务器内置工具调用 ${String(item.type)}${detail};本通道不支持该工具]` };
239
+ }
240
+ function translateInput(input, model, maxTokens, nativeWebSearch) {
241
+ if (typeof input === "string")
242
+ return { messages: bridgeText(input, model, maxTokens), system: [] };
243
+ if (!Array.isArray(input))
244
+ fail("input 必须是字符串或数组");
245
+ const messages = [];
246
+ const system = [];
247
+ const calls = new Map();
248
+ const results = new Set();
249
+ for (const item of input) {
250
+ if (!record(item))
251
+ fail("input 包含非对象 history 项");
252
+ if (item.type === "reasoning") {
253
+ const blocks = decodeZcodeThinking(item.encrypted_content, model);
254
+ // 外部或不同模型的私有载荷不能转发;自身匹配的载荷按原块还原。
255
+ if (blocks)
256
+ addMessage(messages, "assistant", blocks);
257
+ continue;
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
+ }]);
284
+ continue;
285
+ }
286
+ if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
287
+ const callId = string(item.call_id, `${item.type}.call_id`);
288
+ const callType = calls.get(callId);
289
+ if (!callType || (item.type === "function_call_output") !== (callType === "function_call")) {
290
+ fail(`${item.type} 未关联到同一 input 中匹配的工具调用 ${callId}`);
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
+ }]);
298
+ continue;
299
+ }
300
+ if (item.type === "web_search_call") {
301
+ const query = webSearchCallQuery(item);
302
+ if (nativeWebSearch) {
303
+ // 还原为 z.ai 实测回放形状:server_tool_use(web_search_prime/search_query)+ 同轮 tool_result 文本块。
304
+ const id = webSearchUpstreamID(item);
305
+ const content = webSearchResultContent(item);
306
+ addMessage(messages, "assistant", [
307
+ { type: "server_tool_use", id, name: "web_search_prime", input: query ? { search_query: query } : {} },
308
+ { type: "tool_result", tool_use_id: id, content },
309
+ ]);
310
+ }
311
+ else {
312
+ addMessage(messages, "assistant", [serverCallSummary(item)]);
313
+ }
314
+ continue;
315
+ }
316
+ if (typeof item.type === "string" && (item.type.endsWith("_call") || item.type === "mcp_list_tools" || item.type === "mcp_list_resources")) {
317
+ // 其余服务器执行的历史调用(file_search_call、image_generation_call、托管 MCP 等)无法在上游执行,降级为文本摘要。
318
+ addMessage(messages, "assistant", [serverCallSummary(item)]);
319
+ continue;
320
+ }
321
+ fail(`不支持的 Responses history 项类型 ${typeof item.type === "string" ? item.type : "unknown"}`);
322
+ }
323
+ return { messages, system };
324
+ }
325
+ function translateChoice(value, tools, parallel) {
326
+ if (parallel !== undefined && typeof parallel !== "boolean")
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) {
351
+ if (typeof upstreamModel !== "string" || !upstreamModel)
352
+ fail("上游模型不能为空");
353
+ if (body.previous_response_id !== undefined && body.previous_response_id !== null && body.previous_response_id !== "") {
354
+ fail("ZCode 不支持 previous_response_id;请提供完整 input 历史");
355
+ }
356
+ if (body.conversation !== undefined && body.conversation !== null && body.conversation !== "") {
357
+ fail("ZCode 不支持 conversation;请提供完整 input 历史");
358
+ }
359
+ if (body.background === true)
360
+ fail("ZCode 不支持 background 请求");
361
+ for (const format of [body.response_format, record(body.text) ? body.text.format : undefined]) {
362
+ if (format !== undefined && (!record(format) || format.type !== "text"))
363
+ fail("ZCode 不支持 Responses 的结构化输出格式");
364
+ }
365
+ if (body.instructions !== undefined && typeof body.instructions !== "string")
366
+ fail("instructions 必须是字符串");
367
+ const maxTokens = body.max_output_tokens === undefined ? 16384 : positiveInteger(body.max_output_tokens, "max_output_tokens");
368
+ const convertedTools = translateTools(body.tools);
369
+ const nativeWebSearch = convertedTools.serverTools.length > 0;
370
+ const clientModel = body.model === undefined ? upstreamModel : string(body.model, "model");
371
+ const convertedInput = translateInput(body.input, clientModel, maxTokens, nativeWebSearch);
372
+ const system = [...(body.instructions ? [body.instructions] : []), ...convertedInput.system];
373
+ if (convertedTools.dropped.size) {
374
+ // 剥离后必须告知模型,否则模型会声称拥有已被移除的能力。
375
+ system.push(`本通道无法执行以下服务器内置工具,已从本请求移除:${[...convertedTools.dropped].join("、")}。请勿声称已使用这些工具。`);
376
+ }
377
+ // 请求含图片时声明网关代执行的 analyze_image(GLM 文本模型经此识别图片);
378
+ // 客户端自己声明了同名工具则让位,不注入适配说明避免行为冲突。
379
+ const images = collectZcodeImages(convertedInput.messages);
380
+ const vision = images.length > 0 && !convertedTools.map.has(ANALYZE_IMAGE_TOOL_NAME);
381
+ if (vision) {
382
+ convertedTools.tools.push(ANALYZE_IMAGE_TOOL_DECLARATION);
383
+ convertedTools.map.set(ANALYZE_IMAGE_TOOL_NAME, { name: ANALYZE_IMAGE_TOOL_NAME, custom: false, gateway: "analyze_image" });
384
+ system.push(ANALYZE_IMAGE_SYSTEM_NOTE);
385
+ }
386
+ const translated = { model: upstreamModel, stream: true, max_tokens: maxTokens, messages: convertedInput.messages };
387
+ if (system.length)
388
+ translated.system = system.join("\n\n");
389
+ if (convertedTools.tools.length || convertedTools.serverTools.length)
390
+ translated.tools = [...convertedTools.tools, ...convertedTools.serverTools];
391
+ if (body.temperature !== undefined)
392
+ translated.temperature = body.temperature;
393
+ if (body.top_p !== undefined)
394
+ translated.top_p = body.top_p;
395
+ const choice = translateChoice(body.tool_choice, convertedTools.map, body.parallel_tool_calls);
396
+ if (choice)
397
+ translated.tool_choice = choice;
398
+ if (body.reasoning !== undefined) {
399
+ if (!record(body.reasoning))
400
+ fail("reasoning 必须是对象");
401
+ if (body.reasoning.effort === "none")
402
+ translated.thinking = { type: "disabled" };
403
+ else if (body.reasoning.effort !== undefined) {
404
+ const effort = body.reasoning.effort;
405
+ if (typeof effort !== "string" || THINKING_BUDGETS[effort] === undefined)
406
+ fail("不支持的 reasoning.effort");
407
+ const budget = Math.min(THINKING_BUDGETS[effort], maxTokens - 1);
408
+ if (budget < 1024)
409
+ fail("启用 reasoning 至少需要 max_output_tokens 为 1025");
410
+ translated.thinking = { type: "enabled", budget_tokens: budget };
411
+ }
412
+ }
413
+ return { body: translated, tools: convertedTools.map, dropped: [...convertedTools.dropped], vision, images };
414
+ }