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