@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/request.js CHANGED
@@ -1,9 +1,21 @@
1
- import { translateBetweenProviders } from "@agent-proxy/llm-bridge";
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) { super(message); this.name = "ZcodeRequestError"; }
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) { throw new ZcodeRequestError(message); }
15
- function record(value) { return isZcodeRecord(value); }
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
- function translateTools(value) {
146
- const tools = [];
78
+ /** Responses 工具声明规划(web_search 服务端工具、custom/namespace 安全名身份表)。 */
79
+ function planResponsesTools(value, map) {
80
+ const declarations = [];
147
81
  const serverTools = [];
148
- const map = new Map();
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 : undefined);
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.add("web_search");
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.add(typeof raw.type === "string" ? raw.type : "unknown");
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 工具 ${identity.original} 缺少对象 parameters`);
183
- const tool = { name: identity.name, input_schema: custom ? CUSTOM_INPUT_SCHEMA : raw.parameters };
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
- tools.push(tool);
188
- map.set(identity.name, { name: identity.original, custom, ...(namespace === undefined ? {} : { namespace }) });
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 { 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 };
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
- function translateInput(input, model, maxTokens, nativeWebSearch) {
241
- if (typeof input === "string")
242
- return { messages: bridgeText(input, model, maxTokens), system: [] };
243
- if (!Array.isArray(input))
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 messages = [];
246
- const system = [];
247
- const calls = new Map();
248
- const results = new Set();
249
- for (const item of input) {
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
- 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
- }]);
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
- 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
- }]);
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 (item.type === "web_search_call") {
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
- 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
- ]);
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
- addMessage(messages, "assistant", [serverCallSummary(item)]);
232
+ legs.push({ private: [serverCallSummary(item)] });
313
233
  }
314
234
  continue;
315
235
  }
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"}`);
236
+ // 其余服务器执行的历史调用(file_search_call、image_generation_call、托管 MCP 等)无法在上游执行,降级为文本摘要。
237
+ legs.push({ private: [serverCallSummary(item)] });
322
238
  }
323
- return { messages, system };
239
+ flush();
240
+ return legs;
324
241
  }
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) {
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
- 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;
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 convertedInput = translateInput(body.input, clientModel, maxTokens, nativeWebSearch);
372
- const system = [...(body.instructions ? [body.instructions] : []), ...convertedInput.system];
373
- if (convertedTools.dropped.size) {
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(`本通道无法执行以下服务器内置工具,已从本请求移除:${[...convertedTools.dropped].join("、")}。请勿声称已使用这些工具。`);
321
+ system.push(`本通道无法执行以下服务器内置工具,已从本请求移除:${plan.dropped.join("、")}。请勿声称已使用这些工具。`);
376
322
  }
377
323
  // 请求含图片时声明网关代执行的 analyze_image(GLM 文本模型经此识别图片);
378
324
  // 客户端自己声明了同名工具则让位,不注入适配说明避免行为冲突。
379
- const images = collectZcodeImages(convertedInput.messages);
380
- const vision = images.length > 0 && !convertedTools.map.has(ANALYZE_IMAGE_TOOL_NAME);
325
+ const images = collectZcodeImages(messages);
326
+ const vision = images.length > 0 && !toolMap.has(ANALYZE_IMAGE_TOOL_NAME);
381
327
  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" });
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: convertedInput.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 (convertedTools.tools.length || convertedTools.serverTools.length)
390
- translated.tools = [...convertedTools.tools, ...convertedTools.serverTools];
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
- 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")
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
- 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");
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
- return { body: translated, tools: convertedTools.map, dropped: [...convertedTools.dropped], vision, images };
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
  }