@agent-proxy/agy 0.15.1 → 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 +14 -8
- package/dist/request.js +70 -1
- package/dist/response.js +241 -165
- package/package.json +4 -4
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
|
@@ -16,6 +16,61 @@ function fail(message) { throw new AgyRequestError(message); }
|
|
|
16
16
|
function record(value) {
|
|
17
17
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
18
|
}
|
|
19
|
+
/** AGY 保持独立转换:增补信封先提升为声明,避免进入普通历史回放。 */
|
|
20
|
+
function normalizeAdditionalTools(body) {
|
|
21
|
+
if (!Array.isArray(body.input))
|
|
22
|
+
return body;
|
|
23
|
+
const envelopes = body.input.filter((item) => record(item) && item.type === "additional_tools");
|
|
24
|
+
if (envelopes.length === 0)
|
|
25
|
+
return body;
|
|
26
|
+
if (body.tools !== undefined && !Array.isArray(body.tools))
|
|
27
|
+
fail("additional_tools 合并要求 tools 为数组");
|
|
28
|
+
const groups = [body.tools ?? []];
|
|
29
|
+
for (const envelope of envelopes) {
|
|
30
|
+
if (!Array.isArray(envelope.tools))
|
|
31
|
+
fail("additional_tools.tools 必须是数组");
|
|
32
|
+
groups.push(envelope.tools);
|
|
33
|
+
}
|
|
34
|
+
const merge = (entries, inNamespace = false) => {
|
|
35
|
+
const tools = [];
|
|
36
|
+
const indices = new Map();
|
|
37
|
+
for (const group of entries) {
|
|
38
|
+
for (const raw of group) {
|
|
39
|
+
if (!record(raw) || typeof raw.type !== "string" || !raw.type)
|
|
40
|
+
fail("additional_tools.tools 包含无效工具声明");
|
|
41
|
+
if (["function", "custom", "namespace"].includes(raw.type) && (typeof raw.name !== "string" || !raw.name)) {
|
|
42
|
+
fail("additional_tools 工具 name 必须是非空字符串");
|
|
43
|
+
}
|
|
44
|
+
if (raw.type === "function" && !record(raw.parameters))
|
|
45
|
+
fail(`additional_tools function 工具 ${String(raw.name)} 缺少对象 parameters`);
|
|
46
|
+
let tool = raw;
|
|
47
|
+
if (raw.type === "namespace") {
|
|
48
|
+
if (inNamespace || !Array.isArray(raw.tools))
|
|
49
|
+
fail("additional_tools namespace.tools 必须是工具数组且不得嵌套 namespace");
|
|
50
|
+
tool = { ...raw, tools: merge([raw.tools], true) };
|
|
51
|
+
}
|
|
52
|
+
// function/custom 共用名称身份,顶层声明优先于历史信封中的旧类型。
|
|
53
|
+
const type = tool.type === "function" || tool.type === "custom" ? "callable" : tool.type;
|
|
54
|
+
const key = JSON.stringify([type, tool.namespace ?? null, tool.name ?? null]);
|
|
55
|
+
const index = indices.get(key);
|
|
56
|
+
if (index === undefined) {
|
|
57
|
+
indices.set(key, tools.length);
|
|
58
|
+
tools.push(tool);
|
|
59
|
+
}
|
|
60
|
+
else if (tool.type === "namespace") {
|
|
61
|
+
const existing = tools[index];
|
|
62
|
+
tools[index] = { ...existing, tools: merge([existing.tools, tool.tools], true) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return tools;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
...body,
|
|
70
|
+
tools: merge(groups),
|
|
71
|
+
input: body.input.filter((item) => !record(item) || item.type !== "additional_tools"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
19
74
|
function string(value, field) {
|
|
20
75
|
if (typeof value !== "string")
|
|
21
76
|
fail(`${field} 必须是字符串`);
|
|
@@ -400,6 +455,9 @@ function normalizeSourceItems(body, to) {
|
|
|
400
455
|
const calls = [];
|
|
401
456
|
const results = [];
|
|
402
457
|
const parts = [];
|
|
458
|
+
// 带响应侧 agn1: 信封的 thinking 块回放为 reasoning 项,由共用路径把签名挂回
|
|
459
|
+
// 下一个 functionCall(上游强制要求 thoughtSignature,缺失直接 400)。
|
|
460
|
+
const signatures = [];
|
|
403
461
|
for (const block of raw.content) {
|
|
404
462
|
if (!record(block))
|
|
405
463
|
fail("messages 包含无效内容块");
|
|
@@ -421,7 +479,14 @@ function normalizeSourceItems(body, to) {
|
|
|
421
479
|
parts.push({ type: "input_image", image_url: url });
|
|
422
480
|
}
|
|
423
481
|
else {
|
|
424
|
-
|
|
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
|
+
}
|
|
425
490
|
}
|
|
426
491
|
}
|
|
427
492
|
}
|
|
@@ -429,6 +494,8 @@ function normalizeSourceItems(body, to) {
|
|
|
429
494
|
items.push(...results);
|
|
430
495
|
else if (parts.length > 0)
|
|
431
496
|
items.push({ type: "message", role: raw.role, content: parts });
|
|
497
|
+
if (signatures.length > 0)
|
|
498
|
+
items.push(...signatures);
|
|
432
499
|
if (calls.length > 0)
|
|
433
500
|
items.push(...calls);
|
|
434
501
|
}
|
|
@@ -476,6 +543,8 @@ function normalizeSourceItems(body, to) {
|
|
|
476
543
|
export function translateAgyRequest(body, upstreamModel, to = "responses") {
|
|
477
544
|
if (typeof upstreamModel !== "string" || !upstreamModel)
|
|
478
545
|
fail("上游模型不能为空");
|
|
546
|
+
if (to === "responses")
|
|
547
|
+
body = normalizeAdditionalTools(body);
|
|
479
548
|
body = normalizeSourceItems(body, to);
|
|
480
549
|
if (body.previous_response_id !== undefined && body.previous_response_id !== null && body.previous_response_id !== "") {
|
|
481
550
|
fail("Antigravity 不支持 previous_response_id;请提供完整 input 历史");
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Agent Proxy agy 插件;保留基线转换行为。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@agent-proxy/models": "0.
|
|
38
|
+
"@agent-proxy/models": "0.16.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@agent-proxy/plugin-api": "0.
|
|
41
|
+
"@agent-proxy/plugin-api": "0.16.0"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "559eed932ecf1112d90c17f50a7908411574031a"
|
|
44
44
|
}
|