@agent-proxy/agy 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 +86 -0
- package/dist/catalog.js +442 -0
- package/dist/credentials.d.ts +36 -0
- package/dist/credentials.js +84 -0
- package/dist/fingerprint-data.json +8 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +388 -0
- package/dist/request.d.ts +28 -0
- package/dist/request.js +504 -0
- package/dist/resources/models.js +3854 -0
- package/dist/response.d.ts +22 -0
- package/dist/response.js +445 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.js +84 -0
- package/package.json +41 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgyToolMap } from "./request.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Antigravity v1internal streamGenerateContent(SSE)→ Codex Responses 事件流。
|
|
4
|
+
*
|
|
5
|
+
* 上游每帧是 v1internal 包装:{"response": master.GenerateContentResponse, "traceId"}。
|
|
6
|
+
* 结构对齐 zcode/codebuddy 的 response.ts:严格 SSE 解析、事件序号连续、快照聚合,
|
|
7
|
+
* 客户端非流式时聚合为完整 JSON。thought=true 的 parts 映射为 reasoning 摘要;
|
|
8
|
+
* 函数调用携带的 thoughtSignature 以 agn1: 信封放进 reasoning 项的 encrypted_content
|
|
9
|
+
* 下发(上游要求回放调用时带回,缺失直接 400);functionCall 部件按声明表还原
|
|
10
|
+
* function_call/custom_tool_call。
|
|
11
|
+
*/
|
|
12
|
+
export interface AgyResponseOptions {
|
|
13
|
+
model: string;
|
|
14
|
+
tools: AgyToolMap;
|
|
15
|
+
stream: boolean;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
abort?: () => void;
|
|
18
|
+
onComplete?: (response: Record<string, unknown>) => void;
|
|
19
|
+
onChunk?: (chunk: string) => void;
|
|
20
|
+
sanitizeError?: (error: Record<string, unknown>) => Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createAgyResponse(upstream: Response, options: AgyResponseOptions): Promise<Response>;
|
package/dist/response.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
function safely(callback) {
|
|
3
|
+
try {
|
|
4
|
+
callback?.();
|
|
5
|
+
}
|
|
6
|
+
catch { /* 日志回调不能中断响应。 */ }
|
|
7
|
+
}
|
|
8
|
+
function record(value, label) {
|
|
9
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
10
|
+
throw new Error(`Antigravity ${label} 必须是对象`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** 仅消费一条上游流;严格解析 SSE(多行 data 拼接、注释行忽略)。 */
|
|
14
|
+
async function* readFrames(reader) {
|
|
15
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
16
|
+
let buffer = "";
|
|
17
|
+
let data = [];
|
|
18
|
+
function line(value) {
|
|
19
|
+
if (value === "") {
|
|
20
|
+
if (!data.length)
|
|
21
|
+
return;
|
|
22
|
+
const joined = data.join("\n");
|
|
23
|
+
data = [];
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(joined);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error("Antigravity SSE 包含无效 JSON");
|
|
30
|
+
}
|
|
31
|
+
return record(parsed, "SSE data");
|
|
32
|
+
}
|
|
33
|
+
if (value.startsWith(":"))
|
|
34
|
+
return;
|
|
35
|
+
const colon = value.indexOf(":");
|
|
36
|
+
const field = colon < 0 ? value : value.slice(0, colon);
|
|
37
|
+
let content = colon < 0 ? "" : value.slice(colon + 1);
|
|
38
|
+
if (content.startsWith(" "))
|
|
39
|
+
content = content.slice(1);
|
|
40
|
+
if (field === "data")
|
|
41
|
+
data.push(content);
|
|
42
|
+
}
|
|
43
|
+
while (true) {
|
|
44
|
+
const { value, done } = await reader.read();
|
|
45
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
46
|
+
while (true) {
|
|
47
|
+
const position = buffer.search(/[\r\n]/);
|
|
48
|
+
if (position < 0)
|
|
49
|
+
break;
|
|
50
|
+
if (buffer[position] === "\r" && position + 1 === buffer.length && !done)
|
|
51
|
+
break;
|
|
52
|
+
const width = buffer[position] === "\r" && buffer[position + 1] === "\n" ? 2 : 1;
|
|
53
|
+
const next = line(buffer.slice(0, position));
|
|
54
|
+
buffer = buffer.slice(position + width);
|
|
55
|
+
if (next)
|
|
56
|
+
yield next;
|
|
57
|
+
}
|
|
58
|
+
if (done) {
|
|
59
|
+
if (buffer.length || data.length)
|
|
60
|
+
throw new Error("Antigravity SSE 在事件结束前意外 EOF");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function usageFromUpstream(value) {
|
|
66
|
+
if (value === undefined || value === null)
|
|
67
|
+
return null;
|
|
68
|
+
const usage = record(value, "usageMetadata");
|
|
69
|
+
const count = (key) => typeof usage[key] === "number" ? usage[key] : 0;
|
|
70
|
+
const input = count("promptTokenCount");
|
|
71
|
+
const output = count("candidatesTokenCount");
|
|
72
|
+
const result = { input_tokens: input, output_tokens: output, total_tokens: count("totalTokenCount") || input + output };
|
|
73
|
+
const thoughts = count("thoughtsTokenCount");
|
|
74
|
+
if (thoughts > 0)
|
|
75
|
+
result.output_tokens_details = { reasoning_tokens: thoughts };
|
|
76
|
+
const cached = usage.promptTokensDetails;
|
|
77
|
+
if (cached !== undefined && cached !== null) {
|
|
78
|
+
const details = record(cached, "promptTokensDetails");
|
|
79
|
+
if (typeof details.cachedTokens === "number")
|
|
80
|
+
result.input_tokens_details = { cached_tokens: details.cachedTokens };
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
const SAFETY_FINISH = new Set(["SAFETY", "PROHIBITED_CONTENT", "BLOCKLIST", "SPII", "RECITATION", "IMAGE_SAFETY", "LANGUAGE"]);
|
|
85
|
+
function finishDisposition(finishReason) {
|
|
86
|
+
if (finishReason === undefined || finishReason === "" || finishReason === "STOP")
|
|
87
|
+
return { status: "completed" };
|
|
88
|
+
if (finishReason === "MAX_TOKENS")
|
|
89
|
+
return { status: "incomplete", incompleteReason: "max_output_tokens" };
|
|
90
|
+
if (SAFETY_FINISH.has(finishReason))
|
|
91
|
+
return { status: "incomplete", incompleteReason: "content_filter" };
|
|
92
|
+
if (finishReason === "MALFORMED_FUNCTION_CALL")
|
|
93
|
+
return { status: "failed", failure: "Antigravity 上游报告函数调用格式无效,请检查工具声明后重试" };
|
|
94
|
+
return { status: "failed", failure: `Antigravity 上游返回不支持的 finishReason:${finishReason}` };
|
|
95
|
+
}
|
|
96
|
+
export async function createAgyResponse(upstream, options) {
|
|
97
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
98
|
+
if (!contentType.includes("text/event-stream")) {
|
|
99
|
+
// 上游忽略 alt=sse 时可能整包返回:包装成单帧 SSE 复用同一事件合成路径。
|
|
100
|
+
let payload;
|
|
101
|
+
try {
|
|
102
|
+
payload = record(await upstream.json(), "上游响应");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error("Antigravity 上游响应不是有效 JSON");
|
|
106
|
+
}
|
|
107
|
+
if (payload.error !== undefined) {
|
|
108
|
+
const error = record(payload.error, "error");
|
|
109
|
+
throw new Error(typeof error.message === "string" && error.message ? error.message : "Antigravity 上游响应失败");
|
|
110
|
+
}
|
|
111
|
+
const frames = [payload];
|
|
112
|
+
return streamResponses(new ReadableStream({
|
|
113
|
+
start(controller) {
|
|
114
|
+
controller.enqueue(encoder.encode(frames.map((frame) => `data: ${JSON.stringify(frame)}\n\n`).join("")));
|
|
115
|
+
controller.close();
|
|
116
|
+
},
|
|
117
|
+
}), options);
|
|
118
|
+
}
|
|
119
|
+
if (!upstream.body)
|
|
120
|
+
throw new Error("Antigravity 上游响应缺少 body");
|
|
121
|
+
return streamResponses(upstream.body, options);
|
|
122
|
+
}
|
|
123
|
+
async function streamResponses(body, options) {
|
|
124
|
+
const reader = body.getReader();
|
|
125
|
+
const blocks = new Map();
|
|
126
|
+
const closedBlocks = new Set();
|
|
127
|
+
const output = [];
|
|
128
|
+
const id = `resp_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
129
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
130
|
+
let sequence = 0;
|
|
131
|
+
let finalResponse;
|
|
132
|
+
let finishReason;
|
|
133
|
+
let blockReason;
|
|
134
|
+
let upstreamUsage = null;
|
|
135
|
+
let canceled = false;
|
|
136
|
+
let cleaned = false;
|
|
137
|
+
const snapshot = (status) => ({
|
|
138
|
+
id, object: "response", created_at: createdAt, model: options.model, status,
|
|
139
|
+
output: structuredClone(output), error: null, incomplete_details: null, usage: null,
|
|
140
|
+
});
|
|
141
|
+
const event = (type, fields = {}) => ({ type, sequence_number: sequence++, ...fields });
|
|
142
|
+
const fields = (block) => ({ item_id: block.item.id, output_index: block.outputIndex });
|
|
143
|
+
function finish(status, error) {
|
|
144
|
+
if (finalResponse)
|
|
145
|
+
return;
|
|
146
|
+
for (const block of blocks.values()) {
|
|
147
|
+
if (closedBlocks.has(block))
|
|
148
|
+
continue;
|
|
149
|
+
block.item.status = "incomplete";
|
|
150
|
+
}
|
|
151
|
+
finalResponse = { ...snapshot(status), usage: upstreamUsage };
|
|
152
|
+
if (status === "incomplete") {
|
|
153
|
+
finalResponse.incomplete_details = { reason: finishReason === "MAX_TOKENS" ? "max_output_tokens" : "content_filter" };
|
|
154
|
+
}
|
|
155
|
+
if (error) {
|
|
156
|
+
try {
|
|
157
|
+
finalResponse.error = options.sanitizeError ? options.sanitizeError(error) : error;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
finalResponse.error = { code: "upstream_error", message: "Antigravity 上游响应失败" };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
cleanup();
|
|
164
|
+
safely(() => options.onComplete?.(structuredClone(finalResponse)));
|
|
165
|
+
return event(`response.${status}`, { response: structuredClone(finalResponse) });
|
|
166
|
+
}
|
|
167
|
+
function cleanup() {
|
|
168
|
+
if (cleaned)
|
|
169
|
+
return;
|
|
170
|
+
cleaned = true;
|
|
171
|
+
options.signal?.removeEventListener("abort", handleAbort);
|
|
172
|
+
safely(options.abort);
|
|
173
|
+
}
|
|
174
|
+
function handleAbort() {
|
|
175
|
+
if (finalResponse)
|
|
176
|
+
return;
|
|
177
|
+
canceled = true;
|
|
178
|
+
finish("failed", { code: "response_cancelled", message: "Antigravity 响应已取消" });
|
|
179
|
+
}
|
|
180
|
+
options.signal?.addEventListener("abort", handleAbort, { once: true });
|
|
181
|
+
if (options.signal?.aborted)
|
|
182
|
+
handleAbort();
|
|
183
|
+
const pending = [];
|
|
184
|
+
function emit(value) { pending.push(value); }
|
|
185
|
+
function ensureMessage() {
|
|
186
|
+
const existing = blocks.get("message");
|
|
187
|
+
if (existing)
|
|
188
|
+
return existing;
|
|
189
|
+
const item = { id: `item_${crypto.randomUUID().replaceAll("-", "")}`, type: "message", role: "assistant", status: "in_progress", content: [] };
|
|
190
|
+
const block = { kind: "message", item, outputIndex: output.length, text: "" };
|
|
191
|
+
blocks.set("message", block);
|
|
192
|
+
output.push(item);
|
|
193
|
+
emit(event("response.output_item.added", { output_index: block.outputIndex, item: structuredClone(item) }));
|
|
194
|
+
return block;
|
|
195
|
+
}
|
|
196
|
+
function appendMessageText(value) {
|
|
197
|
+
if (!value)
|
|
198
|
+
return;
|
|
199
|
+
const block = ensureMessage();
|
|
200
|
+
const existing = block.item.content;
|
|
201
|
+
let index = existing.findIndex((part) => part.type === "output_text");
|
|
202
|
+
if (index < 0) {
|
|
203
|
+
const part = { type: "output_text", text: "", annotations: [], logprobs: [] };
|
|
204
|
+
existing.push(part);
|
|
205
|
+
index = existing.length - 1;
|
|
206
|
+
emit(event("response.content_part.added", { ...fields(block), content_index: index, part: structuredClone(part) }));
|
|
207
|
+
}
|
|
208
|
+
block.text += value;
|
|
209
|
+
existing[index].text = block.text;
|
|
210
|
+
emit(event("response.output_text.delta", { ...fields(block), content_index: index, delta: value, logprobs: [] }));
|
|
211
|
+
}
|
|
212
|
+
function ensureReasoning() {
|
|
213
|
+
const existing = blocks.get("reasoning");
|
|
214
|
+
if (existing)
|
|
215
|
+
return existing;
|
|
216
|
+
const item = { id: `item_${crypto.randomUUID().replaceAll("-", "")}`, type: "reasoning", status: "in_progress", summary: [] };
|
|
217
|
+
const block = { kind: "reasoning", item, outputIndex: output.length, text: "" };
|
|
218
|
+
blocks.set("reasoning", block);
|
|
219
|
+
output.push(item);
|
|
220
|
+
emit(event("response.output_item.added", { output_index: block.outputIndex, item: structuredClone(item) }));
|
|
221
|
+
return block;
|
|
222
|
+
}
|
|
223
|
+
function appendReasoning(value) {
|
|
224
|
+
if (!value)
|
|
225
|
+
return;
|
|
226
|
+
const block = ensureReasoning();
|
|
227
|
+
const summary = block.item.summary;
|
|
228
|
+
if (!summary.length) {
|
|
229
|
+
const part = { type: "summary_text", text: "" };
|
|
230
|
+
summary.push(part);
|
|
231
|
+
emit(event("response.reasoning_summary_part.added", { ...fields(block), summary_index: 0, part: structuredClone(part) }));
|
|
232
|
+
}
|
|
233
|
+
block.text += value;
|
|
234
|
+
summary[0].text = block.text;
|
|
235
|
+
emit(event("response.reasoning_summary_text.delta", { ...fields(block), summary_index: 0, delta: value }));
|
|
236
|
+
}
|
|
237
|
+
function openToolCall(index, call, signature) {
|
|
238
|
+
const key = `tool:${index}`;
|
|
239
|
+
if (blocks.has(key))
|
|
240
|
+
throw new Error(`Antigravity 重复的工具调用序号 ${index}`);
|
|
241
|
+
const name = typeof call.name === "string" ? call.name : "";
|
|
242
|
+
if (!name)
|
|
243
|
+
throw new Error("Antigravity functionCall 缺少 name");
|
|
244
|
+
const mapped = options.tools.get(name);
|
|
245
|
+
if (!mapped)
|
|
246
|
+
throw new Error(`Antigravity 返回了未声明的工具:${name}`);
|
|
247
|
+
// 上游要求回放函数调用时携带 thoughtSignature:以 agn1: 信封放进 reasoning 项的
|
|
248
|
+
// encrypted_content 随调用下发,客户端下一轮回传后由请求侧挂回 functionCall。
|
|
249
|
+
if (signature) {
|
|
250
|
+
const sigItem = { id: `item_${crypto.randomUUID().replaceAll("-", "")}`, type: "reasoning", status: "in_progress", summary: [], encrypted_content: `agn1:${signature}` };
|
|
251
|
+
const sigBlock = { kind: "reasoning", item: sigItem, outputIndex: output.length, text: "" };
|
|
252
|
+
blocks.set(`sig:${index}`, sigBlock);
|
|
253
|
+
output.push(sigItem);
|
|
254
|
+
emit(event("response.output_item.added", { output_index: sigBlock.outputIndex, item: structuredClone(sigItem) }));
|
|
255
|
+
}
|
|
256
|
+
const callId = `call_${crypto.randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
257
|
+
const itemId = `item_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
258
|
+
const args = record(call.args ?? {}, "functionCall.args");
|
|
259
|
+
const item = mapped.custom
|
|
260
|
+
? { id: itemId, type: "custom_tool_call", call_id: callId, name: mapped.name, ...(mapped.namespace === undefined ? {} : { namespace: mapped.namespace }), status: "in_progress", input: "" }
|
|
261
|
+
: { id: itemId, type: "function_call", call_id: callId, name: mapped.name, ...(mapped.namespace === undefined ? {} : { namespace: mapped.namespace }), status: "in_progress", arguments: "" };
|
|
262
|
+
const block = { kind: "tool", item, outputIndex: output.length };
|
|
263
|
+
blocks.set(key, block);
|
|
264
|
+
output.push(item);
|
|
265
|
+
emit(event("response.output_item.added", { output_index: block.outputIndex, item: structuredClone(item) }));
|
|
266
|
+
// Gemini 的 functionCall 一次给全量 args,不存在参数增量:直接整段下发。
|
|
267
|
+
if (mapped.custom) {
|
|
268
|
+
const input = typeof args.input === "string" ? args.input : JSON.stringify(args.input ?? null);
|
|
269
|
+
block.item.input = input;
|
|
270
|
+
emit(event("response.custom_tool_call_input.delta", { ...fields(block), delta: input }));
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const argumentsText = JSON.stringify(args);
|
|
274
|
+
block.item.arguments = argumentsText;
|
|
275
|
+
emit(event("response.function_call_arguments.delta", { ...fields(block), delta: argumentsText }));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function closeBlocks(result) {
|
|
279
|
+
for (const block of blocks.values()) {
|
|
280
|
+
if (closedBlocks.has(block))
|
|
281
|
+
continue;
|
|
282
|
+
closedBlocks.add(block);
|
|
283
|
+
const base = fields(block);
|
|
284
|
+
if (block.kind === "message") {
|
|
285
|
+
const textIndex = block.item.content.findIndex((entry) => entry.type === "output_text");
|
|
286
|
+
if (textIndex >= 0 && block.text) {
|
|
287
|
+
const part = block.item.content[textIndex];
|
|
288
|
+
result.push(event("response.output_text.done", { ...base, content_index: textIndex, text: block.text, logprobs: [] }));
|
|
289
|
+
result.push(event("response.content_part.done", { ...base, content_index: textIndex, part: structuredClone(part) }));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (block.kind === "reasoning") {
|
|
293
|
+
if (block.item.summary.length) {
|
|
294
|
+
result.push(event("response.reasoning_summary_text.done", { ...base, summary_index: 0, text: block.text }));
|
|
295
|
+
result.push(event("response.reasoning_summary_part.done", { ...base, summary_index: 0, part: { type: "summary_text", text: block.text } }));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
if (block.item.type === "custom_tool_call") {
|
|
300
|
+
result.push(event("response.custom_tool_call_input.done", { ...base, input: block.item.input }));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
result.push(event("response.function_call_arguments.done", { ...base, arguments: block.item.arguments }));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
block.item.status = "completed";
|
|
307
|
+
result.push(event("response.output_item.done", { output_index: block.outputIndex, item: structuredClone(block.item) }));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function* events() {
|
|
311
|
+
try {
|
|
312
|
+
if (canceled)
|
|
313
|
+
return;
|
|
314
|
+
yield event("response.created", { response: snapshot("in_progress") });
|
|
315
|
+
yield event("response.in_progress", { response: snapshot("in_progress") });
|
|
316
|
+
let toolIndex = 0;
|
|
317
|
+
for await (const frame of readFrames(reader)) {
|
|
318
|
+
if (canceled)
|
|
319
|
+
return;
|
|
320
|
+
if (frame.error !== undefined) {
|
|
321
|
+
const original = record(frame.error, "error");
|
|
322
|
+
const terminal = finish("failed", {
|
|
323
|
+
code: typeof original.code === "number" || typeof original.code === "string" ? String(original.code) : "upstream_error",
|
|
324
|
+
message: typeof original.message === "string" && original.message ? original.message : "Antigravity 上游响应失败",
|
|
325
|
+
upstream_error: original,
|
|
326
|
+
});
|
|
327
|
+
if (terminal && !canceled)
|
|
328
|
+
yield terminal;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const inner = frame.response === undefined || frame.response === null ? undefined : record(frame.response, "response");
|
|
332
|
+
if (inner === undefined)
|
|
333
|
+
continue;
|
|
334
|
+
const usage = usageFromUpstream(inner.usageMetadata);
|
|
335
|
+
if (usage)
|
|
336
|
+
upstreamUsage = usage;
|
|
337
|
+
const feedback = inner.promptFeedback === undefined || inner.promptFeedback === null
|
|
338
|
+
? undefined : record(inner.promptFeedback, "promptFeedback");
|
|
339
|
+
if (feedback !== undefined && typeof feedback.blockReason === "string" && feedback.blockReason) {
|
|
340
|
+
blockReason = feedback.blockReason;
|
|
341
|
+
}
|
|
342
|
+
if (!Array.isArray(inner.candidates))
|
|
343
|
+
continue;
|
|
344
|
+
const candidate = record(inner.candidates[0] ?? {}, "candidates[0]");
|
|
345
|
+
if (typeof candidate.finishReason === "string" && candidate.finishReason)
|
|
346
|
+
finishReason = candidate.finishReason;
|
|
347
|
+
const content = candidate.content === undefined || candidate.content === null
|
|
348
|
+
? {} : record(candidate.content, "candidates[0].content");
|
|
349
|
+
if (!Array.isArray(content.parts)) {
|
|
350
|
+
for (const value of pending.splice(0))
|
|
351
|
+
yield value;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
// thoughtSignature 可能独占一个 part(无 text/functionCall),也可能附着在
|
|
355
|
+
// functionCall part 上:独立出现的缓存给下一个函数调用,附着的一并下发。
|
|
356
|
+
let pendingSignature;
|
|
357
|
+
for (const raw of content.parts) {
|
|
358
|
+
const part = record(raw, "candidates[0].content.parts");
|
|
359
|
+
const signature = typeof part.thoughtSignature === "string" && part.thoughtSignature
|
|
360
|
+
? part.thoughtSignature : undefined;
|
|
361
|
+
if (part.functionCall !== undefined && part.functionCall !== null) {
|
|
362
|
+
openToolCall(toolIndex++, record(part.functionCall, "functionCall"), signature ?? pendingSignature);
|
|
363
|
+
pendingSignature = undefined;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (signature)
|
|
367
|
+
pendingSignature = signature;
|
|
368
|
+
if (typeof part.text !== "string" || !part.text)
|
|
369
|
+
continue;
|
|
370
|
+
if (part.thought === true)
|
|
371
|
+
appendReasoning(part.text);
|
|
372
|
+
else
|
|
373
|
+
appendMessageText(part.text);
|
|
374
|
+
}
|
|
375
|
+
for (const value of pending.splice(0))
|
|
376
|
+
yield value;
|
|
377
|
+
}
|
|
378
|
+
if (canceled)
|
|
379
|
+
return;
|
|
380
|
+
if (blockReason !== undefined) {
|
|
381
|
+
const terminal = finish("incomplete");
|
|
382
|
+
if (terminal)
|
|
383
|
+
yield terminal;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const disposition = finishDisposition(finishReason);
|
|
387
|
+
if (disposition.status === "failed") {
|
|
388
|
+
const terminal = finish("failed", { code: "agy_finish_error", message: disposition.failure });
|
|
389
|
+
if (terminal && !canceled)
|
|
390
|
+
yield terminal;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const result = [];
|
|
394
|
+
closeBlocks(result);
|
|
395
|
+
const terminal = finish(disposition.status);
|
|
396
|
+
for (const value of result)
|
|
397
|
+
yield value;
|
|
398
|
+
if (terminal)
|
|
399
|
+
yield terminal;
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
const terminal = finish("failed", { code: "upstream_protocol_error", message: error instanceof Error ? error.message : String(error) });
|
|
403
|
+
if (terminal && !canceled)
|
|
404
|
+
yield terminal;
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
cleanup();
|
|
408
|
+
try {
|
|
409
|
+
reader.releaseLock();
|
|
410
|
+
}
|
|
411
|
+
catch { /* 正在取消的读取由 cancel 收尾。 */ }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const iterator = events();
|
|
415
|
+
if (!options.stream) {
|
|
416
|
+
for await (const value of iterator)
|
|
417
|
+
safely(() => options.onChunk?.(JSON.stringify(value)));
|
|
418
|
+
const response = finalResponse ?? snapshot("failed");
|
|
419
|
+
return Response.json(response, { status: finalResponse?.status === "failed" || canceled ? 502 : 200 });
|
|
420
|
+
}
|
|
421
|
+
return new Response(new ReadableStream({
|
|
422
|
+
async pull(controller) {
|
|
423
|
+
try {
|
|
424
|
+
const next = await iterator.next();
|
|
425
|
+
if (canceled || next.done) {
|
|
426
|
+
controller.close();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const chunk = `event: ${next.value.type}\ndata: ${JSON.stringify(next.value)}\n\n`;
|
|
430
|
+
safely(() => options.onChunk?.(chunk));
|
|
431
|
+
controller.enqueue(encoder.encode(chunk));
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
if (!canceled)
|
|
435
|
+
controller.error(error);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
async cancel() {
|
|
439
|
+
handleAbort();
|
|
440
|
+
cleanup();
|
|
441
|
+
await iterator.return(undefined);
|
|
442
|
+
reader.cancel().catch(() => undefined);
|
|
443
|
+
},
|
|
444
|
+
}, { highWaterMark: 0 }), { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" } });
|
|
445
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AgyCredentials } from "./credentials.ts";
|
|
2
|
+
/** Antigravity 内置系统提示词(Go 模板剥离变量后),供目录条目的 base_instructions 使用。 */
|
|
3
|
+
export declare const AGY_AGENT_SYSTEM_PROMPT: string;
|
|
4
|
+
/**
|
|
5
|
+
* Antigravity 上游传输:Cloud Code Assist 内部 REST(`/v1internal:*`)。
|
|
6
|
+
*
|
|
7
|
+
* 本机 CLI 是 daily 通道构建(daily-cloudcode-pa.googleapis.com),网关默认指向
|
|
8
|
+
* stable 通道的 cloudcode-pa.googleapis.com,两者线协议一致;端点可经依赖注入覆盖。
|
|
9
|
+
* headers 只带最小集:Authorization、Content-Type、User-Agent;客户端指纹的其余
|
|
10
|
+
* 部分按 CLI 形态放在请求 body(metadata / user_agent)里。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 默认端点取 daily 通道:本机 CLI 实际使用该通道,且实测该账号在 stable 通道上
|
|
14
|
+
* 管理面可用但推理一律 429(2026-10-06 验证);端点可经依赖注入覆盖。
|
|
15
|
+
*/
|
|
16
|
+
export declare const AGY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
17
|
+
/** consumer 档的哨兵配额项目;CLI 缓存文件 default_project_id.txt 即此字面量。 */
|
|
18
|
+
export declare const AGY_DEFAULT_PROJECT = "aicode-consumers";
|
|
19
|
+
/**
|
|
20
|
+
* 指纹对齐用的客户端标识。上游鉴权只依赖 Bearer token;UA 与 body 内 userAgent
|
|
21
|
+
* 字段按官方 CLI 形态对齐(版本与 changelist 成对取自本机 agy 构建,见
|
|
22
|
+
* fingerprint-data.json;格式源自 2026-10-06 MITM 抓包)。
|
|
23
|
+
*/
|
|
24
|
+
export declare const AGY_CLIENT_VERSION: string;
|
|
25
|
+
export declare const AGY_USER_AGENT: string;
|
|
26
|
+
export declare class AgyTransportError extends Error {
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly body: string;
|
|
29
|
+
constructor(status: number, body: string);
|
|
30
|
+
}
|
|
31
|
+
/** 上游请求 headers:白名单构造,绝不透传客户端任何 header(含入站授权)。 */
|
|
32
|
+
export declare function buildAgyHeaders(credential: AgyCredentials): Headers;
|
|
33
|
+
export interface AgyTransportOptions {
|
|
34
|
+
endpoint?: string;
|
|
35
|
+
fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
36
|
+
}
|
|
37
|
+
export interface AgyTransport {
|
|
38
|
+
fetchModels(credential: AgyCredentials, signal?: AbortSignal): Promise<unknown>;
|
|
39
|
+
streamInfer(credential: AgyCredentials, body: Record<string, unknown>, signal?: AbortSignal): Promise<Response>;
|
|
40
|
+
}
|
|
41
|
+
export declare function createAgyTransport(options?: AgyTransportOptions): AgyTransport;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fingerprintData from "./fingerprint-data.json";
|
|
2
|
+
const FINGERPRINT = fingerprintData;
|
|
3
|
+
/** Antigravity 内置系统提示词(Go 模板剥离变量后),供目录条目的 base_instructions 使用。 */
|
|
4
|
+
export const AGY_AGENT_SYSTEM_PROMPT = FINGERPRINT.agentSystemPrompt;
|
|
5
|
+
/**
|
|
6
|
+
* Antigravity 上游传输:Cloud Code Assist 内部 REST(`/v1internal:*`)。
|
|
7
|
+
*
|
|
8
|
+
* 本机 CLI 是 daily 通道构建(daily-cloudcode-pa.googleapis.com),网关默认指向
|
|
9
|
+
* stable 通道的 cloudcode-pa.googleapis.com,两者线协议一致;端点可经依赖注入覆盖。
|
|
10
|
+
* headers 只带最小集:Authorization、Content-Type、User-Agent;客户端指纹的其余
|
|
11
|
+
* 部分按 CLI 形态放在请求 body(metadata / user_agent)里。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 默认端点取 daily 通道:本机 CLI 实际使用该通道,且实测该账号在 stable 通道上
|
|
15
|
+
* 管理面可用但推理一律 429(2026-10-06 验证);端点可经依赖注入覆盖。
|
|
16
|
+
*/
|
|
17
|
+
export const AGY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
18
|
+
/** consumer 档的哨兵配额项目;CLI 缓存文件 default_project_id.txt 即此字面量。 */
|
|
19
|
+
export const AGY_DEFAULT_PROJECT = "aicode-consumers";
|
|
20
|
+
/**
|
|
21
|
+
* 指纹对齐用的客户端标识。上游鉴权只依赖 Bearer token;UA 与 body 内 userAgent
|
|
22
|
+
* 字段按官方 CLI 形态对齐(版本与 changelist 成对取自本机 agy 构建,见
|
|
23
|
+
* fingerprint-data.json;格式源自 2026-10-06 MITM 抓包)。
|
|
24
|
+
*/
|
|
25
|
+
export const AGY_CLIENT_VERSION = FINGERPRINT.version;
|
|
26
|
+
export const AGY_USER_AGENT = `antigravity/cli/${AGY_CLIENT_VERSION} (aidev_client; os_type=${process.platform === "darwin" ? "darwin" : process.platform}; `
|
|
27
|
+
+ `arch=${process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "amd64" : process.arch}; cl=${FINGERPRINT.changelist}; auth_method=consumer)`;
|
|
28
|
+
export class AgyTransportError extends Error {
|
|
29
|
+
status;
|
|
30
|
+
body;
|
|
31
|
+
constructor(status, body) {
|
|
32
|
+
super(`Antigravity 上游返回 HTTP ${status}`);
|
|
33
|
+
this.name = "AgyTransportError";
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.body = body;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** 上游请求 headers:白名单构造,绝不透传客户端任何 header(含入站授权)。 */
|
|
39
|
+
export function buildAgyHeaders(credential) {
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
headers.set("authorization", `Bearer ${credential.accessToken}`);
|
|
42
|
+
headers.set("content-type", "application/json");
|
|
43
|
+
headers.set("user-agent", AGY_USER_AGENT);
|
|
44
|
+
return headers;
|
|
45
|
+
}
|
|
46
|
+
function endpointUrl(endpoint, method, stream) {
|
|
47
|
+
return `${endpoint.replace(/\/+$/, "")}/v1internal:${method}${stream ? "?alt=sse" : ""}`;
|
|
48
|
+
}
|
|
49
|
+
export function createAgyTransport(options = {}) {
|
|
50
|
+
const endpoint = options.endpoint ?? AGY_ENDPOINT;
|
|
51
|
+
const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
|
|
52
|
+
const post = async (credential, method, body, stream, signal) => {
|
|
53
|
+
const response = await doFetch(endpointUrl(endpoint, method, stream), {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: buildAgyHeaders(credential),
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
redirect: "manual",
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
// 错误正文可能回显请求内容或凭据,只保留状态码与截断文本供分类,不进入客户端。
|
|
62
|
+
const text = await response.text().catch(() => "");
|
|
63
|
+
throw new AgyTransportError(response.status, text.slice(0, 4_096));
|
|
64
|
+
}
|
|
65
|
+
return response;
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
async fetchModels(credential, signal) {
|
|
69
|
+
// 真实 CLI 的 body 只有 project(MITM 抓包验证)。
|
|
70
|
+
const response = await post(credential, "fetchAvailableModels", {
|
|
71
|
+
project: AGY_DEFAULT_PROJECT,
|
|
72
|
+
}, false, signal);
|
|
73
|
+
try {
|
|
74
|
+
return await response.json();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new AgyTransportError(response.status, "");
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async streamInfer(credential, body, signal) {
|
|
81
|
+
return post(credential, "streamGenerateContent", body, true, signal);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-proxy/agy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"description": "Agent Proxy agy 插件;保留基线转换行为。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./*": {
|
|
13
|
+
"types": "./dist/*.d.ts",
|
|
14
|
+
"import": "./dist/*.js",
|
|
15
|
+
"default": "./dist/*.js"
|
|
16
|
+
},
|
|
17
|
+
"./*.json": "./dist/*.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun ../../scripts/build-library.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
26
|
+
"prepack": "bun ../../scripts/verify-package.ts ."
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@agent-proxy/models": "0.11.1"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@agent-proxy/plugin-api": "0.11.1"
|
|
41
|
+
},
|
|
42
|
+
"gitHead": "affd71836c6cb82ec474edc5efa46146eb92398f"
|
|
43
|
+
}
|