@agent-proxy/zcode 0.12.0 → 0.12.2

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 CHANGED
@@ -1,4 +1,4 @@
1
- import type { HostServices, GatewayPlugin } from "@agent-proxy/plugin-api";
1
+ import type { HostServices, GatewayPlugin, ClientProtocol } from "@agent-proxy/plugin-api";
2
2
  import type { ZcodeConfigCache, ZcodeSelection } from "./config.ts";
3
3
  import { ZcodeEndpointRouting } from "./endpoint-routing.ts";
4
4
  import { ZcodeClientSigning } from "./client-signing.ts";
@@ -32,15 +32,13 @@ export interface ZcodeDependencies {
32
32
  export type GatewayHandler = ((request: Request) => Promise<Response>) & {
33
33
  close(): void;
34
34
  };
35
- /** 插件启停仅由 ZCode 开关决定,default 上游路由策略不参与。 */
36
- export declare function zcodeEnabled<Config extends AgentConfig>(config: Config): boolean;
37
- export declare function validateZcodeConfig<Config extends AgentConfig>(config: Config): void;
35
+ export declare function validateZcodeConfig(config: AgentConfig): void;
38
36
  export declare function zcodeError(status: number, message: string, type?: string): Response;
39
- export declare function createZcodeAdapter<Config extends AgentConfig>(config: Config, dependencies?: ZcodeDependencies): {
37
+ export declare function createZcodeAdapter(config: AgentConfig, dependencies?: ZcodeDependencies): {
40
38
  catalog(): Promise<ModelCatalog>;
41
39
  refreshCatalog: () => Promise<ModelCatalog>;
42
40
  reloadCatalog(): Promise<ModelCatalog>;
43
- forward(request: Request, input: Record<string, unknown>, mapResult?: (payload: Record<string, unknown>) => Response): Promise<Response>;
41
+ forward(request: Request, input: Record<string, unknown>, to?: ClientProtocol, mapResult?: (payload: Record<string, unknown>) => Response): Promise<Response>;
44
42
  close(): void;
45
43
  };
46
44
  export declare const ZCODE_MODEL_PATTERNS: readonly string[];
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { createZcodeConfigCache, readZcodeAPIKeyProviders, zcodeConfigPresent, Z
6
6
  import { clearModelsCacheEntries, invalidateModelsCache, withAgentSystemPrompt } from "@agent-proxy/plugin-api";
7
7
  import { buildZcodeVendorCatalog, createZcodeCatalog, isZcodeModel, writeZcodeServedCatalog, zcodeAPIProviderIDFromModel, zcodeModelPlan, zcodeUpstreamModel, } from "./catalog.js";
8
8
  import { translateZcodeRequest, ZcodeRequestError } from "./request.js";
9
- import { createZcodeResponse } from "./response.js";
9
+ import { createZcodeClientResponse } from "./response.js";
10
10
  import { executeZcodeAnalyzeImage, matchZcodeAnalyzeImage } from "./vision.js";
11
11
  import { ZcodeEndpointRouting } from "./endpoint-routing.js";
12
12
  import { clientSigningVerifyRejection, ZcodeClientSigning } from "./client-signing.js";
@@ -14,37 +14,22 @@ import { isZcodeRecord } from "./wire.js";
14
14
  import { buildZcodeModelHeaders, createZcodeContexts, decorateZcodeBody, readZcodeIdentity, zcodePlan, ZCODE_AGENT_SYSTEM_PROMPT } from "./request-context.js";
15
15
  import { localTime, logExchange, logGroupFromPath, requestLogDir } from "@agent-proxy/plugin-api";
16
16
  import { logGatewayError, logRequestSummary } from "@agent-proxy/plugin-api";
17
- /** 插件启停仅由 ZCode 开关决定,default 上游路由策略不参与。 */
18
- export function zcodeEnabled(config) {
19
- return config.zcode === undefined || config.zcode === true;
20
- }
21
17
  export function validateZcodeConfig(config) {
22
- if (config.zcode !== undefined && typeof config.zcode !== "boolean")
23
- throw new Error("zcode 必须为 boolean");
24
- if (!zcodeEnabled(config))
25
- return;
26
18
  const host = config.host;
27
19
  if (!(host === "localhost" || host === "::1" || host === "[::1]" || (isIP(host) === 4 && host.startsWith("127.")))) {
28
20
  throw new Error("启用 ZCode 时网关只能监听环回地址");
29
21
  }
30
- if (config.prefix && ("zcode/".startsWith(config.prefix) || config.prefix.startsWith("zcode/")
31
- || config.prefix.startsWith("zcode-"))) {
32
- throw new Error("启用 ZCode 时 zcode/ 与 zcode- 前缀保留给 ZCode,请调整第三方 prefix");
33
- }
34
22
  }
35
23
  export function zcodeError(status, message, type = "invalid_request_error") {
36
24
  return Response.json({ error: { type, message } }, { status });
37
25
  }
38
26
  function buildZcodeAdapter(config, dependencies = {}) {
39
27
  validateZcodeConfig(config);
40
- const enabled = zcodeEnabled(config);
41
28
  const manual = dependencies.catalogMode === "manual";
42
29
  // 自管目录文件一律放运行主目录,不从 catalogPath 位置倒推。
43
30
  const cacheDirectory = dependencies.cacheDirectory ?? resolvePaths().runtimeHome;
44
31
  // 厂商元数据只取模型包预设;账号或套餐刷新仅重新投影支持集合。
45
- const vendorCatalog = enabled
46
- ? buildZcodeVendorCatalog()
47
- : undefined;
32
+ const vendorCatalog = buildZcodeVendorCatalog();
48
33
  /** 套餐或选择变化时立即撤下 Codex 缓存里的旧 ZCode 条目,避免旧套餐模型继续可选。 */
49
34
  const dropCachedZcodeModels = () => {
50
35
  if (dependencies.codexModelsCacheFile) {
@@ -84,8 +69,6 @@ function buildZcodeAdapter(config, dependencies = {}) {
84
69
  ];
85
70
  let synchronizeAPIKeyRoutes = () => { };
86
71
  const republishFromPlans = (force = false) => {
87
- if (!vendorCatalog)
88
- return;
89
72
  const current = planRoutes.map((route) => route.snapshot);
90
73
  const previous = publishedSnapshots;
91
74
  if (!force && previous && current.length === previous.length
@@ -108,121 +91,119 @@ function buildZcodeAdapter(config, dependencies = {}) {
108
91
  }
109
92
  publishServedCatalog({ models });
110
93
  };
111
- if (enabled) {
112
- // 注入模式(测试)只为显式给出的套餐建缓存,避免读到真实的 ~/.zcode。
113
- const injected = dependencies.planCaches
114
- ?? (dependencies.apiKeyCaches ? {} : dependencies.configCache ? { "api-key": dependencies.configCache } : undefined);
115
- for (const route of planRoutes) {
116
- route.cache = injected?.[route.plan]
117
- ?? (injected ? undefined : createZcodeConfigCache(home, {
118
- plan: route.plan,
119
- refreshOnStart: false,
120
- // 选择变化只撤下本套餐的快照并重发并集;另一个套餐的条目不受影响。
121
- onSelectionChange: () => {
122
- if (manual)
123
- return;
124
- route.snapshot = undefined;
125
- republishFromPlans();
126
- },
127
- onSnapshotChange: (snapshot) => {
128
- if (manual)
129
- return;
130
- route.snapshot = snapshot;
131
- republishFromPlans();
132
- },
133
- }));
94
+ // 注入模式(测试)只为显式给出的套餐建缓存,避免读到真实的 ~/.zcode。
95
+ const injected = dependencies.planCaches
96
+ ?? (dependencies.apiKeyCaches ? {} : dependencies.configCache ? { "api-key": dependencies.configCache } : undefined);
97
+ for (const route of planRoutes) {
98
+ route.cache = injected?.[route.plan]
99
+ ?? (injected ? undefined : createZcodeConfigCache(home, {
100
+ plan: route.plan,
101
+ refreshOnStart: false,
102
+ // 选择变化只撤下本套餐的快照并重发并集;另一个套餐的条目不受影响。
103
+ onSelectionChange: () => {
104
+ if (manual)
105
+ return;
106
+ route.snapshot = undefined;
107
+ republishFromPlans();
108
+ },
109
+ onSnapshotChange: (snapshot) => {
110
+ if (manual)
111
+ return;
112
+ route.snapshot = snapshot;
113
+ republishFromPlans();
114
+ },
115
+ }));
116
+ }
117
+ if (injected) {
118
+ const apiCaches = dependencies.apiKeyCaches
119
+ ?? (dependencies.planCaches?.["api-key"] || dependencies.configCache
120
+ ? { current: dependencies.planCaches?.["api-key"] ?? dependencies.configCache }
121
+ : {});
122
+ for (const [apiProviderID, cache] of Object.entries(apiCaches)) {
123
+ planRoutes.push({ plan: "api-key", ...(apiProviderID === "current" ? {} : { apiProviderID }), cache });
134
124
  }
135
- if (injected) {
136
- const apiCaches = dependencies.apiKeyCaches
137
- ?? (dependencies.planCaches?.["api-key"] || dependencies.configCache
138
- ? { current: dependencies.planCaches?.["api-key"] ?? dependencies.configCache }
139
- : {});
140
- for (const [apiProviderID, cache] of Object.entries(apiCaches)) {
141
- planRoutes.push({ plan: "api-key", ...(apiProviderID === "current" ? {} : { apiProviderID }), cache });
125
+ }
126
+ else {
127
+ const createAPIRoute = (provider) => {
128
+ const route = { plan: "api-key", apiProviderID: provider.providerID };
129
+ route.cache = createZcodeConfigCache(home, {
130
+ plan: "api-key",
131
+ apiProviderID: provider.providerID,
132
+ refreshOnStart: false,
133
+ onSelectionChange: () => {
134
+ if (manual)
135
+ return;
136
+ route.snapshot = undefined;
137
+ republishFromPlans();
138
+ },
139
+ onSnapshotChange: (snapshot) => {
140
+ if (manual)
141
+ return;
142
+ route.snapshot = snapshot;
143
+ republishFromPlans();
144
+ },
145
+ });
146
+ return route;
147
+ };
148
+ const synchronize = (republish = true, strict = false) => {
149
+ let providers = [];
150
+ try {
151
+ providers = readZcodeAPIKeyProviders(home);
142
152
  }
143
- }
144
- else {
145
- const createAPIRoute = (provider) => {
146
- const route = { plan: "api-key", apiProviderID: provider.providerID };
147
- route.cache = createZcodeConfigCache(home, {
148
- plan: "api-key",
149
- apiProviderID: provider.providerID,
150
- refreshOnStart: false,
151
- onSelectionChange: () => {
152
- if (manual)
153
- return;
154
- route.snapshot = undefined;
155
- republishFromPlans();
156
- },
157
- onSnapshotChange: (snapshot) => {
158
- if (manual)
159
- return;
160
- route.snapshot = snapshot;
161
- republishFromPlans();
162
- },
163
- });
164
- return route;
165
- };
166
- const synchronize = (republish = true, strict = false) => {
167
- let providers = [];
168
- try {
169
- providers = readZcodeAPIKeyProviders(home);
170
- }
171
- catch (error) {
172
- if (strict)
173
- throw error;
174
- /* 动态目录撤下无效 API Key;套餐路由不受影响。 */
175
- }
176
- const targetIDs = new Set(providers.map((provider) => provider.providerID));
177
- let changed = false;
178
- const removed = planRoutes.filter((route) => route.plan === "api-key" && route.apiProviderID && !targetIDs.has(route.apiProviderID));
179
- for (const route of removed)
180
- route.cache?.close();
181
- if (removed.length) {
182
- for (let index = planRoutes.length - 1; index >= 0; index--) {
183
- if (removed.includes(planRoutes[index]))
184
- planRoutes.splice(index, 1);
185
- }
186
- changed = true;
187
- }
188
- for (const provider of providers) {
189
- if (planRoutes.some((route) => route.plan === "api-key" && route.apiProviderID === provider.providerID))
190
- continue;
191
- planRoutes.push(createAPIRoute(provider));
192
- changed = true;
153
+ catch (error) {
154
+ if (strict)
155
+ throw error;
156
+ /* 动态目录撤下无效 API Key;套餐路由不受影响。 */
157
+ }
158
+ const targetIDs = new Set(providers.map((provider) => provider.providerID));
159
+ let changed = false;
160
+ const removed = planRoutes.filter((route) => route.plan === "api-key" && route.apiProviderID && !targetIDs.has(route.apiProviderID));
161
+ for (const route of removed)
162
+ route.cache?.close();
163
+ if (removed.length) {
164
+ for (let index = planRoutes.length - 1; index >= 0; index--) {
165
+ if (removed.includes(planRoutes[index]))
166
+ planRoutes.splice(index, 1);
193
167
  }
194
- if (changed && republish)
195
- republishFromPlans();
196
- };
197
- synchronizeAPIKeyRoutes = (strict) => synchronize(!manual, strict);
198
- synchronize(false);
199
- // /models 与未知 API 前缀请求都会重新同步,支持新增/删除 provider;现有
200
- // provider 的 Key 与模型变化由各自 cache 的 provider_config watcher 驱动。
201
- }
168
+ changed = true;
169
+ }
170
+ for (const provider of providers) {
171
+ if (planRoutes.some((route) => route.plan === "api-key" && route.apiProviderID === provider.providerID))
172
+ continue;
173
+ planRoutes.push(createAPIRoute(provider));
174
+ changed = true;
175
+ }
176
+ if (changed && republish)
177
+ republishFromPlans();
178
+ };
179
+ synchronizeAPIKeyRoutes = (strict) => synchronize(!manual, strict);
180
+ synchronize(false);
181
+ // /models 与未知 API 前缀请求都会重新同步,支持新增/删除 provider;现有
182
+ // provider 的 Key 与模型变化由各自 cache 的 provider_config watcher 驱动。
202
183
  }
203
184
  const fetchUpstream = dependencies.fetch ?? ((url, init) => fetch(url, init));
204
185
  const activeRequests = new Set();
205
- const identity = enabled ? dependencies.identity ?? readZcodeIdentity() : undefined;
206
- const endpointRouting = enabled && dependencies.endpointRouting !== null
186
+ const identity = dependencies.identity ?? readZcodeIdentity();
187
+ const endpointRouting = dependencies.endpointRouting !== null
207
188
  ? dependencies.endpointRouting ?? new ZcodeEndpointRouting({
208
- identity: identity,
189
+ identity,
209
190
  fetch: (url, init) => fetchUpstream(url, init),
210
191
  })
211
192
  : undefined;
212
193
  // coding plan 官方客户端签名:逐请求补 X-Client-* 签名头;fail-open,失败按未签名继续。
213
- const clientSigning = enabled && dependencies.clientSigning !== null
194
+ const clientSigning = dependencies.clientSigning !== null
214
195
  ? dependencies.clientSigning ?? new ZcodeClientSigning({ fetch: (url, init) => fetchUpstream(url, init) })
215
196
  : undefined;
216
197
  const contexts = createZcodeContexts();
217
198
  const sink = config.requestLogging === true ? {
218
- dir: requestLogDir(config),
199
+ dir: requestLogDir(),
219
200
  maxLogs: Math.max(0, Math.trunc(config.maxRequestLogs ?? 0)),
220
201
  processLog: dependencies.processLog,
221
202
  } : undefined;
222
203
  let closed = false;
223
204
  let refreshPromise;
224
205
  const readCatalog = async (mode) => {
225
- if (closed || !enabled)
206
+ if (closed)
226
207
  return { models: [] };
227
208
  synchronizeAPIKeyRoutes(manual && mode !== "get");
228
209
  const neverPublished = publishedSnapshots === undefined;
@@ -279,7 +260,7 @@ function buildZcodeAdapter(config, dependencies = {}) {
279
260
  return promise;
280
261
  };
281
262
  /** 启动首读仍允许凭据授权,手动策略仅停止后续目录自动发布。 */
282
- const startup = enabled && vendorCatalog && dependencies.refreshCatalogOnStart !== false
263
+ const startup = dependencies.refreshCatalogOnStart !== false
283
264
  ? refreshCatalog() : Promise.resolve({ models: [] });
284
265
  // 构造适配器与等待 modelsReady 之间允许异步间隙;失败由公开目录接口传递。
285
266
  void startup.catch(() => { });
@@ -297,7 +278,7 @@ function buildZcodeAdapter(config, dependencies = {}) {
297
278
  await refreshPromise;
298
279
  return readCatalog("reload");
299
280
  },
300
- async forward(request, input, mapResult) {
281
+ async forward(request, input, to = "responses", mapResult) {
301
282
  const start = Date.now();
302
283
  const requestTime = localTime();
303
284
  const incoming = new URL(request.url);
@@ -426,7 +407,7 @@ function buildZcodeAdapter(config, dependencies = {}) {
426
407
  cleanup();
427
408
  return fail(404, "此模型不属于 ZCode 当前套餐或厂商目录");
428
409
  }
429
- const translated = translateZcodeRequest(input, model);
410
+ const translated = await translateZcodeRequest(input, model, to);
430
411
  const baseUpstreamUrl = `${snapshot.baseURL}${snapshot.baseURL.endsWith("/v1") ? "/messages" : "/v1/messages"}`;
431
412
  const routedUrl = endpointRouting
432
413
  // 跟随 z.ai 服务端下发的端点映射(如 zcode.z.ai ultra 中转);resolve 内部 fail-open,失败保持原 URL。
@@ -562,7 +543,7 @@ function buildZcodeAdapter(config, dependencies = {}) {
562
543
  }
563
544
  let chunks = "";
564
545
  const stream = !mapResult && input.stream === true;
565
- const response = await createZcodeResponse(upstream, {
546
+ const response = await createZcodeClientResponse(to, upstream, {
566
547
  model: String(input.model), tools: translated.tools, stream, signal: abort.signal,
567
548
  gatewayTools,
568
549
  sanitizeError: (error) => ({
@@ -637,9 +618,7 @@ export function createZcodePlugin(host, dependencies = {}) {
637
618
  const input = await context.body.json();
638
619
  if (!input || typeof input !== "object" || Array.isArray(input))
639
620
  return Response.json({ error: { type: "invalid_request_error", message: "请求正文必须是 JSON 对象" } }, { status: 400 });
640
- if (context.from !== "responses")
641
- return Response.json({ error: { type: "invalid_request_error", message: "Zcode 模型仅支持 Responses API" } }, { status: 400 });
642
- return adapter.forward(context.request, input);
621
+ return adapter.forward(context.request, input, context.to);
643
622
  },
644
623
  getCatalog: () => adapter.catalog(),
645
624
  refreshCatalog: () => adapter.refreshCatalog(),
package/dist/request.d.ts CHANGED
@@ -1,5 +1,16 @@
1
+ import type { ClientProtocol } from "@agent-proxy/plugin-api";
1
2
  import type { ZcodeTranslatedRequest } from "./wire.ts";
3
+ /**
4
+ * ZCode 请求准备:标准消息/工具/参数统一经 @agent-proxy/llm-bridge 的
5
+ * convertRequest(to, "anthropic");z.ai 私有形状(web_search_20250305 服务端工具、
6
+ * thinking 预算表、zcode-thinking 信封回放、analyze_image 代执行)作为插件前后处理。
7
+ *
8
+ * Responses 源的私有 history 项(reasoning / web_search_call / 其它服务器调用)不能
9
+ * 进标准转换:按项把 input 切成「标准段 + 私有项」,标准段逐段调用公共入口,私有段
10
+ * 由本模块转换,最后按 main 的同角色合并语义拼装。
11
+ */
2
12
  export declare class ZcodeRequestError extends Error {
3
13
  constructor(message: string);
4
14
  }
5
- export declare function translateZcodeRequest(body: Record<string, unknown>, upstreamModel: string): ZcodeTranslatedRequest;
15
+ /** to 为路由端点(客户端入口)协议:请求方向 to → anthropic,响应方向 anthropic → to。 */
16
+ export declare function translateZcodeRequest(body: Record<string, unknown>, upstreamModel: string, to?: ClientProtocol): Promise<ZcodeTranslatedRequest>;
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
  }
@@ -1,3 +1,4 @@
1
+ import type { ClientProtocol } from "@agent-proxy/plugin-api";
1
2
  import { type ZcodeToolMap } from "./wire.ts";
2
3
  import { type ZcodeGatewayCall } from "./vision.ts";
3
4
  /** 网关代执行工具(analyze_image)的驱动层钩子:执行调用并按需发起上游续跑。 */
@@ -24,4 +25,11 @@ export interface ZcodeResponseOptions {
24
25
  /** 声明了网关代执行工具时必须提供;否则吸收到相关调用会让响应失败。 */
25
26
  gatewayTools?: ZcodeGatewayToolsHook;
26
27
  }
28
+ /**
29
+ * 四种客户端协议的统一入口:zcode 始终以帧级驱动器合成 Responses 事件(多腿、
30
+ * server tool 折叠与 thinking 信封都在该层),chat-completions / anthropic / gemini
31
+ * 客户端由 @agent-proxy/llm-bridge 从同一事件流渲染,语义与 Responses 输出一致。
32
+ */
33
+ /** to 为路由端点(客户端入口)协议:上游 anthropic → to。 */
34
+ export declare function createZcodeClientResponse(to: ClientProtocol, upstream: Response, options: ZcodeResponseOptions): Promise<Response>;
27
35
  export declare function createZcodeResponse(upstream: Response, options: ZcodeResponseOptions): Promise<Response>;
package/dist/response.js CHANGED
@@ -1,4 +1,4 @@
1
- import { parseAnthropicStream } from "@agent-proxy/llm-bridge";
1
+ import { createClientResponse } from "@agent-proxy/llm-bridge";
2
2
  import { encodeZcodeThinking, isZcodeRecord } from "./wire.js";
3
3
  import { gatewayToolCallNarration, gatewayToolResultNarration } from "./vision.js";
4
4
  /** z.ai 的服务端搜索按 web_search_prime/search_query 形状返回 query;兼容 Anthropic 标准的 query。 */
@@ -108,29 +108,74 @@ async function* readFrames(reader) {
108
108
  }
109
109
  }
110
110
  }
111
- /** 库只保存一个工具上下文;按帧补入对应块,使交错的块仍通过库正确归一化。 */
111
+ /**
112
+ * anthropic 帧级归一化(工具事件抽取)。库的 parseAnthropicStream 只保存一个工具
113
+ * 上下文,交错块会串扰;这里直接从帧结构提取 tool_call_start/delta 事件,不依赖
114
+ * npm llm-bridge(桥接包不再转发库原语)。
115
+ */
112
116
  async function normalize(frame, block) {
113
- const frames = [];
114
- if (block?.source.type === "tool_use" && (frame.type === "content_block_delta" || frame.type === "content_block_stop")) {
115
- frames.push({ type: "content_block_start", index: frame.index, content_block: block.source });
117
+ const events = [];
118
+ if (frame.type === "message_start") {
119
+ // 驱动器以事件存在性校验 message_start 的可归一化;保持该合同。
120
+ return [{ type: "message_start" }];
116
121
  }
117
- frames.push(frame);
118
- const stream = new ReadableStream({
119
- start(controller) {
120
- controller.enqueue(encoder.encode(frames.map((value) => `event: ${value.type}\ndata: ${JSON.stringify(value)}\n\n`).join("")));
121
- controller.close();
122
- },
123
- });
124
- const result = [];
125
- let skipContext = frames.length > 1;
126
- for await (const value of parseAnthropicStream(stream)) {
127
- if (skipContext) {
128
- skipContext = false;
129
- continue;
122
+ if (frame.type === "error") {
123
+ // 错误帧透传原始字段(request_id 等),并把 anthropic 的 type 映射为 code
124
+ //(驱动器按 code 生成 failed 终态的错误标识)。
125
+ const original = record(frame.error, "error");
126
+ return [{ type: "error", error: { ...original, ...(original.code === undefined && typeof original.type === "string" ? { code: original.type } : {}) } }];
127
+ }
128
+ if (frame.type === "content_block_start") {
129
+ const source = record(frame.content_block, "content_block");
130
+ if (source.type === "tool_use") {
131
+ events.push({ type: "tool_call_start", tool_call: { id: source.id ?? "", name: source.name ?? "" } });
132
+ }
133
+ return events;
134
+ }
135
+ if (frame.type === "content_block_delta") {
136
+ const delta = record(frame.delta, "delta");
137
+ if (block?.source.type === "tool_use") {
138
+ if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
139
+ events.push({ type: "tool_call_delta", tool_call: { id: block.source.id ?? "", arguments_delta: delta.partial_json } });
140
+ }
141
+ return events;
142
+ }
143
+ // 文本/思考增量保持 content_delta 事件形状,驱动器按块类型取对应字段。
144
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
145
+ events.push({ type: "content_delta", delta: { text: delta.text } });
146
+ }
147
+ else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
148
+ events.push({ type: "content_delta", delta: { thinking: delta.thinking } });
130
149
  }
131
- result.push(value);
150
+ return events;
132
151
  }
133
- return result;
152
+ if (frame.type === "content_block_stop" && block?.source.type === "tool_use") {
153
+ events.push({ type: "tool_call_end", tool_call: { id: block.source.id ?? "" } });
154
+ return events;
155
+ }
156
+ return events;
157
+ }
158
+ /**
159
+ * 四种客户端协议的统一入口:zcode 始终以帧级驱动器合成 Responses 事件(多腿、
160
+ * server tool 折叠与 thinking 信封都在该层),chat-completions / anthropic / gemini
161
+ * 客户端由 @agent-proxy/llm-bridge 从同一事件流渲染,语义与 Responses 输出一致。
162
+ */
163
+ /** to 为路由端点(客户端入口)协议:上游 anthropic → to。 */
164
+ export async function createZcodeClientResponse(to, upstream, options) {
165
+ const responsesOutput = await createZcodeResponse(upstream, options);
166
+ if (to === "responses")
167
+ return responsesOutput;
168
+ const result = await createClientResponse(to, responsesOutput, {
169
+ upstreamProtocol: "responses",
170
+ model: options.model,
171
+ stream: options.stream,
172
+ signal: options.signal,
173
+ abort: options.abort,
174
+ onChunk: options.onChunk,
175
+ sanitizeError: options.sanitizeError,
176
+ // Responses 输出已由 zcode 驱动器还原原始工具名,无需再做身份映射。
177
+ });
178
+ return result.response;
134
179
  }
135
180
  export async function createZcodeResponse(upstream, options) {
136
181
  let reader = upstream.body?.getReader();
package/dist/wire.d.ts CHANGED
@@ -1,8 +1,6 @@
1
+ import type { ToolIdentity } from "@agent-proxy/llm-bridge";
1
2
  /** ZCode 协议共享类型;元数据不包含授权凭据。 */
2
- export interface ZcodeToolDefinition {
3
- name: string;
4
- custom: boolean;
5
- namespace?: string;
3
+ export interface ZcodeToolDefinition extends ToolIdentity {
6
4
  /** 网关代执行标记:该工具由网关在上游续跑中执行并吸收,不回传客户端。 */
7
5
  gateway?: "analyze_image";
8
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-proxy/zcode",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Agent Proxy zcode 插件;保留基线转换行为。",
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,7 +26,8 @@
26
26
  "prepack": "bun ../../scripts/verify-package.ts ."
27
27
  },
28
28
  "publishConfig": {
29
- "access": "public"
29
+ "access": "public",
30
+ "timeout": 30000
30
31
  },
31
32
  "license": "MIT",
32
33
  "repository": {
@@ -34,11 +35,11 @@
34
35
  "url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
35
36
  },
36
37
  "devDependencies": {
37
- "@agent-proxy/models": "0.12.0"
38
+ "@agent-proxy/models": "0.12.2"
38
39
  },
39
40
  "dependencies": {
40
- "@agent-proxy/llm-bridge": "0.12.0",
41
- "@agent-proxy/plugin-api": "0.12.0"
41
+ "@agent-proxy/llm-bridge": "0.12.2",
42
+ "@agent-proxy/plugin-api": "0.12.2"
42
43
  },
43
- "gitHead": "d589acd7af461ab0bc38d1b89b59c68b84731ea1"
44
+ "gitHead": "3461eb517c21cd330f2c3495f6bc01ca4191690c"
44
45
  }