@agent-proxy/zcode 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +19 -2
- package/dist/catalog.d.ts +30 -0
- package/dist/catalog.js +150 -0
- package/dist/client-signing.d.ts +46 -0
- package/dist/client-signing.js +225 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +944 -0
- package/dist/credential-cipher.d.ts +2 -0
- package/dist/credential-cipher.js +43 -0
- package/dist/endpoint-routing.d.ts +33 -0
- package/dist/endpoint-routing.js +147 -0
- package/dist/fingerprint-data.json +12 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +648 -0
- package/dist/individual-credentials.d.ts +19 -0
- package/dist/individual-credentials.js +39 -0
- package/dist/plans.d.ts +19 -0
- package/dist/plans.js +74 -0
- package/dist/request-context.d.ts +39 -0
- package/dist/request-context.js +213 -0
- package/dist/request.d.ts +5 -0
- package/dist/request.js +414 -0
- package/dist/resources/models.js +3854 -0
- package/dist/response.d.ts +27 -0
- package/dist/response.js +725 -0
- package/dist/team-credentials.d.ts +12 -0
- package/dist/team-credentials.js +123 -0
- package/dist/vision-template.json +1 -0
- package/dist/vision.d.ts +62 -0
- package/dist/vision.js +204 -0
- package/dist/wire.d.ts +23 -0
- package/dist/wire.js +25 -0
- package/package.json +42 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { bindHostMethods, withHostServices } from "@agent-proxy/plugin-api";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
import { resolvePaths } from "@agent-proxy/plugin-api";
|
|
5
|
+
import { createZcodeConfigCache, readZcodeAPIKeyProviders, zcodeConfigPresent, ZcodeConfigError } from "./config.js";
|
|
6
|
+
import { clearModelsCacheEntries, invalidateModelsCache, withAgentSystemPrompt } from "@agent-proxy/plugin-api";
|
|
7
|
+
import { buildZcodeVendorCatalog, createZcodeCatalog, isZcodeModel, writeZcodeServedCatalog, zcodeAPIProviderIDFromModel, zcodeModelPlan, zcodeUpstreamModel, } from "./catalog.js";
|
|
8
|
+
import { translateZcodeRequest, ZcodeRequestError } from "./request.js";
|
|
9
|
+
import { createZcodeResponse } from "./response.js";
|
|
10
|
+
import { executeZcodeAnalyzeImage, matchZcodeAnalyzeImage } from "./vision.js";
|
|
11
|
+
import { ZcodeEndpointRouting } from "./endpoint-routing.js";
|
|
12
|
+
import { clientSigningVerifyRejection, ZcodeClientSigning } from "./client-signing.js";
|
|
13
|
+
import { isZcodeRecord } from "./wire.js";
|
|
14
|
+
import { buildZcodeModelHeaders, createZcodeContexts, decorateZcodeBody, readZcodeIdentity, zcodePlan, ZCODE_AGENT_SYSTEM_PROMPT } from "./request-context.js";
|
|
15
|
+
import { localTime, logExchange, logGroupFromPath, requestLogDir } from "@agent-proxy/plugin-api";
|
|
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
|
+
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
|
+
const host = config.host;
|
|
27
|
+
if (!(host === "localhost" || host === "::1" || host === "[::1]" || (isIP(host) === 4 && host.startsWith("127.")))) {
|
|
28
|
+
throw new Error("启用 ZCode 时网关只能监听环回地址");
|
|
29
|
+
}
|
|
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
|
+
}
|
|
35
|
+
export function zcodeError(status, message, type = "invalid_request_error") {
|
|
36
|
+
return Response.json({ error: { type, message } }, { status });
|
|
37
|
+
}
|
|
38
|
+
function buildZcodeAdapter(config, dependencies = {}) {
|
|
39
|
+
validateZcodeConfig(config);
|
|
40
|
+
const enabled = zcodeEnabled(config);
|
|
41
|
+
const manual = dependencies.catalogMode === "manual";
|
|
42
|
+
// 自管目录文件一律放运行主目录,不从 catalogPath 位置倒推。
|
|
43
|
+
const cacheDirectory = dependencies.cacheDirectory ?? resolvePaths().runtimeHome;
|
|
44
|
+
// 厂商元数据只取模型包预设;账号或套餐刷新仅重新投影支持集合。
|
|
45
|
+
const vendorCatalog = enabled
|
|
46
|
+
? buildZcodeVendorCatalog()
|
|
47
|
+
: undefined;
|
|
48
|
+
/** 套餐或选择变化时立即撤下 Codex 缓存里的旧 ZCode 条目,避免旧套餐模型继续可选。 */
|
|
49
|
+
const dropCachedZcodeModels = () => {
|
|
50
|
+
if (dependencies.codexModelsCacheFile) {
|
|
51
|
+
clearModelsCacheEntries(dependencies.codexModelsCacheFile, isZcodeModel);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const servedCatalogFile = path.join(cacheDirectory, "zcode-catalog.json");
|
|
55
|
+
/** 当前对外目录(各套餐作用域前缀的并集)。watch 或启动时重建,/v1/models 只读取这里。 */
|
|
56
|
+
let servedCatalog = { models: [] };
|
|
57
|
+
/** 已发布目录对应的各套餐快照;仅在身份变化时才重算,避免每次 /v1/models 重新求交集。 */
|
|
58
|
+
let publishedSnapshots;
|
|
59
|
+
const home = dependencies.zcodeHome ?? path.join(resolvePaths().home, ".zcode");
|
|
60
|
+
const injectedCaches = dependencies.planCaches !== undefined || dependencies.apiKeyCaches !== undefined
|
|
61
|
+
|| dependencies.configCache !== undefined;
|
|
62
|
+
/** 收敛 Codex 缓存:撤下已不在当前对外目录里的 ZCode 条目;返回是否发生了删除。 */
|
|
63
|
+
const pruneCodexCache = (catalog) => {
|
|
64
|
+
if (!dependencies.codexModelsCacheFile)
|
|
65
|
+
return false;
|
|
66
|
+
const current = new Set(catalog.models.map((model) => model.slug.toLowerCase()));
|
|
67
|
+
return clearModelsCacheEntries(dependencies.codexModelsCacheFile, (slug) => isZcodeModel(slug) && !current.has(slug.toLowerCase()));
|
|
68
|
+
};
|
|
69
|
+
const publishServedCatalog = (catalog) => {
|
|
70
|
+
// 系统提示词在发布成品时替换(含 model_messages 模板):zcode-catalog.json 落盘即终态,
|
|
71
|
+
// 与其它适配器的合成期替换对齐;Codex 客户端优先按模板渲染系统提示词,只改
|
|
72
|
+
// base_instructions 无效(2026-10-09 实测)。
|
|
73
|
+
servedCatalog = { models: catalog.models.map((entry) => withAgentSystemPrompt(entry, ZCODE_AGENT_SYSTEM_PROMPT)) };
|
|
74
|
+
const changed = writeZcodeServedCatalog(servedCatalogFile, servedCatalog);
|
|
75
|
+
if (!changed || !dependencies.codexModelsCacheFile)
|
|
76
|
+
return;
|
|
77
|
+
// 目录变化:既撤下已下线条目,也过期新鲜度让 Codex 主动重拉一次。
|
|
78
|
+
pruneCodexCache(servedCatalog);
|
|
79
|
+
invalidateModelsCache(dependencies.codexModelsCacheFile);
|
|
80
|
+
};
|
|
81
|
+
const planRoutes = [
|
|
82
|
+
{ plan: "individual-coding-plan" },
|
|
83
|
+
{ plan: "team-coding-plan" },
|
|
84
|
+
];
|
|
85
|
+
let synchronizeAPIKeyRoutes = () => { };
|
|
86
|
+
const republishFromPlans = (force = false) => {
|
|
87
|
+
if (!vendorCatalog)
|
|
88
|
+
return;
|
|
89
|
+
const current = planRoutes.map((route) => route.snapshot);
|
|
90
|
+
const previous = publishedSnapshots;
|
|
91
|
+
if (!force && previous && current.length === previous.length
|
|
92
|
+
&& current.every((snapshot, index) => snapshot === previous[index]))
|
|
93
|
+
return;
|
|
94
|
+
publishedSnapshots = current;
|
|
95
|
+
// 各可用套餐目录的并集:单套餐失效只撤下自己的条目,其余套餐继续可选。
|
|
96
|
+
const models = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const route of planRoutes) {
|
|
99
|
+
if (!route.snapshot)
|
|
100
|
+
continue;
|
|
101
|
+
for (const entry of createZcodeCatalog(route.snapshot, vendorCatalog).models) {
|
|
102
|
+
const slug = entry.slug.toLowerCase();
|
|
103
|
+
if (seen.has(slug))
|
|
104
|
+
continue;
|
|
105
|
+
seen.add(slug);
|
|
106
|
+
models.push(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
publishServedCatalog({ models });
|
|
110
|
+
};
|
|
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
|
+
}));
|
|
134
|
+
}
|
|
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 });
|
|
142
|
+
}
|
|
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;
|
|
193
|
+
}
|
|
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
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const fetchUpstream = dependencies.fetch ?? ((url, init) => fetch(url, init));
|
|
204
|
+
const activeRequests = new Set();
|
|
205
|
+
const identity = enabled ? dependencies.identity ?? readZcodeIdentity() : undefined;
|
|
206
|
+
const endpointRouting = enabled && dependencies.endpointRouting !== null
|
|
207
|
+
? dependencies.endpointRouting ?? new ZcodeEndpointRouting({
|
|
208
|
+
identity: identity,
|
|
209
|
+
fetch: (url, init) => fetchUpstream(url, init),
|
|
210
|
+
})
|
|
211
|
+
: undefined;
|
|
212
|
+
// coding plan 官方客户端签名:逐请求补 X-Client-* 签名头;fail-open,失败按未签名继续。
|
|
213
|
+
const clientSigning = enabled && dependencies.clientSigning !== null
|
|
214
|
+
? dependencies.clientSigning ?? new ZcodeClientSigning({ fetch: (url, init) => fetchUpstream(url, init) })
|
|
215
|
+
: undefined;
|
|
216
|
+
const contexts = createZcodeContexts();
|
|
217
|
+
const sink = config.requestLogging === true ? {
|
|
218
|
+
dir: requestLogDir(config),
|
|
219
|
+
maxLogs: Math.max(0, Math.trunc(config.maxRequestLogs ?? 0)),
|
|
220
|
+
processLog: dependencies.processLog,
|
|
221
|
+
} : undefined;
|
|
222
|
+
let closed = false;
|
|
223
|
+
let refreshPromise;
|
|
224
|
+
const readCatalog = async (mode) => {
|
|
225
|
+
if (closed || !enabled)
|
|
226
|
+
return { models: [] };
|
|
227
|
+
synchronizeAPIKeyRoutes(manual && mode !== "get");
|
|
228
|
+
const neverPublished = publishedSnapshots === undefined;
|
|
229
|
+
const results = await Promise.all(planRoutes.map(async (route) => {
|
|
230
|
+
if (!route.cache)
|
|
231
|
+
return;
|
|
232
|
+
const previous = route.snapshot;
|
|
233
|
+
try {
|
|
234
|
+
const read = mode === "get" ? route.cache.get : route.cache[mode] ?? route.cache.get;
|
|
235
|
+
return { route, snapshot: await read.call(route.cache) };
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (manual && mode !== "get") {
|
|
239
|
+
// 固定个人/团队槽位可能尚未连接;有效目录的认证失败不能覆盖为部分快照。
|
|
240
|
+
if (previous)
|
|
241
|
+
return { route, snapshot: previous };
|
|
242
|
+
if (!injectedCaches && !route.apiProviderID && !zcodeConfigPresent(home))
|
|
243
|
+
return { route, snapshot: undefined };
|
|
244
|
+
let metadataFailure = error;
|
|
245
|
+
if (mode === "refresh" && route.cache.reload) {
|
|
246
|
+
try {
|
|
247
|
+
await route.cache.reload();
|
|
248
|
+
}
|
|
249
|
+
catch (metadataError) {
|
|
250
|
+
metadataFailure = metadataError;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (metadataFailure instanceof ZcodeConfigError
|
|
254
|
+
&& /没有可用的|未找到可用的官方 API Key provider/.test(metadataFailure.message)) {
|
|
255
|
+
return { route, snapshot: undefined };
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
return { route, snapshot: undefined };
|
|
260
|
+
}
|
|
261
|
+
}));
|
|
262
|
+
if (closed)
|
|
263
|
+
return { models: [] };
|
|
264
|
+
for (const result of results)
|
|
265
|
+
if (result)
|
|
266
|
+
result.route.snapshot = result.snapshot;
|
|
267
|
+
if (neverPublished && !planRoutes.some((route) => route.snapshot))
|
|
268
|
+
dropCachedZcodeModels();
|
|
269
|
+
republishFromPlans(mode !== "get");
|
|
270
|
+
return { models: servedCatalog.models };
|
|
271
|
+
};
|
|
272
|
+
const refreshCatalog = () => {
|
|
273
|
+
if (refreshPromise)
|
|
274
|
+
return refreshPromise;
|
|
275
|
+
const promise = readCatalog("refresh");
|
|
276
|
+
refreshPromise = promise;
|
|
277
|
+
void promise.finally(() => { if (refreshPromise === promise)
|
|
278
|
+
refreshPromise = undefined; }).catch(() => { });
|
|
279
|
+
return promise;
|
|
280
|
+
};
|
|
281
|
+
/** 启动首读仍允许凭据授权,手动策略仅停止后续目录自动发布。 */
|
|
282
|
+
const startup = enabled && vendorCatalog && dependencies.refreshCatalogOnStart !== false
|
|
283
|
+
? refreshCatalog() : Promise.resolve({ models: [] });
|
|
284
|
+
// 构造适配器与等待 modelsReady 之间允许异步间隙;失败由公开目录接口传递。
|
|
285
|
+
void startup.catch(() => { });
|
|
286
|
+
return {
|
|
287
|
+
async catalog() {
|
|
288
|
+
await startup;
|
|
289
|
+
if (closed)
|
|
290
|
+
return { models: [] };
|
|
291
|
+
return manual ? { models: servedCatalog.models } : readCatalog("get");
|
|
292
|
+
},
|
|
293
|
+
refreshCatalog,
|
|
294
|
+
async reloadCatalog() {
|
|
295
|
+
await startup;
|
|
296
|
+
if (refreshPromise)
|
|
297
|
+
await refreshPromise;
|
|
298
|
+
return readCatalog("reload");
|
|
299
|
+
},
|
|
300
|
+
async forward(request, input, mapResult) {
|
|
301
|
+
const start = Date.now();
|
|
302
|
+
const requestTime = localTime();
|
|
303
|
+
const incoming = new URL(request.url);
|
|
304
|
+
const group = logGroupFromPath(incoming.pathname);
|
|
305
|
+
const zcode = isZcodeModel(input.model);
|
|
306
|
+
// 渠道(zai/bigmodel)只在转发时按当前套餐快照判定,用于鉴权与日志分流;模型 ID 不携带渠道。
|
|
307
|
+
let family;
|
|
308
|
+
let key = "";
|
|
309
|
+
let upstreamUrl;
|
|
310
|
+
let upstreamRequestHeaders;
|
|
311
|
+
let upstreamRequestBody;
|
|
312
|
+
let logged = false;
|
|
313
|
+
const redact = (text) => {
|
|
314
|
+
if (!key)
|
|
315
|
+
return text;
|
|
316
|
+
// 错误正文可能以任意 JSON 转义形式回显 key(\/、\uXXXX 及多层反斜杠嵌套):
|
|
317
|
+
// 固定枚举变体漏掉转义形式时,客户端反序列化 JSON 即可还原完整密钥。逐字符
|
|
318
|
+
// 生成四种互斥形式(裸字符、反斜杠+字符、反斜杠+u 十六进制两种大小写),
|
|
319
|
+
// 保证每个字符位置的匹配分解唯一,避免灾难性回溯。
|
|
320
|
+
const pattern = [...key].map((char) => {
|
|
321
|
+
const escaped = char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
322
|
+
const hex = char.codePointAt(0).toString(16).padStart(4, "0");
|
|
323
|
+
return `(?:${escaped}|\\\\{1,4}${escaped}|\\\\{1,4}u${hex}|\\\\{1,4}u${hex.toUpperCase()})`;
|
|
324
|
+
}).join("");
|
|
325
|
+
let result = text.replace(new RegExp(pattern, "gu"), "***");
|
|
326
|
+
let variant = key;
|
|
327
|
+
// 错误正文可能把请求又序列化成 JSON 字符串,连同转义形式一起遮蔽。
|
|
328
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
329
|
+
result = result.replaceAll(variant, "***");
|
|
330
|
+
variant = JSON.stringify(variant).slice(1, -1);
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
};
|
|
334
|
+
const redactValue = (value) => {
|
|
335
|
+
if (typeof value === "string")
|
|
336
|
+
return redact(value);
|
|
337
|
+
if (Array.isArray(value))
|
|
338
|
+
return value.map(redactValue);
|
|
339
|
+
if (isZcodeRecord(value))
|
|
340
|
+
return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, redactValue(item)]));
|
|
341
|
+
return value;
|
|
342
|
+
};
|
|
343
|
+
const log = (status, body, headers = new Headers(), logicalError) => {
|
|
344
|
+
if (logged || !zcode || !family)
|
|
345
|
+
return;
|
|
346
|
+
logged = true;
|
|
347
|
+
const durationMs = Date.now() - start;
|
|
348
|
+
const at = incoming.pathname + incoming.search;
|
|
349
|
+
logExchange(sink, group, {
|
|
350
|
+
requestTime, method: request.method, url: at,
|
|
351
|
+
reqHeaders: request.headers, reqBody: redactValue(input),
|
|
352
|
+
status, resHeaders: headers, resBody: redact(body), upstreamUrl, upstreamRequestHeaders, upstreamRequestBody, durationMs,
|
|
353
|
+
}, "zcode");
|
|
354
|
+
// 与网关主链一样,进程日志里每条请求恰好一行:逻辑错误也算错误,走错误摘要。
|
|
355
|
+
if (status >= 400 || logicalError) {
|
|
356
|
+
logGatewayError(sink?.processLog, {
|
|
357
|
+
requestTime, method: request.method, url: at,
|
|
358
|
+
status: logicalError && status < 400 ? 502 : status,
|
|
359
|
+
message: redact(logicalError ?? body), upstreamUrl, durationMs,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
logRequestSummary(sink?.processLog, {
|
|
364
|
+
requestTime, method: request.method, url: at, status, upstreamUrl, durationMs,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const fail = (status, message, type) => {
|
|
369
|
+
const response = zcodeError(status, redact(message), type);
|
|
370
|
+
log(status, JSON.stringify({ error: { type, message: redact(message) } }), response.headers);
|
|
371
|
+
return response;
|
|
372
|
+
};
|
|
373
|
+
if (closed || planRoutes.every((route) => !route.cache))
|
|
374
|
+
return fail(503, "ZCode 未启用或网关已关闭", "configuration_error");
|
|
375
|
+
const abort = new AbortController();
|
|
376
|
+
const onAbort = () => abort.abort(request.signal.reason);
|
|
377
|
+
let cleaned = false;
|
|
378
|
+
const cleanup = () => {
|
|
379
|
+
if (cleaned)
|
|
380
|
+
return;
|
|
381
|
+
cleaned = true;
|
|
382
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
383
|
+
activeRequests.delete(abort);
|
|
384
|
+
};
|
|
385
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
386
|
+
activeRequests.add(abort);
|
|
387
|
+
try {
|
|
388
|
+
if (request.signal.aborted)
|
|
389
|
+
onAbort();
|
|
390
|
+
abort.signal.throwIfAborted();
|
|
391
|
+
// 会话级套餐选择:模型 slug 的套餐段决定走哪条路由;key 的解析与上游调用链路不变。
|
|
392
|
+
const requested = typeof input.model === "string" ? input.model : "";
|
|
393
|
+
const requestedPlan = zcodeModelPlan(requested);
|
|
394
|
+
if (!manual && requestedPlan === "api-key")
|
|
395
|
+
synchronizeAPIKeyRoutes();
|
|
396
|
+
const requestedProviderID = requestedPlan === "api-key" ? zcodeAPIProviderIDFromModel(requested) : undefined;
|
|
397
|
+
const route = planRoutes.find((entry) => {
|
|
398
|
+
if (entry.plan !== requestedPlan)
|
|
399
|
+
return false;
|
|
400
|
+
if (requestedPlan !== "api-key")
|
|
401
|
+
return true;
|
|
402
|
+
if (!requestedProviderID)
|
|
403
|
+
return !entry.apiProviderID || planRoutes.find((candidate) => candidate.plan === "api-key" && candidate.apiProviderID) === entry;
|
|
404
|
+
if (entry.apiProviderID === requestedProviderID)
|
|
405
|
+
return true;
|
|
406
|
+
// 注入模式只有一个 legacy api-key 缓存时先命中,再由 zcodeUpstreamModel
|
|
407
|
+
// 按快照 providerID 精确拒绝不匹配的请求。
|
|
408
|
+
return !entry.apiProviderID;
|
|
409
|
+
});
|
|
410
|
+
if (!route?.cache) {
|
|
411
|
+
cleanup();
|
|
412
|
+
return fail(404, "此模型不属于 ZCode 当前套餐或厂商目录");
|
|
413
|
+
}
|
|
414
|
+
const currentSnapshot = await route.cache.get();
|
|
415
|
+
if (manual && !route.snapshot)
|
|
416
|
+
await startup;
|
|
417
|
+
// 凭据与端点继续按原机制维护;手动目录模式只固定已发布的授权模型集合。
|
|
418
|
+
const snapshot = manual
|
|
419
|
+
? { ...currentSnapshot, modelIds: route.snapshot?.modelIds ?? [] }
|
|
420
|
+
: currentSnapshot;
|
|
421
|
+
key = snapshot.apiKey;
|
|
422
|
+
family = snapshot.family;
|
|
423
|
+
abort.signal.throwIfAborted();
|
|
424
|
+
const model = zcodeUpstreamModel(requested, snapshot);
|
|
425
|
+
if (!model) {
|
|
426
|
+
cleanup();
|
|
427
|
+
return fail(404, "此模型不属于 ZCode 当前套餐或厂商目录");
|
|
428
|
+
}
|
|
429
|
+
const translated = translateZcodeRequest(input, model);
|
|
430
|
+
const baseUpstreamUrl = `${snapshot.baseURL}${snapshot.baseURL.endsWith("/v1") ? "/messages" : "/v1/messages"}`;
|
|
431
|
+
const routedUrl = endpointRouting
|
|
432
|
+
// 跟随 z.ai 服务端下发的端点映射(如 zcode.z.ai ultra 中转);resolve 内部 fail-open,失败保持原 URL。
|
|
433
|
+
? (await endpointRouting.resolve(baseUpstreamUrl, key)).url
|
|
434
|
+
: baseUpstreamUrl;
|
|
435
|
+
upstreamUrl = routedUrl;
|
|
436
|
+
// 从当前快照重建协议头与会话归因,不采信入站客户端的 ZCode 身份或授权头。
|
|
437
|
+
const context = contexts.resolve(request, snapshot);
|
|
438
|
+
const headers = buildZcodeModelHeaders(identity, context, zcodePlan(snapshot), key);
|
|
439
|
+
const beta = request.headers.get("anthropic-beta")?.trim();
|
|
440
|
+
if (beta && beta.length <= 1024 && /^[\x20-\x7e]+$/.test(beta))
|
|
441
|
+
headers.set("anthropic-beta", beta);
|
|
442
|
+
// 客户端签名作用域绑定 base origin 与当前会话;端点重映射发生在签名之后(先签后路由)。
|
|
443
|
+
const signingScope = {
|
|
444
|
+
apiKey: key,
|
|
445
|
+
baseUrl: snapshot.baseURL,
|
|
446
|
+
clientVersion: identity.appVersion,
|
|
447
|
+
sessionId: context.sessionId,
|
|
448
|
+
};
|
|
449
|
+
// 版本未知(本机未装 ZCode)时无法声明可信客户端版本,跳过签名。
|
|
450
|
+
const signable = clientSigning !== undefined && signingScope.clientVersion !== "unknown";
|
|
451
|
+
const body = decorateZcodeBody(translated.body, context);
|
|
452
|
+
// 图片识别适配:请求含图片时把被吸收的 analyze_image 调用落到网关执行并续跑上游。
|
|
453
|
+
let gatewayTools;
|
|
454
|
+
let continuationLegs = 0;
|
|
455
|
+
if (translated.vision) {
|
|
456
|
+
gatewayTools = {
|
|
457
|
+
maxContinuations: 3,
|
|
458
|
+
execute: async (call) => {
|
|
459
|
+
abort.signal.throwIfAborted();
|
|
460
|
+
const prompt = typeof call.input.prompt === "string" && call.input.prompt
|
|
461
|
+
? call.input.prompt
|
|
462
|
+
: "请详细识别并提取这张图片中的所有内容。";
|
|
463
|
+
const image = matchZcodeAnalyzeImage(call.input.imageSource, translated.images);
|
|
464
|
+
if (!image)
|
|
465
|
+
return "analyze_image 无法定位请求中的图片(imageSource 缺少有效引用)。请向用户说明或改用其他方式。";
|
|
466
|
+
try {
|
|
467
|
+
return await executeZcodeAnalyzeImage({
|
|
468
|
+
url: routedUrl,
|
|
469
|
+
model,
|
|
470
|
+
image,
|
|
471
|
+
prompt,
|
|
472
|
+
headers: async () => {
|
|
473
|
+
const requestHeaders = buildZcodeModelHeaders(identity, context, zcodePlan(snapshot), key);
|
|
474
|
+
if (signable)
|
|
475
|
+
await clientSigning.decorate(requestHeaders, signingScope);
|
|
476
|
+
return requestHeaders;
|
|
477
|
+
},
|
|
478
|
+
fetchImpl: fetchUpstream,
|
|
479
|
+
signal: abort.signal,
|
|
480
|
+
// 错误正文脱敏必须在截断之前发生(见 ZcodeExecutorOptions.redact)。
|
|
481
|
+
redact,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
if (abort.signal.aborted)
|
|
486
|
+
throw error;
|
|
487
|
+
// 降级旁白会把异常消息直接发给客户端(响应仍为 completed,失败事件的
|
|
488
|
+
// sanitizeError 覆盖不到):先按同一规则抹掉上游错误正文可能回显的 key。
|
|
489
|
+
throw new Error(redact(error instanceof Error ? error.message : String(error)));
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
nextUpstream: async (calls) => {
|
|
493
|
+
abort.signal.throwIfAborted();
|
|
494
|
+
// 把被吸收的调用与执行结果回放为上游 tool_use/tool_result 消息对。
|
|
495
|
+
const toolUses = calls.map(({ call }) => ({ type: "tool_use", id: call.id, name: call.name, input: call.input }));
|
|
496
|
+
const toolResults = calls.map(({ call, result }) => ({
|
|
497
|
+
type: "tool_result", tool_use_id: call.id, content: [{ type: "text", text: result }],
|
|
498
|
+
}));
|
|
499
|
+
body.messages.push({ role: "assistant", content: toolUses }, { role: "user", content: toolResults });
|
|
500
|
+
continuationLegs++;
|
|
501
|
+
// 续跑腿逐次重新签名(ts/nonce/PoW 不可复用);签名失败按未签名继续。
|
|
502
|
+
const legHeaders = new Headers(headers);
|
|
503
|
+
if (signable)
|
|
504
|
+
await clientSigning.decorate(legHeaders, signingScope);
|
|
505
|
+
const response = await fetchUpstream(routedUrl, {
|
|
506
|
+
method: "POST", headers: legHeaders, body: JSON.stringify(body), redirect: "manual", signal: abort.signal,
|
|
507
|
+
});
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
// 与执行信封同规则:先对完整正文脱敏、后截断,避免跨边界 key 前缀泄漏。
|
|
510
|
+
const text = redact(await response.text().catch(() => ""));
|
|
511
|
+
throw new Error(`续跑上游返回 HTTP ${response.status}${text ? `:${text.slice(0, 200)}` : ""}`);
|
|
512
|
+
}
|
|
513
|
+
return response;
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
// 记录转换后真正发往上游的正文;未开启请求日志时不付出遍历脱敏的开销。
|
|
518
|
+
upstreamRequestBody = sink ? redactValue(body) : undefined;
|
|
519
|
+
const serializedBody = JSON.stringify(body);
|
|
520
|
+
const postUpstream = async () => {
|
|
521
|
+
// 每次发送都克隆基础头并现场签名;签名值(ts/nonce/PoW)不可跨请求复用。
|
|
522
|
+
const requestHeaders = new Headers(headers);
|
|
523
|
+
const signed = signable ? await clientSigning.decorate(requestHeaders, signingScope) : false;
|
|
524
|
+
upstreamRequestHeaders = requestHeaders;
|
|
525
|
+
return {
|
|
526
|
+
response: await fetchUpstream(routedUrl, {
|
|
527
|
+
method: "POST", headers: requestHeaders, body: serializedBody, redirect: "manual", signal: abort.signal,
|
|
528
|
+
}),
|
|
529
|
+
signed,
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
let sent = await postUpstream();
|
|
533
|
+
let upstream = sent.response;
|
|
534
|
+
let upstreamErrorText;
|
|
535
|
+
if (!upstream.ok) {
|
|
536
|
+
let rawText = await upstream.text();
|
|
537
|
+
// 服务端密钥轮换会拒绝缓存的签名身份:作废重握手、重签一次;仍失败按原错误返回。
|
|
538
|
+
if (sent.signed && upstream.status === 401 && clientSigningVerifyRejection(rawText)) {
|
|
539
|
+
clientSigning.invalidate(key, snapshot.baseURL);
|
|
540
|
+
sent = await postUpstream();
|
|
541
|
+
upstream = sent.response;
|
|
542
|
+
// 只有重试仍失败才读错误正文;成功响应的流必须留给后续 SSE 解析。
|
|
543
|
+
if (!upstream.ok)
|
|
544
|
+
rawText = await upstream.text();
|
|
545
|
+
}
|
|
546
|
+
if (!upstream.ok)
|
|
547
|
+
upstreamErrorText = rawText;
|
|
548
|
+
}
|
|
549
|
+
if (upstreamErrorText !== undefined) {
|
|
550
|
+
const text = redact(upstreamErrorText);
|
|
551
|
+
cleanup();
|
|
552
|
+
let message = text || `ZCode 上游返回 HTTP ${upstream.status}`;
|
|
553
|
+
try {
|
|
554
|
+
const error = redactValue(JSON.parse(text));
|
|
555
|
+
if (isZcodeRecord(error) && isZcodeRecord(error.error) && typeof error.error.message === "string")
|
|
556
|
+
message = error.error.message;
|
|
557
|
+
else
|
|
558
|
+
message = JSON.stringify(error);
|
|
559
|
+
}
|
|
560
|
+
catch { /* 非 JSON 上游错误保留脱敏正文。 */ }
|
|
561
|
+
return fail(upstream.status, message, "upstream_error");
|
|
562
|
+
}
|
|
563
|
+
let chunks = "";
|
|
564
|
+
const stream = !mapResult && input.stream === true;
|
|
565
|
+
const response = await createZcodeResponse(upstream, {
|
|
566
|
+
model: String(input.model), tools: translated.tools, stream, signal: abort.signal,
|
|
567
|
+
gatewayTools,
|
|
568
|
+
sanitizeError: (error) => ({
|
|
569
|
+
code: typeof error.code === "string" ? redact(error.code) : "upstream_error",
|
|
570
|
+
message: typeof error.message === "string" ? redact(error.message) : "ZCode 上游响应失败",
|
|
571
|
+
}),
|
|
572
|
+
abort: () => { abort.abort(); cleanup(); },
|
|
573
|
+
onChunk: (chunk) => { if (sink)
|
|
574
|
+
chunks += chunk; },
|
|
575
|
+
onComplete: (payload) => {
|
|
576
|
+
cleanup();
|
|
577
|
+
// 剥离的内置工具与图片识别续跑腿数写进交换日志,避免“静默降级”无从排查。
|
|
578
|
+
const stripped = translated.dropped.length ? `\n--- stripped built-in tools: ${translated.dropped.join(", ")} ---` : "";
|
|
579
|
+
const visionLegs = continuationLegs > 0 ? `\n--- analyze_image continuation legs: ${continuationLegs} ---` : "";
|
|
580
|
+
if (!mapResult)
|
|
581
|
+
log(stream ? 200 : payload.status === "failed" ? 502 : 200, stream ? `${chunks}${stripped}${visionLegs}\n--- response.${payload.status} ---\n${JSON.stringify(redactValue(payload))}` : `${JSON.stringify(redactValue(payload))}${stripped}${visionLegs}`, new Headers({ "content-type": stream ? "text/event-stream" : "application/json" }), payload.status === "failed" ? JSON.stringify(payload.error ?? "响应失败") : undefined);
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
if (!mapResult)
|
|
585
|
+
return response;
|
|
586
|
+
const payload = await response.json();
|
|
587
|
+
if (!response.ok || !isZcodeRecord(payload)) {
|
|
588
|
+
cleanup();
|
|
589
|
+
return fail(502, "ZCode 上游未完成上下文压缩", "invalid_response_error");
|
|
590
|
+
}
|
|
591
|
+
const mapped = mapResult(payload);
|
|
592
|
+
log(mapped.status, await mapped.clone().text(), mapped.headers);
|
|
593
|
+
cleanup();
|
|
594
|
+
return mapped;
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
abort.abort();
|
|
598
|
+
cleanup();
|
|
599
|
+
if (error instanceof ZcodeRequestError)
|
|
600
|
+
return fail(400, error.message);
|
|
601
|
+
if (error instanceof ZcodeConfigError)
|
|
602
|
+
return fail(503, error.message, "configuration_error");
|
|
603
|
+
if (request.signal.aborted)
|
|
604
|
+
return fail(499, "ZCode 请求已取消", "request_cancelled");
|
|
605
|
+
return fail(502, "ZCode 上游请求失败", "upstream_error");
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
close() {
|
|
609
|
+
if (closed)
|
|
610
|
+
return;
|
|
611
|
+
closed = true;
|
|
612
|
+
for (const route of planRoutes)
|
|
613
|
+
route.cache?.close();
|
|
614
|
+
contexts.close();
|
|
615
|
+
for (const request of activeRequests)
|
|
616
|
+
request.abort();
|
|
617
|
+
activeRequests.clear();
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
export function createZcodeAdapter(config, dependencies = {}) {
|
|
622
|
+
return bindHostMethods(buildZcodeAdapter(config, dependencies));
|
|
623
|
+
}
|
|
624
|
+
export const ZCODE_MODEL_PATTERNS = Object.freeze(["zcode/*", "zcode-*/*"]);
|
|
625
|
+
/** 插件只负责本代理路由,协议转换沿用原适配器实现。 */
|
|
626
|
+
export function createZcodePlugin(host, dependencies = {}) {
|
|
627
|
+
return withHostServices(host, () => {
|
|
628
|
+
const adapter = createZcodeAdapter(host.getConfig(), { fetch: host.fetch, ...dependencies });
|
|
629
|
+
return bindHostMethods({
|
|
630
|
+
id: "zcode",
|
|
631
|
+
modelPatterns: ZCODE_MODEL_PATTERNS,
|
|
632
|
+
websocket: false,
|
|
633
|
+
async handleHttp(context) {
|
|
634
|
+
const input = await context.body.json();
|
|
635
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
636
|
+
return Response.json({ error: { type: "invalid_request_error", message: "请求正文必须是 JSON 对象" } }, { status: 400 });
|
|
637
|
+
if (context.from !== "responses")
|
|
638
|
+
return Response.json({ error: { type: "invalid_request_error", message: "Zcode 模型仅支持 Responses API" } }, { status: 400 });
|
|
639
|
+
return adapter.forward(context.request, input);
|
|
640
|
+
},
|
|
641
|
+
getCatalog: () => adapter.catalog(),
|
|
642
|
+
refreshCatalog: () => adapter.refreshCatalog(),
|
|
643
|
+
reloadCatalog: () => adapter.reloadCatalog(),
|
|
644
|
+
ready: async () => { },
|
|
645
|
+
close: () => adapter.close(),
|
|
646
|
+
}, host);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ZcodeFamily } from "./config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* 个人 Coding Plan 的账号身份与 Key 解析结果。
|
|
4
|
+
* - absent:没有任何 user_info,允许旧安装回退 config 镜像;
|
|
5
|
+
* - available:账号身份和对应 Key 都可用;
|
|
6
|
+
* - unavailable:身份存在但损坏,或当前账号的 Key 缺失/不可解密,禁止回退。
|
|
7
|
+
*/
|
|
8
|
+
export type ZcodeIndividualCredential = {
|
|
9
|
+
state: "absent";
|
|
10
|
+
} | {
|
|
11
|
+
state: "available";
|
|
12
|
+
identity: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
} | {
|
|
15
|
+
state: "unavailable";
|
|
16
|
+
identity?: string;
|
|
17
|
+
};
|
|
18
|
+
/** 读取当前账号身份对应的个人 Coding Plan Key。 */
|
|
19
|
+
export declare function readZcodeIndividualCredential(credentials: Record<string, unknown>, family: ZcodeFamily, env?: NodeJS.ProcessEnv): ZcodeIndividualCredential;
|