@agent-proxy/agy 0.16.0 → 0.16.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { bindHostMethods, withHostServices } from "@agent-proxy/plugin-api";
1
+ import { bindHostMethods, getHostServices, withHostServices } from "@agent-proxy/plugin-api";
2
2
  import path from "node:path";
3
3
  import { isIP } from "node:net";
4
4
  import { atomicWrite } from "@agent-proxy/plugin-api";
@@ -247,9 +247,15 @@ function buildAgyAdapter(config, dependencies = {}) {
247
247
  const translated = translateAgyRequest(input, resolvedModel, to);
248
248
  abort.signal.throwIfAborted();
249
249
  const upstream = await transport.streamInfer(credential, translated.wrapper, abort.signal);
250
+ // 流式响应在 forward 返回后由客户端 pull 驱动,下列回调可能在调用上下文之外
251
+ // 触发(Bun 的流调度不继承 withHostServices 的 AsyncLocalStorage);日志等
252
+ // 宿主服务调用必须重入当前宿主上下文,否则 logExchange 抛错被 safely 吞掉、
253
+ // 请求日志静默丢失。
254
+ const host = getHostServices();
255
+ const inHostContext = (callback) => withHostServices(host, callback);
250
256
  const response = await createAgyClientResponse(to, upstream, {
251
257
  model, tools: translated.tools, stream, signal: abort.signal,
252
- sanitizeError: (error) => {
258
+ sanitizeError: (error) => inHostContext(() => {
253
259
  // 上游流内错误的原始对象只进本机网关日志(截断),供排查;客户端只见预定义分类。
254
260
  if (error?.code !== "response_cancelled") {
255
261
  logGatewayError(sink?.processLog, {
@@ -260,17 +266,17 @@ function buildAgyAdapter(config, dependencies = {}) {
260
266
  });
261
267
  }
262
268
  return safeAgyUpstreamError(0, `${String(error.code ?? "")} ${String(error.message ?? "")}`);
263
- },
264
- abort: () => { abort.abort(); cleanup(); },
265
- onChunk: (chunk) => { if (sink)
266
- chunks += chunk; },
267
- onComplete: (payload) => {
269
+ }),
270
+ abort: () => inHostContext(() => { abort.abort(); cleanup(); }),
271
+ onChunk: (chunk) => inHostContext(() => { if (sink)
272
+ chunks += chunk; }),
273
+ onComplete: (payload) => inHostContext(() => {
268
274
  cleanup();
269
275
  const stripped = translated.dropped.length ? `\n--- stripped built-in tools: ${translated.dropped.join(", ")} ---\n` : "";
270
276
  if (!mapResult)
271
277
  log(stream ? 200 : payload.status === "failed" ? 502 : 200, stream ? `${chunks}${stripped}\n--- response.${String(payload.status)} ---\n${JSON.stringify(payload)}`
272
278
  : `${JSON.stringify(payload)}${stripped}`, new Headers({ "content-type": stream ? "text/event-stream" : "application/json" }));
273
- },
279
+ }),
274
280
  });
275
281
  if (!mapResult)
276
282
  return response;
package/dist/request.js CHANGED
@@ -455,6 +455,9 @@ function normalizeSourceItems(body, to) {
455
455
  const calls = [];
456
456
  const results = [];
457
457
  const parts = [];
458
+ // 带响应侧 agn1: 信封的 thinking 块回放为 reasoning 项,由共用路径把签名挂回
459
+ // 下一个 functionCall(上游强制要求 thoughtSignature,缺失直接 400)。
460
+ const signatures = [];
458
461
  for (const block of raw.content) {
459
462
  if (!record(block))
460
463
  fail("messages 包含无效内容块");
@@ -476,7 +479,14 @@ function normalizeSourceItems(body, to) {
476
479
  parts.push({ type: "input_image", image_url: url });
477
480
  }
478
481
  else {
479
- parts.push({ type: "input_text", text: `[thinking] ${typeof block.thinking === "string" ? block.thinking : ""}` });
482
+ const signature = typeof block.signature === "string" ? block.signature : "";
483
+ if (signature.startsWith("agn1:")) {
484
+ // 签名 thinking 块只回放 agn1 信封,思考文本不回放(thought 属于模型私有态)。
485
+ signatures.push({ type: "reasoning", encrypted_content: signature });
486
+ }
487
+ else {
488
+ parts.push({ type: "input_text", text: `[thinking] ${typeof block.thinking === "string" ? block.thinking : ""}` });
489
+ }
480
490
  }
481
491
  }
482
492
  }
@@ -484,6 +494,8 @@ function normalizeSourceItems(body, to) {
484
494
  items.push(...results);
485
495
  else if (parts.length > 0)
486
496
  items.push({ type: "message", role: raw.role, content: parts });
497
+ if (signatures.length > 0)
498
+ items.push(...signatures);
487
499
  if (calls.length > 0)
488
500
  items.push(...calls);
489
501
  }
package/dist/response.js CHANGED
@@ -93,9 +93,37 @@ function finishDisposition(finishReason) {
93
93
  return { status: "failed", failure: "Antigravity 上游报告函数调用格式无效,请检查工具声明后重试" };
94
94
  return { status: "failed", failure: `Antigravity 上游返回不支持的 finishReason:${finishReason}` };
95
95
  }
96
+ /* ====================== 四种客户端协议的独立渲染层 ====================== */
97
+ /**
98
+ * 渲染事件迭代器 → pull 型增量 SSE 响应:客户端每拉一次消费一帧上游事件,
99
+ * 背压沿链路回传;取消时同步取消上游流,让 streamGenerateContent 一并中止。
100
+ */
101
+ function incrementalSseResponse(chunks, upstream) {
102
+ return new Response(new ReadableStream({
103
+ async pull(controller) {
104
+ try {
105
+ const next = await chunks.next();
106
+ if (next.done) {
107
+ controller.close();
108
+ return;
109
+ }
110
+ controller.enqueue(encoder.encode(next.value));
111
+ }
112
+ catch (error) {
113
+ controller.error(error);
114
+ }
115
+ },
116
+ async cancel() {
117
+ // 先取消上游以解除挂起的读取,再终结渲染迭代器释放读取锁。
118
+ await upstream.cancel().catch(() => undefined);
119
+ await chunks.return(undefined).catch(() => undefined);
120
+ },
121
+ }, { highWaterMark: 0 }), { headers: sseHeaders() });
122
+ }
96
123
  /**
97
124
  * AGY 自产 Responses SSE → Chat Completions chunks(含 [DONE])。本渲染器属于
98
- * agy 插件包(不依赖桥接包):解析自身产出的受信事件流并重组为客户端协议外壳。
125
+ * agy 插件包(不依赖桥接包):解析自身产出的受信事件流并重组为客户端协议外壳,
126
+ * 逐帧增量下发(非流式客户端由渲染器聚合)。
99
127
  */
100
128
  async function renderChatClient(responsesSse, state) {
101
129
  const id = `chatcmpl-${crypto.randomUUID().replaceAll("-", "")}`;
@@ -105,48 +133,62 @@ async function renderChatClient(responsesSse, state) {
105
133
  let model = state.model;
106
134
  const frames = [];
107
135
  const push = (delta) => {
108
- frames.push(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] })}\n\n`);
136
+ const chunk = `data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] })}\n\n`;
137
+ frames.push(chunk);
138
+ return chunk;
109
139
  };
110
140
  let contentStarted = false;
111
- for await (const frame of iterateOwnSse(responsesSse)) {
112
- if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
113
- contentStarted = true;
114
- push({ content: frame.delta, role: contentStarted && !frames.length ? "assistant" : undefined });
115
- }
116
- else if (frame.type === "response.reasoning_summary_text.delta" && typeof frame.delta === "string") {
117
- push({ reasoning_content: frame.delta });
118
- }
119
- else if (frame.type === "response.refusal.delta" && typeof frame.delta === "string") {
120
- push({ refusal: frame.delta });
121
- }
122
- else if (frame.type === "response.output_item.added" && record(frame.item, "item").type === "function_call") {
123
- const item = record(frame.item, "item");
124
- push({ role: "assistant", tool_calls: [{ index: Number(frame.output_index ?? 0), id: item.call_id ?? "", type: "function", function: { name: item.name ?? "", arguments: "" } }] });
125
- }
126
- else if (frame.type === "response.function_call_arguments.delta" && typeof frame.delta === "string") {
127
- push({ tool_calls: [{ index: Number(frame.output_index ?? 0), function: { arguments: frame.delta } }] });
128
- }
129
- else if (frame.type === "response.completed" || frame.type === "response.incomplete" || frame.type === "response.failed") {
130
- const response = record(frame.response, "response");
131
- if (typeof response.model === "string")
132
- model = state.model;
133
- const output = Array.isArray(response.output) ? response.output : [];
134
- finish = response.status === "incomplete" ? "length" : output.some((entry) => record(entry, "output").type === "function_call") ? "tool_calls" : "stop";
135
- if (record(response.usage ?? {}, "usage") && response.usage !== null) {
136
- const value = record(response.usage, "usage");
137
- usage = value;
141
+ async function* iterate() {
142
+ for await (const frame of iterateOwnSse(responsesSse)) {
143
+ const from = frames.length;
144
+ if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
145
+ contentStarted = true;
146
+ push({ content: frame.delta, role: contentStarted && !frames.length ? "assistant" : undefined });
138
147
  }
139
- }
140
- }
141
- frames.push(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: {}, finish_reason: finish }] })}\n\n`);
142
- if (usage) {
143
- frames.push(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage })}\n\n`);
148
+ else if (frame.type === "response.reasoning_summary_text.delta" && typeof frame.delta === "string") {
149
+ push({ reasoning_content: frame.delta });
150
+ }
151
+ else if (frame.type === "response.refusal.delta" && typeof frame.delta === "string") {
152
+ push({ refusal: frame.delta });
153
+ }
154
+ else if (frame.type === "response.output_item.added" && record(frame.item, "item").type === "function_call") {
155
+ const item = record(frame.item, "item");
156
+ push({ role: "assistant", tool_calls: [{ index: Number(frame.output_index ?? 0), id: item.call_id ?? "", type: "function", function: { name: item.name ?? "", arguments: "" } }] });
157
+ }
158
+ else if (frame.type === "response.function_call_arguments.delta" && typeof frame.delta === "string") {
159
+ push({ tool_calls: [{ index: Number(frame.output_index ?? 0), function: { arguments: frame.delta } }] });
160
+ }
161
+ else if (frame.type === "response.completed" || frame.type === "response.incomplete" || frame.type === "response.failed") {
162
+ const response = record(frame.response, "response");
163
+ if (typeof response.model === "string")
164
+ model = state.model;
165
+ const output = Array.isArray(response.output) ? response.output : [];
166
+ finish = response.status === "incomplete" ? "length" : output.some((entry) => record(entry, "output").type === "function_call") ? "tool_calls" : "stop";
167
+ if (record(response.usage ?? {}, "usage") && response.usage !== null) {
168
+ const value = record(response.usage, "usage");
169
+ usage = value;
170
+ }
171
+ }
172
+ if (frames.length === from)
173
+ continue;
174
+ const chunk = frames.slice(from).join("");
175
+ safely(() => state.onChunk?.(chunk));
176
+ yield chunk;
177
+ }
178
+ const tail = [
179
+ `data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: {}, finish_reason: finish }] })}\n\n`,
180
+ ...(usage ? [`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage })}\n\n`] : []),
181
+ "data: [DONE]\n\n",
182
+ ];
183
+ frames.push(...tail);
184
+ const text = tail.join("");
185
+ safely(() => state.onChunk?.(text));
186
+ yield text;
144
187
  }
145
- frames.push("data: [DONE]\n\n");
146
- const text = frames.join("");
147
- safely(() => state.onChunk?.(text));
148
188
  if (!state.stream) {
149
189
  // 非流式:聚合为完整 completion JSON。
190
+ for await (const chunk of iterate())
191
+ void chunk;
150
192
  let content = "";
151
193
  const toolCalls = new Map();
152
194
  let reasoning = "";
@@ -192,174 +234,212 @@ async function renderChatClient(responsesSse, state) {
192
234
  };
193
235
  return Response.json(completion);
194
236
  }
195
- return new Response(strBytes(text), { headers: sseHeaders() });
237
+ return incrementalSseResponse(iterate(), responsesSse);
196
238
  }
197
- /** AGY 自产 Responses SSE → Gemini generateContent SSE / 完整 JSON(零桥接依赖)。 */
239
+ /** AGY 自产 Responses SSE → Gemini generateContent SSE / 完整 JSON(零桥接依赖,文本增量下发)。 */
198
240
  async function renderGeminiClient(responsesSse, state) {
199
241
  let finish = "STOP";
200
242
  let model = state.model;
201
243
  const parts = [];
202
244
  const toolArgs = new Map();
203
245
  const toolNames = new Map();
204
- let toolCalled = false;
205
246
  let usage;
206
- for await (const frame of iterateOwnSse(responsesSse)) {
207
- if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
208
- parts.push({ text: frame.delta });
209
- }
210
- else if (frame.type === "response.reasoning_summary_text.delta" && typeof frame.delta === "string") {
211
- parts.push({ text: frame.delta, thought: true });
212
- }
213
- else if (frame.type === "response.output_item.added" && record(frame.item, "item").type === "function_call") {
214
- const item = record(frame.item, "item");
215
- const callId = typeof item.call_id === "string" ? item.call_id : "";
216
- toolArgs.set(callId, "");
217
- toolNames.set(callId, typeof item.name === "string" ? item.name : "");
218
- }
219
- else if (frame.type === "response.function_call_arguments.delta" && typeof frame.delta === "string") {
220
- const callId = [...toolArgs.keys()].at(-1) ?? "";
221
- toolArgs.set(callId, (toolArgs.get(callId) ?? "") + frame.delta);
222
- }
223
- else if (frame.type === "response.completed" || frame.type === "response.incomplete" || frame.type === "response.failed") {
224
- const response = record(frame.response, "response");
225
- if (typeof response.model === "string")
226
- model = state.model;
227
- const output = Array.isArray(response.output) ? response.output : [];
228
- finish = response.status === "incomplete" ? "MAX_TOKENS" : "STOP";
229
- if (response.status === "failed")
230
- finish = "OTHER";
231
- if (record(response.usage ?? {}, "usage") && response.usage !== null)
232
- usage = record(response.usage, "usage");
233
- void output;
234
- }
235
- }
236
- // functionCall parts 依到达顺序附加在正文之后(Gemini 的候选内容是单序列)。
237
- for (const [callId, argsText] of toolArgs) {
238
- let args = {};
239
- try {
240
- args = record(JSON.parse(argsText || "{}"), "args");
241
- }
242
- catch {
243
- args = {};
244
- }
245
- parts.push({ functionCall: { name: toolNames.get(callId) ?? "", args } });
246
- toolCalled = true;
247
- }
248
- void toolCalled;
249
- const inputTokens = typeof usage?.input_tokens === "number" ? usage.input_tokens : 0;
250
- const outputTokens = typeof usage?.output_tokens === "number" ? usage.output_tokens : 0;
251
- const usageMetadata = usage
252
- ? {
247
+ const usageMetadata = () => {
248
+ if (!usage)
249
+ return undefined;
250
+ const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
251
+ const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
252
+ return {
253
253
  promptTokenCount: inputTokens,
254
254
  candidatesTokenCount: outputTokens,
255
255
  totalTokenCount: typeof usage.total_tokens === "number" ? usage.total_tokens : inputTokens + outputTokens,
256
+ };
257
+ };
258
+ async function* iterate() {
259
+ const frameFor = (part) => `data: ${JSON.stringify({ candidates: [{ content: { parts: [part], role: "model" } }], modelVersion: model })}\n\n`;
260
+ for await (const frame of iterateOwnSse(responsesSse)) {
261
+ if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
262
+ const part = { text: frame.delta };
263
+ parts.push(part);
264
+ const text = frameFor(part);
265
+ safely(() => state.onChunk?.(text));
266
+ yield text;
267
+ }
268
+ else if (frame.type === "response.reasoning_summary_text.delta" && typeof frame.delta === "string") {
269
+ const part = { text: frame.delta, thought: true };
270
+ parts.push(part);
271
+ const text = frameFor(part);
272
+ safely(() => state.onChunk?.(text));
273
+ yield text;
274
+ }
275
+ else if (frame.type === "response.output_item.added" && record(frame.item, "item").type === "function_call") {
276
+ const item = record(frame.item, "item");
277
+ const callId = typeof item.call_id === "string" ? item.call_id : "";
278
+ toolArgs.set(callId, "");
279
+ toolNames.set(callId, typeof item.name === "string" ? item.name : "");
280
+ }
281
+ else if (frame.type === "response.function_call_arguments.delta" && typeof frame.delta === "string") {
282
+ const callId = [...toolArgs.keys()].at(-1) ?? "";
283
+ toolArgs.set(callId, (toolArgs.get(callId) ?? "") + frame.delta);
284
+ }
285
+ else if (frame.type === "response.completed" || frame.type === "response.incomplete" || frame.type === "response.failed") {
286
+ const response = record(frame.response, "response");
287
+ if (typeof response.model === "string")
288
+ model = state.model;
289
+ finish = response.status === "incomplete" ? "MAX_TOKENS" : "STOP";
290
+ if (response.status === "failed")
291
+ finish = "OTHER";
292
+ if (record(response.usage ?? {}, "usage") && response.usage !== null)
293
+ usage = record(response.usage, "usage");
294
+ }
256
295
  }
257
- : undefined;
258
- if (state.stream) {
259
- const frames = [];
260
- for (const part of parts) {
261
- const chunk = { candidates: [{ content: { parts: [part], role: "model" } }], modelVersion: model };
262
- frames.push(`data: ${JSON.stringify(chunk)}\n\n`);
263
- }
264
- const terminal = { candidates: [{ content: { role: "model" }, finishReason: finish }], modelVersion: model, ...(usageMetadata ? { usageMetadata } : {}) };
265
- frames.push(`data: ${JSON.stringify(terminal)}\n\n`);
266
- const text = frames.join("");
296
+ // functionCall parts 依到达顺序附加在正文之后(Gemini 的候选内容是单序列)。
297
+ for (const [callId, argsText] of toolArgs) {
298
+ let args = {};
299
+ try {
300
+ args = record(JSON.parse(argsText || "{}"), "args");
301
+ }
302
+ catch {
303
+ args = {};
304
+ }
305
+ const part = { functionCall: { name: toolNames.get(callId) ?? "", args } };
306
+ parts.push(part);
307
+ const text = frameFor(part);
308
+ safely(() => state.onChunk?.(text));
309
+ yield text;
310
+ }
311
+ const terminal = { candidates: [{ content: { role: "model" }, finishReason: finish }], modelVersion: model, ...(usageMetadata() ? { usageMetadata: usageMetadata() } : {}) };
312
+ const text = `data: ${JSON.stringify(terminal)}\n\n`;
267
313
  safely(() => state.onChunk?.(text));
268
- return new Response(strBytes(text), { headers: sseHeaders() });
314
+ yield text;
269
315
  }
316
+ if (state.stream)
317
+ return incrementalSseResponse(iterate(), responsesSse);
318
+ // 非流式:聚合为完整 generateContent JSON。
319
+ for await (const chunk of iterate())
320
+ void chunk;
270
321
  const completion = {
271
322
  candidates: [{ content: { ...(parts.length > 0 ? { parts } : { parts: [{ text: "" }] }), role: "model" }, finishReason: finish }],
272
323
  modelVersion: model,
273
- ...(usageMetadata ? { usageMetadata } : {}),
324
+ ...(usageMetadata() ? { usageMetadata: usageMetadata() } : {}),
274
325
  };
275
326
  safely(() => state.onChunk?.(JSON.stringify(completion)));
276
327
  return Response.json(completion);
277
328
  }
278
- /** AGY 自产 Responses SSE → Anthropic Messages 事件(message_start/块生命周期/终态)。 */
329
+ /** AGY 自产 Responses SSE → Anthropic Messages 事件(message_start/块生命周期/终态),逐帧增量下发。 */
279
330
  async function renderAnthropicClient(responsesSse, state) {
280
331
  const id = `msg_${crypto.randomUUID().replaceAll("-", "")}`;
281
332
  let model = state.model;
282
333
  const frames = [];
334
+ const emit = (event, payload) => {
335
+ frames.push(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
336
+ };
283
337
  let started = false;
284
338
  let blockIndex = -1;
285
339
  let blockKind;
286
340
  let textValue = "";
287
341
  let thinkingValue = "";
288
- const openTool = () => {
342
+ const openTool = (item) => {
289
343
  blockIndex += 1;
290
344
  blockKind = "tool";
291
- frames.push(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: blockIndex, content_block: { type: "tool_use", id: "", name: "", input: {} } })}\n\n`);
345
+ emit("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "tool_use", id: item.call_id ?? "", name: item.name ?? "", input: {} } });
346
+ };
347
+ const openThinking = () => {
348
+ blockIndex += 1;
349
+ blockKind = "thinking";
350
+ thinkingValue = "";
351
+ emit("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "thinking", thinking: "", signature: "" } });
352
+ };
353
+ const openText = () => {
354
+ blockIndex += 1;
355
+ blockKind = "text";
356
+ textValue = "";
357
+ emit("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "text", text: "" } });
292
358
  };
293
359
  const closeBlock = () => {
294
360
  if (blockKind === undefined)
295
361
  return;
296
- frames.push(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: blockIndex })}\n\n`);
362
+ emit("content_block_stop", { type: "content_block_stop", index: blockIndex });
297
363
  blockKind = undefined;
298
364
  };
299
365
  let usage = {};
300
366
  let stopReason = "end_turn";
301
- for await (const frame of iterateOwnSse(responsesSse)) {
302
- if (frame.type === "response.created" || frame.type === "response.in_progress") {
303
- if (started)
304
- continue;
305
- started = true;
306
- const response = record(frame.response, "response");
307
- if (typeof response.model === "string")
308
- model = state.model;
309
- frames.push(`event: message_start\ndata: ${JSON.stringify({ type: "message_start", message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } })}\n\n`);
310
- frames.push('event: ping\ndata: {"type": "ping"}\n\n');
311
- }
312
- else if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
313
- if (blockKind !== "text") {
367
+ async function* iterate() {
368
+ for await (const frame of iterateOwnSse(responsesSse)) {
369
+ const from = frames.length;
370
+ if (frame.type === "response.created" || frame.type === "response.in_progress") {
371
+ if (!started) {
372
+ started = true;
373
+ const response = record(frame.response, "response");
374
+ if (typeof response.model === "string")
375
+ model = state.model;
376
+ emit("message_start", { type: "message_start", message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } });
377
+ emit("ping", { type: "ping" });
378
+ }
379
+ }
380
+ else if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
381
+ if (blockKind !== "text") {
382
+ closeBlock();
383
+ openText();
384
+ }
385
+ textValue += frame.delta;
386
+ emit("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "text_delta", text: frame.delta } });
387
+ }
388
+ else if (frame.type === "response.reasoning_summary_text.delta" && typeof frame.delta === "string") {
389
+ if (blockKind !== "thinking") {
390
+ closeBlock();
391
+ openThinking();
392
+ }
393
+ thinkingValue += frame.delta;
394
+ emit("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "thinking_delta", thinking: frame.delta } });
395
+ }
396
+ else if (frame.type === "response.output_item.added") {
397
+ const item = record(frame.item, "item");
398
+ if (item.type === "function_call") {
399
+ closeBlock();
400
+ openTool(item);
401
+ }
402
+ else if (item.type === "reasoning" && typeof item.encrypted_content === "string" && item.encrypted_content.startsWith("agn1:")) {
403
+ // thoughtSignature 信封项:渲染为带 signature 的独立 thinking 块,客户端
404
+ // 下一轮回传后由请求侧还原挂回 functionCall(上游缺失直接 400)。
405
+ closeBlock();
406
+ openThinking();
407
+ emit("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "signature_delta", signature: item.encrypted_content } });
408
+ closeBlock();
409
+ }
410
+ }
411
+ else if (frame.type === "response.function_call_arguments.delta" && typeof frame.delta === "string") {
412
+ emit("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "input_json_delta", partial_json: frame.delta } });
413
+ }
414
+ else if (frame.type === "response.output_item.done" && record(frame.item, "item").type === "function_call") {
314
415
  closeBlock();
315
- blockIndex += 1;
316
- blockKind = "text";
317
- textValue = "";
318
- frames.push(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: blockIndex, content_block: { type: "text", text: "" } })}\n\n`);
319
416
  }
320
- textValue += frame.delta;
321
- frames.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: blockIndex, delta: { type: "text_delta", text: frame.delta } })}\n\n`);
322
- }
323
- else if (frame.type === "response.reasoning_summary_text.delta" && typeof frame.delta === "string") {
324
- if (blockKind !== "thinking") {
417
+ else if (frame.type === "response.completed" || frame.type === "response.incomplete" || frame.type === "response.failed") {
418
+ const response = record(frame.response, "response");
325
419
  closeBlock();
326
- blockIndex += 1;
327
- blockKind = "thinking";
328
- thinkingValue = "";
329
- frames.push(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: blockIndex, content_block: { type: "thinking", thinking: "" } })}\n\n`);
420
+ if (record(response.usage ?? {}, "usage") && response.usage !== null) {
421
+ const value = record(response.usage, "usage");
422
+ usage = { input_tokens: typeof value.input_tokens === "number" ? value.input_tokens : 0, output_tokens: typeof value.output_tokens === "number" ? value.output_tokens : 0 };
423
+ }
424
+ const output = Array.isArray(response.output) ? response.output : [];
425
+ stopReason = response.status === "incomplete" ? "max_tokens" : output.some((entry) => record(entry, "output").type === "function_call") ? "tool_use" : "end_turn";
330
426
  }
331
- thinkingValue += frame.delta;
332
- frames.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: blockIndex, delta: { type: "thinking_delta", thinking: frame.delta } })}\n\n`);
333
- }
334
- else if (frame.type === "response.output_item.added" && record(frame.item, "item").type === "function_call") {
335
- closeBlock();
336
- openTool();
337
- const item = record(frame.item, "item");
338
- frames.push(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: blockIndex, content_block: { type: "tool_use", id: item.call_id ?? "", name: item.name ?? "", input: {} } })}\n\n`);
339
- }
340
- else if (frame.type === "response.function_call_arguments.delta" && typeof frame.delta === "string") {
341
- frames.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: blockIndex, delta: { type: "input_json_delta", partial_json: frame.delta } })}\n\n`);
342
- }
343
- else if (frame.type === "response.output_item.done" && record(frame.item, "item").type === "function_call") {
344
- closeBlock();
345
- }
346
- else if (frame.type === "response.completed" || frame.type === "response.incomplete" || frame.type === "response.failed") {
347
- const response = record(frame.response, "response");
348
- closeBlock();
349
- if (record(response.usage ?? {}, "usage") && response.usage !== null) {
350
- const value = record(response.usage, "usage");
351
- usage = { input_tokens: typeof value.input_tokens === "number" ? value.input_tokens : 0, output_tokens: typeof value.output_tokens === "number" ? value.output_tokens : 0 };
352
- }
353
- const output = Array.isArray(response.output) ? response.output : [];
354
- stopReason = response.status === "incomplete" ? "max_tokens" : output.some((entry) => record(entry, "output").type === "function_call") ? "tool_use" : "end_turn";
355
- }
427
+ if (frames.length === from)
428
+ continue;
429
+ const chunk = frames.slice(from).join("");
430
+ safely(() => state.onChunk?.(chunk));
431
+ yield chunk;
432
+ }
433
+ emit("message_delta", { type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage: { output_tokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0 } });
434
+ emit("message_stop", { type: "message_stop" });
435
+ const tail = frames.slice(-2).join("");
436
+ safely(() => state.onChunk?.(tail));
437
+ yield tail;
356
438
  }
357
- frames.push(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage: { output_tokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0 } })}\n\n`);
358
- frames.push('event: message_stop\ndata: {"type": "message_stop"}\n\n');
359
- const text = frames.join("");
360
- safely(() => state.onChunk?.(text));
361
439
  if (!state.stream) {
362
440
  // 非流式:聚合为完整 Message JSON。
441
+ for await (const chunk of iterate())
442
+ void chunk;
363
443
  const content = [];
364
444
  let current;
365
445
  for (const frame of frames) {
@@ -388,6 +468,8 @@ async function renderAnthropicClient(responsesSse, state) {
388
468
  target.text = `${target.text}${delta.text}`;
389
469
  else if (delta.type === "thinking_delta" && typeof delta.thinking === "string")
390
470
  target.thinking = `${target.thinking}${delta.thinking}`;
471
+ else if (delta.type === "signature_delta" && typeof delta.signature === "string")
472
+ target.signature = `${target.signature}${delta.signature}`;
391
473
  else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
392
474
  // 参数按块整段缓存后一次性解析(上游 functionCall 本就整帧到达)。
393
475
  try {
@@ -408,7 +490,7 @@ async function renderAnthropicClient(responsesSse, state) {
408
490
  };
409
491
  return Response.json(message);
410
492
  }
411
- return new Response(strBytes(text), { headers: sseHeaders() });
493
+ return incrementalSseResponse(iterate(), responsesSse);
412
494
  }
413
495
  /** 解析 AGY 自产的 Responses SSE(受信输入:严格 JSON,忽略 event 行)。 */
414
496
  async function* iterateOwnSse(body) {
@@ -443,12 +525,6 @@ async function* iterateOwnSse(body) {
443
525
  catch { /* 读取已由取消收尾。 */ }
444
526
  }
445
527
  }
446
- function strBytes(text) {
447
- const bytes = encoder.encode(text);
448
- const buffer = new ArrayBuffer(bytes.byteLength);
449
- new Uint8Array(buffer).set(bytes);
450
- return buffer;
451
- }
452
528
  function sseHeaders() {
453
529
  return new Headers({ "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" });
454
530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-proxy/agy",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Agent Proxy agy 插件;保留基线转换行为。",
5
5
  "type": "module",
6
6
  "exports": {
@@ -40,5 +40,5 @@
40
40
  "dependencies": {
41
41
  "@agent-proxy/plugin-api": "0.16.0"
42
42
  },
43
- "gitHead": "062121f545d3391665789b2e8baae842029262be"
43
+ "gitHead": "559eed932ecf1112d90c17f50a7908411574031a"
44
44
  }