@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/response.js
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { parseAnthropicStream } from "@agent-proxy/llm-bridge";
|
|
2
|
+
import { encodeZcodeThinking, isZcodeRecord } from "./wire.js";
|
|
3
|
+
import { gatewayToolCallNarration, gatewayToolResultNarration } from "./vision.js";
|
|
4
|
+
/** z.ai 的服务端搜索按 web_search_prime/search_query 形状返回 query;兼容 Anthropic 标准的 query。 */
|
|
5
|
+
function serverToolQueryOf(input) {
|
|
6
|
+
for (const key of ["search_query", "query"]) {
|
|
7
|
+
if (typeof input[key] === "string" && input[key])
|
|
8
|
+
return input[key];
|
|
9
|
+
}
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
function serverToolResultEntries(content) {
|
|
13
|
+
const entries = [];
|
|
14
|
+
if (typeof content === "string" && content)
|
|
15
|
+
return [{ type: "web_search_result", text: content }];
|
|
16
|
+
if (!Array.isArray(content))
|
|
17
|
+
return entries;
|
|
18
|
+
for (const raw of content) {
|
|
19
|
+
if (!isZcodeRecord(raw))
|
|
20
|
+
continue;
|
|
21
|
+
if (typeof raw.text === "string" && raw.text)
|
|
22
|
+
entries.push({ type: "web_search_result", text: raw.text });
|
|
23
|
+
else if (typeof raw.url === "string" && raw.url)
|
|
24
|
+
entries.push(raw);
|
|
25
|
+
}
|
|
26
|
+
return entries;
|
|
27
|
+
}
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
function safely(callback) {
|
|
30
|
+
try {
|
|
31
|
+
callback?.();
|
|
32
|
+
}
|
|
33
|
+
catch { /* 日志回调不能中断响应。 */ }
|
|
34
|
+
}
|
|
35
|
+
function requiredString(value, label) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
throw new Error(`ZCode ${label} 必须是字符串`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function record(value, label) {
|
|
41
|
+
if (!isZcodeRecord(value))
|
|
42
|
+
throw new Error(`ZCode ${label} 必须是对象`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function blockIndex(value) {
|
|
46
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0)
|
|
47
|
+
throw new Error("ZCode content block index 无效");
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
/** 仅消费一条上游流;严格解析 SSE,保留库解析器忽略的字段和错误。 */
|
|
51
|
+
async function* readFrames(reader) {
|
|
52
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
53
|
+
let buffer = "";
|
|
54
|
+
let event = "";
|
|
55
|
+
let data = [];
|
|
56
|
+
function line(value) {
|
|
57
|
+
if (value === "") {
|
|
58
|
+
if (!data.length) {
|
|
59
|
+
event = "";
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(data.join("\n"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error("ZCode SSE 包含无效 JSON");
|
|
68
|
+
}
|
|
69
|
+
const frame = record(parsed, "SSE data");
|
|
70
|
+
if (event && frame.type !== undefined && event !== frame.type)
|
|
71
|
+
throw new Error("ZCode SSE event 与 type 不一致");
|
|
72
|
+
const type = event || requiredString(frame.type, "SSE type");
|
|
73
|
+
data = [];
|
|
74
|
+
event = "";
|
|
75
|
+
return { ...frame, type };
|
|
76
|
+
}
|
|
77
|
+
if (value.startsWith(":"))
|
|
78
|
+
return;
|
|
79
|
+
const colon = value.indexOf(":");
|
|
80
|
+
const field = colon < 0 ? value : value.slice(0, colon);
|
|
81
|
+
let content = colon < 0 ? "" : value.slice(colon + 1);
|
|
82
|
+
if (content.startsWith(" "))
|
|
83
|
+
content = content.slice(1);
|
|
84
|
+
if (field === "event")
|
|
85
|
+
event = content;
|
|
86
|
+
if (field === "data")
|
|
87
|
+
data.push(content);
|
|
88
|
+
}
|
|
89
|
+
while (true) {
|
|
90
|
+
const { value, done } = await reader.read();
|
|
91
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
92
|
+
while (true) {
|
|
93
|
+
const position = buffer.search(/[\r\n]/);
|
|
94
|
+
if (position < 0)
|
|
95
|
+
break;
|
|
96
|
+
if (buffer[position] === "\r" && position + 1 === buffer.length && !done)
|
|
97
|
+
break;
|
|
98
|
+
const width = buffer[position] === "\r" && buffer[position + 1] === "\n" ? 2 : 1;
|
|
99
|
+
const next = line(buffer.slice(0, position));
|
|
100
|
+
buffer = buffer.slice(position + width);
|
|
101
|
+
if (next)
|
|
102
|
+
yield next;
|
|
103
|
+
}
|
|
104
|
+
if (done) {
|
|
105
|
+
if (buffer.length || data.length)
|
|
106
|
+
throw new Error("ZCode SSE 在事件结束前意外 EOF");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** 库只保存一个工具上下文;按帧补入对应块,使交错的块仍通过库正确归一化。 */
|
|
112
|
+
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 });
|
|
116
|
+
}
|
|
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;
|
|
130
|
+
}
|
|
131
|
+
result.push(value);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
export async function createZcodeResponse(upstream, options) {
|
|
136
|
+
let reader = upstream.body?.getReader();
|
|
137
|
+
const blocks = new Map();
|
|
138
|
+
const serverTools = [];
|
|
139
|
+
const serverToolByIndex = new Map();
|
|
140
|
+
const serverToolByUpstreamId = new Map();
|
|
141
|
+
const serverResults = new Map();
|
|
142
|
+
const ignoredBlocks = new Set();
|
|
143
|
+
const output = [];
|
|
144
|
+
const usage = {};
|
|
145
|
+
const id = `resp_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
146
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
147
|
+
let sequence = 0;
|
|
148
|
+
let started = false;
|
|
149
|
+
let stopped = false;
|
|
150
|
+
let stopReason;
|
|
151
|
+
let finalResponse;
|
|
152
|
+
let canceled = false;
|
|
153
|
+
let upstreamCanceled = false;
|
|
154
|
+
let cleaned = false;
|
|
155
|
+
/** 本腿被吸收的网关工具调用(message_stop 时整批执行并续跑)。 */
|
|
156
|
+
let pendingGateway = [];
|
|
157
|
+
/** 已流出 Input 旁白卡片的吸收调用;未续跑时需补 Output 卡片,避免 "Executing on server..." 悬挂。 */
|
|
158
|
+
let narratedGateway = [];
|
|
159
|
+
let continuations = 0;
|
|
160
|
+
const snapshot = (status) => ({
|
|
161
|
+
id, object: "response", created_at: createdAt, model: options.model, status,
|
|
162
|
+
output: structuredClone(output), error: null, incomplete_details: null, usage: null,
|
|
163
|
+
});
|
|
164
|
+
const event = (type, fields = {}) => ({ type, sequence_number: sequence++, ...fields });
|
|
165
|
+
function finalUsage() {
|
|
166
|
+
if (!Object.keys(usage).length)
|
|
167
|
+
return null;
|
|
168
|
+
const count = (key) => typeof usage[key] === "number" ? usage[key] : 0;
|
|
169
|
+
const input = count("input_tokens") + count("cache_read_input_tokens") + count("cache_creation_input_tokens");
|
|
170
|
+
const result = { input_tokens: input, output_tokens: count("output_tokens"), total_tokens: input + count("output_tokens") };
|
|
171
|
+
const details = {};
|
|
172
|
+
if ("cache_read_input_tokens" in usage)
|
|
173
|
+
details.cached_tokens = count("cache_read_input_tokens");
|
|
174
|
+
if ("cache_creation_input_tokens" in usage)
|
|
175
|
+
details.cache_creation_tokens = count("cache_creation_input_tokens");
|
|
176
|
+
if (Object.keys(details).length)
|
|
177
|
+
result.input_tokens_details = details;
|
|
178
|
+
if (typeof usage.reasoning_tokens === "number")
|
|
179
|
+
result.output_tokens_details = { reasoning_tokens: usage.reasoning_tokens };
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
function finish(status, error) {
|
|
183
|
+
if (finalResponse)
|
|
184
|
+
return;
|
|
185
|
+
for (const block of blocks.values()) {
|
|
186
|
+
if (!block.stopped) {
|
|
187
|
+
block.item.status = "incomplete";
|
|
188
|
+
if (block.source.type === "thinking") {
|
|
189
|
+
block.item.encrypted_content = encodeZcodeThinking(options.model, [{ type: "thinking", thinking: block.text, signature: block.signature }]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// 异常终止时未等到结果块的服务端搜索条目不补事件,仅随快照标记状态(与普通块截断语义一致)。
|
|
194
|
+
for (const entry of serverTools) {
|
|
195
|
+
if (!entry.resultArrived)
|
|
196
|
+
entry.item.status = status === "completed" ? "completed" : "incomplete";
|
|
197
|
+
}
|
|
198
|
+
finalResponse = { ...snapshot(status), usage: finalUsage() };
|
|
199
|
+
if (status === "incomplete")
|
|
200
|
+
finalResponse.incomplete_details = { reason: "max_output_tokens" };
|
|
201
|
+
if (error) {
|
|
202
|
+
try {
|
|
203
|
+
finalResponse.error = options.sanitizeError ? options.sanitizeError(error) : error;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
finalResponse.error = { code: "upstream_error", message: "ZCode 上游响应失败" };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
cleanup();
|
|
210
|
+
safely(() => options.onComplete?.(structuredClone(finalResponse)));
|
|
211
|
+
return event(`response.${status}`, { response: finalResponse });
|
|
212
|
+
}
|
|
213
|
+
function cancelUpstream() {
|
|
214
|
+
if (upstreamCanceled)
|
|
215
|
+
return;
|
|
216
|
+
upstreamCanceled = true;
|
|
217
|
+
safely(options.abort);
|
|
218
|
+
void reader?.cancel().catch(() => undefined).finally(() => {
|
|
219
|
+
try {
|
|
220
|
+
reader?.releaseLock();
|
|
221
|
+
}
|
|
222
|
+
catch { /* 取消期间可能仍有待收尾读取。 */ }
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** 切换到续跑的下一腿上游流;事件序号与输出条目跨腿连续。 */
|
|
226
|
+
function startLeg(response) {
|
|
227
|
+
const next = response.body?.getReader();
|
|
228
|
+
if (!next)
|
|
229
|
+
throw new Error("ZCode 续跑上游响应缺少 body");
|
|
230
|
+
reader = next;
|
|
231
|
+
started = false;
|
|
232
|
+
stopped = false;
|
|
233
|
+
stopReason = undefined;
|
|
234
|
+
blocks.clear();
|
|
235
|
+
serverTools.length = 0;
|
|
236
|
+
serverToolByIndex.clear();
|
|
237
|
+
serverToolByUpstreamId.clear();
|
|
238
|
+
serverResults.clear();
|
|
239
|
+
ignoredBlocks.clear();
|
|
240
|
+
upstreamCanceled = false;
|
|
241
|
+
}
|
|
242
|
+
function cleanup() {
|
|
243
|
+
if (cleaned)
|
|
244
|
+
return;
|
|
245
|
+
cleaned = true;
|
|
246
|
+
options.signal?.removeEventListener("abort", handleAbort);
|
|
247
|
+
cancelUpstream();
|
|
248
|
+
}
|
|
249
|
+
function handleAbort() {
|
|
250
|
+
if (finalResponse)
|
|
251
|
+
return;
|
|
252
|
+
canceled = true;
|
|
253
|
+
finish("failed", { code: "response_cancelled", message: "ZCode 响应已取消" });
|
|
254
|
+
}
|
|
255
|
+
options.signal?.addEventListener("abort", handleAbort, { once: true });
|
|
256
|
+
if (options.signal?.aborted)
|
|
257
|
+
handleAbort();
|
|
258
|
+
function mergeUsage(value) {
|
|
259
|
+
if (!isZcodeRecord(value))
|
|
260
|
+
return;
|
|
261
|
+
for (const [key, item] of Object.entries(value)) {
|
|
262
|
+
if (typeof item === "number" && Number.isFinite(item) && item >= 0)
|
|
263
|
+
usage[key] = item;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const fields = (block) => ({ item_id: block.item.id, output_index: block.outputIndex });
|
|
267
|
+
function completeServerTool(entry, entries, status, result) {
|
|
268
|
+
if (entry.resultArrived)
|
|
269
|
+
return;
|
|
270
|
+
entry.resultArrived = true;
|
|
271
|
+
if (entries.length)
|
|
272
|
+
entry.item.results = entries;
|
|
273
|
+
entry.item.status = status;
|
|
274
|
+
result.push(event("response.output_item.done", { output_index: entry.outputIndex, item: structuredClone(entry.item) }));
|
|
275
|
+
}
|
|
276
|
+
function appendText(block, value, result) {
|
|
277
|
+
if (!value)
|
|
278
|
+
return;
|
|
279
|
+
block.text += value;
|
|
280
|
+
if (block.source.type === "text") {
|
|
281
|
+
block.item.content[0].text = block.text;
|
|
282
|
+
result.push(event("response.output_text.delta", { ...fields(block), content_index: 0, delta: value, logprobs: [] }));
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
block.item.summary[0].text = block.text;
|
|
286
|
+
result.push(event("response.reasoning_summary_text.delta", { ...fields(block), summary_index: 0, delta: value }));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function closeBlock(block, result) {
|
|
290
|
+
if (block.stopped)
|
|
291
|
+
throw new Error("ZCode content block 重复结束");
|
|
292
|
+
const base = fields(block);
|
|
293
|
+
if (block.source.type === "text") {
|
|
294
|
+
const part = block.item.content[0];
|
|
295
|
+
result.push(event("response.output_text.done", { ...base, content_index: 0, text: block.text, logprobs: [] }));
|
|
296
|
+
result.push(event("response.content_part.done", { ...base, content_index: 0, part: structuredClone(part) }));
|
|
297
|
+
}
|
|
298
|
+
else if (block.source.type === "thinking" || block.source.type === "redacted_thinking") {
|
|
299
|
+
const preserved = block.source.type === "thinking"
|
|
300
|
+
? { type: "thinking", thinking: block.text, signature: block.signature }
|
|
301
|
+
: { type: "redacted_thinking", data: requiredString(block.source.data, "redacted thinking data") };
|
|
302
|
+
block.item.encrypted_content = encodeZcodeThinking(options.model, [preserved]);
|
|
303
|
+
if (block.source.type === "thinking") {
|
|
304
|
+
result.push(event("response.reasoning_summary_text.done", { ...base, summary_index: 0, text: block.text }));
|
|
305
|
+
result.push(event("response.reasoning_summary_part.done", { ...base, summary_index: 0, part: { type: "summary_text", text: block.text } }));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const argumentsText = block.hasArgumentDelta ? block.arguments : JSON.stringify(block.source.input ?? {});
|
|
310
|
+
let parsed;
|
|
311
|
+
try {
|
|
312
|
+
parsed = JSON.parse(argumentsText);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
throw new Error("ZCode tool arguments 包含无效或未结束的 JSON");
|
|
316
|
+
}
|
|
317
|
+
if (!isZcodeRecord(parsed))
|
|
318
|
+
throw new Error("ZCode tool arguments 必须是 JSON 对象");
|
|
319
|
+
if (block.item.type === "custom_tool_call") {
|
|
320
|
+
const input = requiredString(parsed.input, "custom tool input");
|
|
321
|
+
block.item.input = input;
|
|
322
|
+
if (input)
|
|
323
|
+
result.push(event("response.custom_tool_call_input.delta", { ...base, delta: input }));
|
|
324
|
+
result.push(event("response.custom_tool_call_input.done", { ...base, input }));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
if (!block.hasArgumentDelta && argumentsText)
|
|
328
|
+
result.push(event("response.function_call_arguments.delta", { ...base, delta: argumentsText }));
|
|
329
|
+
block.item.arguments = argumentsText;
|
|
330
|
+
result.push(event("response.function_call_arguments.done", { ...base, arguments: argumentsText }));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
block.stopped = true;
|
|
334
|
+
block.item.status = "completed";
|
|
335
|
+
result.push(event("response.output_item.done", { output_index: block.outputIndex, item: structuredClone(block.item) }));
|
|
336
|
+
}
|
|
337
|
+
/** 一次性合成完整生命周期的助手文本条目:网关代执行工具的旁白卡片(事件序与真实文本块一致)。 */
|
|
338
|
+
function synthesizeMessage(value, result) {
|
|
339
|
+
const item = { id: `item_${crypto.randomUUID().replaceAll("-", "")}`, type: "message", role: "assistant", status: "in_progress", content: [] };
|
|
340
|
+
const outputIndex = output.length;
|
|
341
|
+
output.push(item);
|
|
342
|
+
result.push(event("response.output_item.added", { output_index: outputIndex, item: structuredClone(item) }));
|
|
343
|
+
const empty = { type: "output_text", text: "", annotations: [], logprobs: [] };
|
|
344
|
+
result.push(event("response.content_part.added", { output_index: outputIndex, item_id: item.id, content_index: 0, part: empty }));
|
|
345
|
+
result.push(event("response.output_text.delta", { output_index: outputIndex, item_id: item.id, content_index: 0, delta: value, logprobs: [] }));
|
|
346
|
+
result.push(event("response.output_text.done", { output_index: outputIndex, item_id: item.id, content_index: 0, text: value, logprobs: [] }));
|
|
347
|
+
result.push(event("response.content_part.done", { output_index: outputIndex, item_id: item.id, content_index: 0, part: { ...empty, text: value } }));
|
|
348
|
+
item.status = "completed";
|
|
349
|
+
item.content.push({ ...empty, text: value });
|
|
350
|
+
result.push(event("response.output_item.done", { output_index: outputIndex, item: structuredClone(item) }));
|
|
351
|
+
}
|
|
352
|
+
async function process(frame) {
|
|
353
|
+
const result = [];
|
|
354
|
+
const index = frame.type === "content_block_start" || frame.type === "content_block_delta" || frame.type === "content_block_stop" ? blockIndex(frame.index) : undefined;
|
|
355
|
+
const block = index === undefined ? undefined : blocks.get(index);
|
|
356
|
+
const normalized = await normalize(frame, block);
|
|
357
|
+
if (frame.type === "ping")
|
|
358
|
+
return result;
|
|
359
|
+
if (frame.type === "error") {
|
|
360
|
+
const original = record(frame.error, "error");
|
|
361
|
+
const errorEvent = normalized.find((value) => value.type === "error");
|
|
362
|
+
const error = record(errorEvent?.error, "normalized error");
|
|
363
|
+
const terminal = finish("failed", { ...original, code: error.code ?? "upstream_error", message: error.message, upstream_error: original });
|
|
364
|
+
if (terminal)
|
|
365
|
+
result.push(terminal);
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
if (frame.type === "message_start") {
|
|
369
|
+
if (started)
|
|
370
|
+
throw new Error("ZCode message_start 重复");
|
|
371
|
+
started = true;
|
|
372
|
+
const message = record(frame.message, "message");
|
|
373
|
+
if (!normalized.some((value) => value.type === "message_start"))
|
|
374
|
+
throw new Error("ZCode message_start 无法归一化");
|
|
375
|
+
mergeUsage(message.usage);
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
if (!started)
|
|
379
|
+
throw new Error("ZCode 缺少 message_start");
|
|
380
|
+
if (frame.type === "content_block_start") {
|
|
381
|
+
if (block || serverToolByIndex.has(index) || serverResults.has(index) || ignoredBlocks.has(index))
|
|
382
|
+
throw new Error("ZCode content block index 重复");
|
|
383
|
+
const source = record(frame.content_block, "content_block");
|
|
384
|
+
// 分配事件序号前验证起始字段,错误帧不能留下序号空洞。
|
|
385
|
+
if (source.type === "text")
|
|
386
|
+
requiredString(source.text ?? "", "initial text");
|
|
387
|
+
if (source.type === "thinking") {
|
|
388
|
+
requiredString(source.thinking ?? "", "initial thinking");
|
|
389
|
+
if (source.signature !== undefined)
|
|
390
|
+
requiredString(source.signature, "initial signature");
|
|
391
|
+
}
|
|
392
|
+
if (source.type === "redacted_thinking")
|
|
393
|
+
requiredString(source.data, "redacted thinking data");
|
|
394
|
+
if (source.type === "server_tool_use") {
|
|
395
|
+
// z.ai 把 web_search_20250305 映射为 web_search_prime;两种名称都折叠回 web_search_call。
|
|
396
|
+
if (source.name !== "web_search_prime" && source.name !== "web_search") {
|
|
397
|
+
ignoredBlocks.add(index);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
const rawId = typeof source.id === "string" ? source.id.replace(/[^A-Za-z0-9_]/g, "") : "";
|
|
401
|
+
const upstreamId = rawId || `call_${crypto.randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
402
|
+
const input = isZcodeRecord(source.input) ? source.input : {};
|
|
403
|
+
const item = {
|
|
404
|
+
id: `ws_${upstreamId}`, type: "web_search_call", status: "in_progress",
|
|
405
|
+
action: { type: "search", query: serverToolQueryOf(input) },
|
|
406
|
+
};
|
|
407
|
+
const entry = { upstreamId, item, outputIndex: output.length, jsonBuffer: "", blockStopped: false, resultArrived: false };
|
|
408
|
+
serverTools.push(entry);
|
|
409
|
+
serverToolByIndex.set(index, entry);
|
|
410
|
+
serverToolByUpstreamId.set(upstreamId, entry);
|
|
411
|
+
output.push(item);
|
|
412
|
+
result.push(event("response.output_item.added", { output_index: entry.outputIndex, item: structuredClone(item) }));
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
if (source.type === "tool_result" || source.type === "web_search_tool_result") {
|
|
416
|
+
// 服务端搜索结果块:按 tool_use_id 配对折叠进对应 web_search_call;未配对的按未知块忽略。
|
|
417
|
+
const toolUseId = typeof source.tool_use_id === "string" ? source.tool_use_id : "";
|
|
418
|
+
const entry = toolUseId ? serverToolByUpstreamId.get(toolUseId) : undefined;
|
|
419
|
+
if (!entry || entry.resultArrived) {
|
|
420
|
+
ignoredBlocks.add(index);
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
serverResults.set(index, { entry, entries: serverToolResultEntries(source.content), text: "" });
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
const itemId = `item_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
427
|
+
let item;
|
|
428
|
+
if (source.type === "text")
|
|
429
|
+
item = { id: itemId, type: "message", role: "assistant", status: "in_progress", content: [] };
|
|
430
|
+
else if (source.type === "thinking" || source.type === "redacted_thinking")
|
|
431
|
+
item = { id: itemId, type: "reasoning", status: "in_progress", summary: [] };
|
|
432
|
+
else if (source.type === "tool_use") {
|
|
433
|
+
const tool = normalized.find((value) => value.type === "tool_call_start");
|
|
434
|
+
const call = record(tool?.tool_call, "tool call");
|
|
435
|
+
const name = requiredString(call.name, "tool name");
|
|
436
|
+
const mapped = options.tools.get(name);
|
|
437
|
+
if (!mapped)
|
|
438
|
+
throw new Error(`ZCode 返回了未声明的工具:${name}`);
|
|
439
|
+
if (mapped.gateway) {
|
|
440
|
+
if (!options.gatewayTools)
|
|
441
|
+
throw new Error(`ZCode 网关工具 ${name} 缺少执行钩子`);
|
|
442
|
+
// 网关代执行:只跟踪参数,不产出客户端事件,也不进入输出条目。
|
|
443
|
+
blocks.set(index, {
|
|
444
|
+
source, item: { type: "function_call" }, outputIndex: -1, text: "", signature: "",
|
|
445
|
+
arguments: "", hasArgumentDelta: false, stopped: false, gateway: true,
|
|
446
|
+
});
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
item = { id: itemId, type: mapped.custom ? "custom_tool_call" : "function_call", call_id: requiredString(call.id, "tool call id"), name: mapped.name, ...(mapped.namespace === undefined ? {} : { namespace: mapped.namespace }), status: "in_progress", ...(mapped.custom ? { input: "" } : { arguments: "" }) };
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
// 未知块类型不再中断整条流:忽略其后续 delta/stop,由快照保证事件序号连续。
|
|
453
|
+
ignoredBlocks.add(index);
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
const added = { source, item, outputIndex: output.length, text: "", signature: typeof source.signature === "string" ? source.signature : "", arguments: "", hasArgumentDelta: false, stopped: false };
|
|
457
|
+
blocks.set(index, added);
|
|
458
|
+
output.push(item);
|
|
459
|
+
result.push(event("response.output_item.added", { output_index: added.outputIndex, item: structuredClone(item) }));
|
|
460
|
+
if (source.type === "text") {
|
|
461
|
+
const part = { type: "output_text", text: "", annotations: [], logprobs: [] };
|
|
462
|
+
item.content.push(part);
|
|
463
|
+
result.push(event("response.content_part.added", { ...fields(added), content_index: 0, part: structuredClone(part) }));
|
|
464
|
+
appendText(added, requiredString(source.text ?? "", "initial text"), result);
|
|
465
|
+
}
|
|
466
|
+
else if (source.type === "thinking") {
|
|
467
|
+
const part = { type: "summary_text", text: "" };
|
|
468
|
+
item.summary.push(part);
|
|
469
|
+
result.push(event("response.reasoning_summary_part.added", { ...fields(added), summary_index: 0, part: structuredClone(part) }));
|
|
470
|
+
appendText(added, requiredString(source.thinking ?? "", "initial thinking"), result);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
if (frame.type === "content_block_delta") {
|
|
475
|
+
if (ignoredBlocks.has(index))
|
|
476
|
+
return result;
|
|
477
|
+
const serverTool = serverToolByIndex.get(index);
|
|
478
|
+
if (serverTool) {
|
|
479
|
+
const delta = record(frame.delta, "delta");
|
|
480
|
+
// Anthropic 标准形状的 query 走 input_json_delta 增量;z.ai 在 start 帧已给全量。
|
|
481
|
+
if (delta.type === "input_json_delta")
|
|
482
|
+
serverTool.jsonBuffer += requiredString(delta.partial_json, "server tool arguments delta");
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
const serverResult = serverResults.get(index);
|
|
486
|
+
if (serverResult) {
|
|
487
|
+
const delta = record(frame.delta, "delta");
|
|
488
|
+
if (delta.type === "text_delta" && typeof delta.text === "string")
|
|
489
|
+
serverResult.text += delta.text;
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
if (!block || block.stopped)
|
|
493
|
+
throw new Error("ZCode delta 引用了不存在或已结束的 block");
|
|
494
|
+
const delta = record(frame.delta, "delta");
|
|
495
|
+
const content = normalized.find((value) => value.type === "content_delta");
|
|
496
|
+
if (delta.type === "text_delta" || delta.type === "thinking_delta") {
|
|
497
|
+
const expected = delta.type === "text_delta" ? "text" : "thinking";
|
|
498
|
+
if (block.source.type !== expected)
|
|
499
|
+
throw new Error("ZCode delta 与 block 类型不匹配");
|
|
500
|
+
const normalizedDelta = record(content?.delta, "normalized delta");
|
|
501
|
+
appendText(block, requiredString(normalizedDelta[expected], "delta text"), result);
|
|
502
|
+
}
|
|
503
|
+
else if (delta.type === "signature_delta") {
|
|
504
|
+
if (block.source.type !== "thinking")
|
|
505
|
+
throw new Error("ZCode signature 引用了非 thinking block");
|
|
506
|
+
block.signature += requiredString(delta.signature, "signature");
|
|
507
|
+
}
|
|
508
|
+
else if (delta.type === "input_json_delta") {
|
|
509
|
+
if (block.source.type !== "tool_use")
|
|
510
|
+
throw new Error("ZCode tool delta 引用了非 tool block");
|
|
511
|
+
const call = record(normalized.find((value) => value.type === "tool_call_delta")?.tool_call, "normalized tool delta");
|
|
512
|
+
const value = requiredString(call.arguments_delta, "tool arguments delta");
|
|
513
|
+
block.hasArgumentDelta = true;
|
|
514
|
+
block.arguments += value;
|
|
515
|
+
if (block.item.type === "function_call" && !block.gateway) {
|
|
516
|
+
block.item.arguments = block.arguments;
|
|
517
|
+
if (value)
|
|
518
|
+
result.push(event("response.function_call_arguments.delta", { ...fields(block), delta: value }));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else
|
|
522
|
+
throw new Error(`ZCode 不支持的 delta:${String(delta.type)}`);
|
|
523
|
+
return result;
|
|
524
|
+
}
|
|
525
|
+
if (frame.type === "content_block_stop") {
|
|
526
|
+
if (ignoredBlocks.has(index))
|
|
527
|
+
return result;
|
|
528
|
+
const serverTool = serverToolByIndex.get(index);
|
|
529
|
+
if (serverTool) {
|
|
530
|
+
if (serverTool.blockStopped)
|
|
531
|
+
throw new Error("ZCode content block 重复结束");
|
|
532
|
+
serverTool.blockStopped = true;
|
|
533
|
+
if (serverTool.jsonBuffer) {
|
|
534
|
+
let parsed;
|
|
535
|
+
try {
|
|
536
|
+
parsed = JSON.parse(serverTool.jsonBuffer);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
throw new Error("ZCode server tool arguments 包含无效或未结束的 JSON");
|
|
540
|
+
}
|
|
541
|
+
if (isZcodeRecord(parsed))
|
|
542
|
+
serverTool.item.action = { type: "search", query: serverToolQueryOf(parsed) };
|
|
543
|
+
}
|
|
544
|
+
// 此刻不结束条目:等配对的结果块到达后折叠,未到达由 message_stop 兜底。
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
const serverResult = serverResults.get(index);
|
|
548
|
+
if (serverResult) {
|
|
549
|
+
if (!serverResult.entries.length && serverResult.text)
|
|
550
|
+
serverResult.entries = [{ type: "web_search_result", text: serverResult.text }];
|
|
551
|
+
completeServerTool(serverResult.entry, serverResult.entries, "completed", result);
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
if (block?.gateway) {
|
|
555
|
+
if (block.stopped)
|
|
556
|
+
throw new Error("ZCode content block 重复结束");
|
|
557
|
+
const argumentsText = block.hasArgumentDelta ? block.arguments : JSON.stringify(block.source.input ?? {});
|
|
558
|
+
let parsed;
|
|
559
|
+
try {
|
|
560
|
+
parsed = JSON.parse(argumentsText || "{}");
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
throw new Error("ZCode 网关工具 arguments 包含无效或未结束的 JSON");
|
|
564
|
+
}
|
|
565
|
+
if (!isZcodeRecord(parsed))
|
|
566
|
+
throw new Error("ZCode 网关工具 arguments 必须是 JSON 对象");
|
|
567
|
+
block.stopped = true;
|
|
568
|
+
const call = {
|
|
569
|
+
id: requiredString(block.source.id, "gateway tool id"),
|
|
570
|
+
name: requiredString(block.source.name, "gateway tool name"),
|
|
571
|
+
input: parsed,
|
|
572
|
+
};
|
|
573
|
+
pendingGateway.push(call);
|
|
574
|
+
// Input 旁白卡片在本帧同步流出:执行等待期间客户端就能看到 "Executing on server..."。
|
|
575
|
+
// 达到续跑上限的调用不会执行,也就不旁白,保持静默丢弃的原语义。
|
|
576
|
+
if (options.gatewayTools && continuations < options.gatewayTools.maxContinuations) {
|
|
577
|
+
synthesizeMessage(gatewayToolCallNarration(call), result);
|
|
578
|
+
narratedGateway.push(call);
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
if (!block)
|
|
583
|
+
throw new Error("ZCode stop 引用了不存在的 block");
|
|
584
|
+
closeBlock(block, result);
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
if (frame.type === "message_delta") {
|
|
588
|
+
const delta = record(frame.delta, "message delta");
|
|
589
|
+
if (delta.stop_reason !== undefined && delta.stop_reason !== null)
|
|
590
|
+
stopReason = requiredString(delta.stop_reason, "stop_reason");
|
|
591
|
+
mergeUsage(frame.usage);
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
if (frame.type === "message_stop") {
|
|
595
|
+
if ([...blocks.values()].some((value) => !value.stopped) || serverTools.some((value) => !value.blockStopped))
|
|
596
|
+
throw new Error("ZCode message_stop 到达时仍存在未结束的 block");
|
|
597
|
+
if (!stopReason)
|
|
598
|
+
throw new Error("ZCode message_stop 缺少 stop_reason");
|
|
599
|
+
if (!["end_turn", "tool_use", "stop_sequence", "max_tokens"].includes(stopReason))
|
|
600
|
+
throw new Error(`ZCode 不支持的 stop_reason:${stopReason}`);
|
|
601
|
+
stopped = true;
|
|
602
|
+
// 未等到结果块的服务端搜索调用仍要闭合条目,否则 Codex 侧留下永久 in_progress 的输出项。
|
|
603
|
+
const pendingStatus = stopReason === "max_tokens" ? "incomplete" : "completed";
|
|
604
|
+
for (const entry of serverTools)
|
|
605
|
+
completeServerTool(entry, [], pendingStatus, result);
|
|
606
|
+
if (stopReason === "tool_use" && pendingGateway.length > 0 && options.gatewayTools && continuations < options.gatewayTools.maxContinuations) {
|
|
607
|
+
// 网关代执行:批量执行被吸收的调用,以 tool_result 续跑上游;事件序号与输出条目跨腿连续。
|
|
608
|
+
const completed = [];
|
|
609
|
+
for (const call of pendingGateway) {
|
|
610
|
+
let callResult;
|
|
611
|
+
try {
|
|
612
|
+
callResult = await options.gatewayTools.execute(call);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
if (options.signal?.aborted)
|
|
616
|
+
throw error;
|
|
617
|
+
// 执行失败降级为错误说明,让模型改用其他方式而不是整条响应失败。
|
|
618
|
+
callResult = `analyze_image 执行失败:${error instanceof Error ? error.message : String(error)}。请改用其他方式完成,或向用户说明无法识别该图片。`;
|
|
619
|
+
}
|
|
620
|
+
synthesizeMessage(gatewayToolResultNarration(call.name, callResult), result);
|
|
621
|
+
completed.push({ call, result: callResult });
|
|
622
|
+
}
|
|
623
|
+
pendingGateway = [];
|
|
624
|
+
narratedGateway = [];
|
|
625
|
+
continuations++;
|
|
626
|
+
const next = await options.gatewayTools.nextUpstream(completed);
|
|
627
|
+
if (!next.ok || !next.body)
|
|
628
|
+
throw new Error(`ZCode 续跑上游返回 HTTP ${next.status}`);
|
|
629
|
+
// 旧腿已无后续帧,取消以释放其读取与连接。
|
|
630
|
+
void reader?.cancel().catch(() => undefined);
|
|
631
|
+
startLeg(next);
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
// 未续跑(无待执行调用或达到上限):丢弃残余的吸收调用,按本腿结果正常收尾。
|
|
635
|
+
for (const call of narratedGateway) {
|
|
636
|
+
synthesizeMessage(gatewayToolResultNarration(call.name, `${call.name} 调用未执行:本次响应未发起网关续跑。`), result);
|
|
637
|
+
}
|
|
638
|
+
narratedGateway = [];
|
|
639
|
+
pendingGateway = [];
|
|
640
|
+
const terminal = finish(stopReason === "max_tokens" ? "incomplete" : "completed");
|
|
641
|
+
if (terminal)
|
|
642
|
+
result.push(terminal);
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
throw new Error(`ZCode 不支持的 SSE 事件:${String(frame.type)}`);
|
|
646
|
+
}
|
|
647
|
+
async function* events() {
|
|
648
|
+
try {
|
|
649
|
+
if (canceled)
|
|
650
|
+
return;
|
|
651
|
+
yield event("response.created", { response: snapshot("in_progress") });
|
|
652
|
+
yield event("response.in_progress", { response: snapshot("in_progress") });
|
|
653
|
+
if (!reader)
|
|
654
|
+
throw new Error("ZCode 上游响应缺少 body");
|
|
655
|
+
while (!canceled && !finalResponse) {
|
|
656
|
+
const legReader = reader;
|
|
657
|
+
for await (const frame of readFrames(legReader)) {
|
|
658
|
+
if (canceled)
|
|
659
|
+
return;
|
|
660
|
+
const before = reader;
|
|
661
|
+
for (const value of await process(frame))
|
|
662
|
+
yield value;
|
|
663
|
+
if (finalResponse)
|
|
664
|
+
break;
|
|
665
|
+
// message_stop 里触发续跑时 reader 已切到新腿,不再等待旧腿连接收尾。
|
|
666
|
+
if (reader !== before)
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
if (canceled || finalResponse)
|
|
670
|
+
break;
|
|
671
|
+
// 上一腿自然读完;message_stop 里触发续跑时 reader 已切到新腿。
|
|
672
|
+
if (reader !== legReader)
|
|
673
|
+
continue;
|
|
674
|
+
if (!stopped)
|
|
675
|
+
throw new Error("ZCode 上游在 message_stop 前意外 EOF");
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
const terminal = finish("failed", { code: "upstream_protocol_error", message: error instanceof Error ? error.message : String(error) });
|
|
681
|
+
if (terminal && !canceled)
|
|
682
|
+
yield terminal;
|
|
683
|
+
}
|
|
684
|
+
finally {
|
|
685
|
+
cleanup();
|
|
686
|
+
try {
|
|
687
|
+
reader?.releaseLock();
|
|
688
|
+
}
|
|
689
|
+
catch { /* 正在取消的读取由 cancel 收尾。 */ }
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const iterator = events();
|
|
693
|
+
if (!options.stream) {
|
|
694
|
+
for await (const value of iterator)
|
|
695
|
+
safely(() => options.onChunk?.(JSON.stringify(value)));
|
|
696
|
+
return Response.json(finalResponse ?? snapshot("failed"), { status: finalResponse?.status === "failed" || canceled ? 502 : 200 });
|
|
697
|
+
}
|
|
698
|
+
return new Response(new ReadableStream({
|
|
699
|
+
async pull(controller) {
|
|
700
|
+
try {
|
|
701
|
+
const next = await iterator.next();
|
|
702
|
+
if (canceled || next.done) {
|
|
703
|
+
controller.close();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const chunk = `event: ${next.value.type}\ndata: ${JSON.stringify(next.value)}\n\n`;
|
|
707
|
+
safely(() => options.onChunk?.(chunk));
|
|
708
|
+
controller.enqueue(encoder.encode(chunk));
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
if (!canceled)
|
|
712
|
+
controller.error(error);
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
async cancel() {
|
|
716
|
+
handleAbort();
|
|
717
|
+
cleanup();
|
|
718
|
+
await iterator.return(undefined);
|
|
719
|
+
try {
|
|
720
|
+
reader?.releaseLock();
|
|
721
|
+
}
|
|
722
|
+
catch { /* reader 可能已经由生成器释放。 */ }
|
|
723
|
+
},
|
|
724
|
+
}, { highWaterMark: 0 }), { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" } });
|
|
725
|
+
}
|