@agent-proxy/agy 0.12.0 → 0.12.2
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.d.ts +13 -5
- package/dist/index.js +26 -45
- package/dist/request.d.ts +2 -1
- package/dist/request.js +207 -1
- package/dist/response.d.ts +6 -0
- package/dist/response.js +378 -0
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type { HostServices, GatewayPlugin } from "@agent-proxy/plugin-api";
|
|
1
|
+
import type { HostServices, GatewayPlugin, ClientProtocol } from "@agent-proxy/plugin-api";
|
|
2
2
|
import type { AgyCredentials } from "./credentials.ts";
|
|
3
3
|
import { isAgyModel, mergeAgyCatalog } from "./catalog.ts";
|
|
4
4
|
import type { AgentConfig, ModelCatalog, ProcessLogTarget } from "@agent-proxy/plugin-api";
|
|
5
|
+
declare module "@agent-proxy/plugin-api" {
|
|
6
|
+
interface AgentConfig {
|
|
7
|
+
/**
|
|
8
|
+
* 是否开启调试转储:上游错误时把完整请求体落盘到日志目录(当前覆盖 agy 上游 400 的 agy-debug-400.json)。
|
|
9
|
+
* 与 requestLogging 互相独立,默认关闭。
|
|
10
|
+
*/
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Antigravity(agy)入口门面:凭据只读消费(过期即失败,绝不刷新)、fetchAvailableModels
|
|
7
16
|
* 动态目录、Responses ↔ v1internal streamGenerateContent 双向转换。`catalog()` 供
|
|
@@ -29,8 +38,7 @@ export interface AgyDependencies {
|
|
|
29
38
|
/** 进程日志目标(gateway.log)。 */
|
|
30
39
|
processLog?: ProcessLogTarget;
|
|
31
40
|
}
|
|
32
|
-
export declare function
|
|
33
|
-
export declare function validateAgyConfig<Config extends AgentConfig>(config: Config): void;
|
|
41
|
+
export declare function validateAgyConfig(config: AgentConfig): void;
|
|
34
42
|
export declare function agyError(status: number, message: string, type?: string): Response;
|
|
35
43
|
export interface SafeAgyError {
|
|
36
44
|
code: string;
|
|
@@ -42,12 +50,12 @@ export interface SafeAgyError {
|
|
|
42
50
|
export declare function safeAgyUpstreamError(status: number, body: string): SafeAgyError;
|
|
43
51
|
export { isAgyModel, mergeAgyCatalog };
|
|
44
52
|
export { AGY_RUNTIME_ARTIFACTS } from "./catalog.ts";
|
|
45
|
-
export declare function createAgyAdapter
|
|
53
|
+
export declare function createAgyAdapter(config: AgentConfig, dependencies?: AgyDependencies): {
|
|
46
54
|
catalog(): Promise<ModelCatalog>;
|
|
47
55
|
/** 显式刷新不吞错;无 last-good 时由命令报告失败。 */
|
|
48
56
|
refreshCatalog(): Promise<ModelCatalog>;
|
|
49
57
|
reloadCatalog(): Promise<ModelCatalog>;
|
|
50
|
-
forward(request: Request, input: Record<string, unknown>, mapResult?: (payload: Record<string, unknown>) => Response): Promise<Response>;
|
|
58
|
+
forward(request: Request, input: Record<string, unknown>, to?: ClientProtocol, mapResult?: (payload: Record<string, unknown>) => Response): Promise<Response>;
|
|
51
59
|
close(): void;
|
|
52
60
|
};
|
|
53
61
|
export declare const AGY_MODEL_PATTERNS: readonly string[];
|
package/dist/index.js
CHANGED
|
@@ -7,26 +7,16 @@ import { agyTokenStale, loadAgyCredentials, AgyCredentialError, defaultAgyCreden
|
|
|
7
7
|
import { createAgyTransport, AgyTransportError } from "./transport.js";
|
|
8
8
|
import { AGY_PREFIX, agyUpstreamModel, createAgyCatalogStore, isAgyModel, mergeAgyCatalog, resolveAgyFamilyModel, } from "./catalog.js";
|
|
9
9
|
import { translateAgyRequest, AgyRequestError } from "./request.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createAgyClientResponse } from "./response.js";
|
|
11
11
|
import { localTime, logExchange, logGroupFromPath, requestLogDir } from "@agent-proxy/plugin-api";
|
|
12
12
|
import { logGatewayError, logRequestSummary } from "@agent-proxy/plugin-api";
|
|
13
13
|
/** CLI 的模型轮询周期是 360 秒(见调研文档 §5.5),网关按同一节奏刷新目录。 */
|
|
14
14
|
const DEFAULT_CATALOG_REFRESH_INTERVAL_MS = 360_000;
|
|
15
|
-
export function agyEnabled(config) {
|
|
16
|
-
return config.agy === undefined || config.agy === true;
|
|
17
|
-
}
|
|
18
15
|
export function validateAgyConfig(config) {
|
|
19
|
-
if (config.agy !== undefined && typeof config.agy !== "boolean")
|
|
20
|
-
throw new Error("agy 必须为 boolean");
|
|
21
|
-
if (!agyEnabled(config))
|
|
22
|
-
return;
|
|
23
16
|
const host = config.host;
|
|
24
17
|
if (!(host === "localhost" || host === "::1" || host === "[::1]" || (isIP(host) === 4 && host.startsWith("127.")))) {
|
|
25
18
|
throw new Error("启用 agy 时网关只能监听环回地址");
|
|
26
19
|
}
|
|
27
|
-
if (config.prefix && (AGY_PREFIX.startsWith(config.prefix) || config.prefix.startsWith(AGY_PREFIX))) {
|
|
28
|
-
throw new Error(`启用 agy 时 ${AGY_PREFIX} 前缀保留给 Antigravity,请调整第三方 prefix`);
|
|
29
|
-
}
|
|
30
20
|
}
|
|
31
21
|
export function agyError(status, message, type = "invalid_request_error") {
|
|
32
22
|
return Response.json({ error: { type, message } }, { status });
|
|
@@ -54,7 +44,6 @@ export function safeAgyUpstreamError(status, body) {
|
|
|
54
44
|
}
|
|
55
45
|
function buildAgyAdapter(config, dependencies = {}) {
|
|
56
46
|
validateAgyConfig(config);
|
|
57
|
-
const enabled = agyEnabled(config);
|
|
58
47
|
const credentialFile = dependencies.credentialFile ?? defaultAgyCredentialFile();
|
|
59
48
|
const loadCredentials = async () => {
|
|
60
49
|
if (dependencies.credentials)
|
|
@@ -68,35 +57,31 @@ function buildAgyAdapter(config, dependencies = {}) {
|
|
|
68
57
|
};
|
|
69
58
|
const transport = createAgyTransport({ endpoint: dependencies.endpoint, fetch: dependencies.fetch });
|
|
70
59
|
// 请求日志与调试转储共用同一目录解析规则;两者开关(requestLogging / debug)互相独立。
|
|
71
|
-
const logDir = requestLogDir(
|
|
60
|
+
const logDir = requestLogDir();
|
|
72
61
|
const sink = config.requestLogging === true ? {
|
|
73
62
|
dir: logDir,
|
|
74
63
|
maxLogs: Math.max(0, Math.trunc(config.maxRequestLogs ?? 0)),
|
|
75
64
|
processLog: dependencies.processLog,
|
|
76
65
|
} : undefined;
|
|
77
|
-
const store =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
: undefined;
|
|
66
|
+
const store = createAgyCatalogStore({
|
|
67
|
+
cacheDirectory: dependencies.cacheDirectory ?? resolvePaths().runtimeHome,
|
|
68
|
+
credentials: loadCredentials,
|
|
69
|
+
codexModelsCacheFile: dependencies.codexModelsCacheFile,
|
|
70
|
+
endpoint: dependencies.endpoint,
|
|
71
|
+
fetchCatalog: (credential) => transport.fetchModels(credential, AbortSignal.timeout(30_000)),
|
|
72
|
+
catalogMode: dependencies.catalogMode,
|
|
73
|
+
});
|
|
87
74
|
const schedule = dependencies.setInterval ?? setInterval;
|
|
88
75
|
const cancel = dependencies.clearInterval ?? clearInterval;
|
|
89
76
|
let timer;
|
|
90
77
|
let startupRefresh;
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
timer.unref?.();
|
|
99
|
-
}
|
|
78
|
+
if (dependencies.refreshCatalogOnStart !== false) {
|
|
79
|
+
startupRefresh = store.refresh();
|
|
80
|
+
void startupRefresh.catch(() => { });
|
|
81
|
+
}
|
|
82
|
+
if (dependencies.catalogMode !== "manual") {
|
|
83
|
+
timer = schedule(() => { void store.refresh().catch(() => { }); }, Math.max(1_000, dependencies.catalogRefreshIntervalMs ?? DEFAULT_CATALOG_REFRESH_INTERVAL_MS));
|
|
84
|
+
timer.unref?.();
|
|
100
85
|
}
|
|
101
86
|
const activeRequests = new Set();
|
|
102
87
|
/** 已知模型集合(含重定向前的旧 id);空目录视为不可校验,透传由上游判定。 */
|
|
@@ -116,8 +101,6 @@ function buildAgyAdapter(config, dependencies = {}) {
|
|
|
116
101
|
};
|
|
117
102
|
/** 尽力加载目录元数据(含磁盘缓存回退);失败时保持未加载状态,由上游判定。 */
|
|
118
103
|
const loadCatalogMeta = async () => {
|
|
119
|
-
if (!store)
|
|
120
|
-
return undefined;
|
|
121
104
|
try {
|
|
122
105
|
await startupRefresh?.catch(() => { });
|
|
123
106
|
return updateCatalogMeta(await store.catalog()).models;
|
|
@@ -127,24 +110,24 @@ function buildAgyAdapter(config, dependencies = {}) {
|
|
|
127
110
|
let closed = false;
|
|
128
111
|
return {
|
|
129
112
|
async catalog() {
|
|
130
|
-
if (
|
|
113
|
+
if (closed)
|
|
131
114
|
return { models: [] };
|
|
132
115
|
// 系统提示词已在 buildAgyCatalog 合成时替换(含 model_messages 模板),缓存即成品。
|
|
133
116
|
return { models: (await loadCatalogMeta()) ?? [] };
|
|
134
117
|
},
|
|
135
118
|
/** 显式刷新不吞错;无 last-good 时由命令报告失败。 */
|
|
136
119
|
async refreshCatalog() {
|
|
137
|
-
if (
|
|
120
|
+
if (closed)
|
|
138
121
|
return { models: [] };
|
|
139
122
|
await store.refresh();
|
|
140
123
|
return updateCatalogMeta(await store.catalog());
|
|
141
124
|
},
|
|
142
125
|
async reloadCatalog() {
|
|
143
|
-
if (
|
|
126
|
+
if (closed)
|
|
144
127
|
return { models: [] };
|
|
145
128
|
return updateCatalogMeta(await store.reload());
|
|
146
129
|
},
|
|
147
|
-
async forward(request, input, mapResult) {
|
|
130
|
+
async forward(request, input, to = "responses", mapResult) {
|
|
148
131
|
const start = Date.now();
|
|
149
132
|
const requestTime = localTime();
|
|
150
133
|
const incoming = new URL(request.url);
|
|
@@ -175,8 +158,8 @@ function buildAgyAdapter(config, dependencies = {}) {
|
|
|
175
158
|
log(status, JSON.stringify({ error: { type, message } }), response.headers);
|
|
176
159
|
return response;
|
|
177
160
|
};
|
|
178
|
-
if (
|
|
179
|
-
return fail(503, "Antigravity
|
|
161
|
+
if (closed)
|
|
162
|
+
return fail(503, "Antigravity 入口已关闭", "configuration_error");
|
|
180
163
|
const abort = new AbortController();
|
|
181
164
|
const onAbort = () => abort.abort(request.signal.reason);
|
|
182
165
|
let cleaned = false;
|
|
@@ -261,10 +244,10 @@ function buildAgyAdapter(config, dependencies = {}) {
|
|
|
261
244
|
}
|
|
262
245
|
}
|
|
263
246
|
}
|
|
264
|
-
const translated = translateAgyRequest(input, resolvedModel);
|
|
247
|
+
const translated = translateAgyRequest(input, resolvedModel, to);
|
|
265
248
|
abort.signal.throwIfAborted();
|
|
266
249
|
const upstream = await transport.streamInfer(credential, translated.wrapper, abort.signal);
|
|
267
|
-
const response = await
|
|
250
|
+
const response = await createAgyClientResponse(to, upstream, {
|
|
268
251
|
model, tools: translated.tools, stream, signal: abort.signal,
|
|
269
252
|
sanitizeError: (error) => {
|
|
270
253
|
// 上游流内错误的原始对象只进本机网关日志(截断),供排查;客户端只见预定义分类。
|
|
@@ -375,9 +358,7 @@ export function createAgyPlugin(host, dependencies = {}) {
|
|
|
375
358
|
const input = await context.body.json();
|
|
376
359
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
377
360
|
return Response.json({ error: { type: "invalid_request_error", message: "请求正文必须是 JSON 对象" } }, { status: 400 });
|
|
378
|
-
|
|
379
|
-
return Response.json({ error: { type: "invalid_request_error", message: "Agy 模型仅支持 Responses API" } }, { status: 400 });
|
|
380
|
-
return adapter.forward(context.request, input);
|
|
361
|
+
return adapter.forward(context.request, input, context.to);
|
|
381
362
|
},
|
|
382
363
|
getCatalog: () => adapter.catalog(),
|
|
383
364
|
refreshCatalog: () => adapter.refreshCatalog(),
|
package/dist/request.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClientProtocol } from "@agent-proxy/plugin-api";
|
|
1
2
|
/**
|
|
2
3
|
* Codex Responses → Antigravity v1internal generateContent 请求转换。
|
|
3
4
|
*
|
|
@@ -25,4 +26,4 @@ export interface AgyTranslatedRequest {
|
|
|
25
26
|
/** reasoning.effort → Gemini thinkingLevel;unknown 档位映射到最近的低/高档。
|
|
26
27
|
* 档位名同时是多档位家族的上游变体后缀,catalog.ts 复用该映射做 effort → 变体解析。 */
|
|
27
28
|
export declare const THINKING_LEVELS: Record<string, string>;
|
|
28
|
-
export declare function translateAgyRequest(body: Record<string, unknown>, upstreamModel: string): AgyTranslatedRequest;
|
|
29
|
+
export declare function translateAgyRequest(body: Record<string, unknown>, upstreamModel: string, to?: ClientProtocol): AgyTranslatedRequest;
|
package/dist/request.js
CHANGED
|
@@ -268,9 +268,215 @@ function translateTools(value) {
|
|
|
268
268
|
visit(value);
|
|
269
269
|
return { tools, map, dropped };
|
|
270
270
|
}
|
|
271
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Chat Completions / Anthropic 源归一化为 Responses 形状的 history 项与参数,
|
|
273
|
+
* 复用既有 Responses → CloudCodeAssist 构建路径;三种来源共用同一工具身份表。
|
|
274
|
+
*/
|
|
275
|
+
function normalizeSourceItems(body, to) {
|
|
276
|
+
if (to === "responses")
|
|
277
|
+
return body;
|
|
278
|
+
if (to === "gemini") {
|
|
279
|
+
if (!Array.isArray(body.contents))
|
|
280
|
+
fail("contents 必须是数组");
|
|
281
|
+
const items = [];
|
|
282
|
+
const systemTexts = [];
|
|
283
|
+
const instruction = body.systemInstruction;
|
|
284
|
+
if (typeof instruction === "string" && instruction)
|
|
285
|
+
systemTexts.push(instruction);
|
|
286
|
+
else if (record(instruction) && Array.isArray(instruction.parts)) {
|
|
287
|
+
for (const part of instruction.parts) {
|
|
288
|
+
if (record(part) && typeof part.text === "string" && part.text)
|
|
289
|
+
systemTexts.push(part.text);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let callSeq = 0;
|
|
293
|
+
const nameToOpenCall = new Map();
|
|
294
|
+
const idByCallId = new Map();
|
|
295
|
+
for (const message of body.contents) {
|
|
296
|
+
if (!record(message) || (message.role !== "user" && message.role !== "model")) {
|
|
297
|
+
fail("contents 必须是 user/model 角色条目");
|
|
298
|
+
}
|
|
299
|
+
for (const raw of Array.isArray(message.parts) ? message.parts : []) {
|
|
300
|
+
if (!record(raw))
|
|
301
|
+
fail("parts 包含非对象块");
|
|
302
|
+
if (typeof raw.text === "string" && raw.text) {
|
|
303
|
+
if (raw.thought === true)
|
|
304
|
+
continue; // 思考块不进入 CloudCodeAssist 回放。
|
|
305
|
+
items.push({ type: "message", role: message.role === "model" ? "assistant" : "user", content: raw.text });
|
|
306
|
+
}
|
|
307
|
+
else if (record(raw.functionCall)) {
|
|
308
|
+
const name = typeof raw.functionCall.name === "string" && raw.functionCall.name ? raw.functionCall.name : fail("functionCall.name 必须是非空字符串");
|
|
309
|
+
const id = typeof raw.functionCall.id === "string" && raw.functionCall.id ? raw.functionCall.id : `agy_gemini_call_${callSeq++}`;
|
|
310
|
+
idByCallId.set(id, id);
|
|
311
|
+
if (!(typeof raw.functionCall.id === "string" && raw.functionCall.id))
|
|
312
|
+
nameToOpenCall.set(name, id);
|
|
313
|
+
items.push({ type: "function_call", call_id: id, name, arguments: JSON.stringify(record(raw.functionCall.args) ? raw.functionCall.args : {}) });
|
|
314
|
+
}
|
|
315
|
+
else if (record(raw.functionResponse)) {
|
|
316
|
+
const name = typeof raw.functionResponse.name === "string" ? raw.functionResponse.name : "";
|
|
317
|
+
let id = typeof raw.functionResponse.id === "string" && raw.functionResponse.id ? raw.functionResponse.id : nameToOpenCall.get(name);
|
|
318
|
+
if (!id)
|
|
319
|
+
fail(`functionResponse 无法配对到 functionCall(${name || "未命名"})`);
|
|
320
|
+
nameToOpenCall.delete(name);
|
|
321
|
+
items.push({ type: "function_call_output", call_id: id, output: JSON.stringify(record(raw.functionResponse.response) ? raw.functionResponse.response : raw.functionResponse.response ?? "") });
|
|
322
|
+
}
|
|
323
|
+
else if (record(raw.inlineData) || record(raw.fileData)) {
|
|
324
|
+
fail("Antigravity gemini 入口暂不支持图片 parts,请改用 responses/chat/anthropic 入口");
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
fail("parts 包含不支持的块");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
...body,
|
|
333
|
+
instructions: [typeof body.instructions === "string" ? body.instructions : "", ...systemTexts].filter(Boolean).join("\n\n") || undefined,
|
|
334
|
+
input: items,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (to === "chat-completions") {
|
|
338
|
+
if (!Array.isArray(body.messages))
|
|
339
|
+
fail("messages 必须是数组");
|
|
340
|
+
const items = [];
|
|
341
|
+
for (const raw of body.messages) {
|
|
342
|
+
if (!record(raw))
|
|
343
|
+
fail("messages 包含非对象项");
|
|
344
|
+
if (raw.role === "system" || raw.role === "developer") {
|
|
345
|
+
items.push({ type: "message", role: raw.role, content: raw.content });
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (raw.role === "tool") {
|
|
349
|
+
items.push({ type: "function_call_output", call_id: raw.tool_call_id ?? "", output: raw.content });
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (raw.role === "assistant" && Array.isArray(raw.tool_calls)) {
|
|
353
|
+
if (typeof raw.content === "string" && raw.content)
|
|
354
|
+
items.push({ type: "message", role: "assistant", content: raw.content });
|
|
355
|
+
for (const call of raw.tool_calls) {
|
|
356
|
+
if (!record(call) || !record(call.function))
|
|
357
|
+
fail("assistant.tool_calls 包含无效调用");
|
|
358
|
+
items.push({ type: "function_call", call_id: typeof call.id === "string" ? call.id : "", name: call.function.name ?? "", arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments ?? {}) });
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (raw.role === "user" || raw.role === "assistant") {
|
|
363
|
+
items.push({ type: "message", role: raw.role, content: raw.content });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
fail(`不支持的消息 role ${String(raw.role)}`);
|
|
367
|
+
}
|
|
368
|
+
const normalized = {
|
|
369
|
+
...body,
|
|
370
|
+
input: items,
|
|
371
|
+
...(typeof body.max_tokens === "number" ? { max_output_tokens: body.max_tokens } : typeof body.max_completion_tokens === "number" ? { max_output_tokens: body.max_completion_tokens } : {}),
|
|
372
|
+
};
|
|
373
|
+
delete normalized.messages;
|
|
374
|
+
delete normalized.max_tokens;
|
|
375
|
+
delete normalized.max_completion_tokens;
|
|
376
|
+
if (typeof body.reasoning_effort === "string" && body.reasoning === undefined) {
|
|
377
|
+
normalized.reasoning = { effort: body.reasoning_effort };
|
|
378
|
+
delete normalized.reasoning_effort;
|
|
379
|
+
}
|
|
380
|
+
if (record(body.tool_choice) && body.tool_choice.type === "function" && record(body.tool_choice.function)) {
|
|
381
|
+
normalized.tool_choice = { type: "function", name: body.tool_choice.function.name };
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(body.tools)) {
|
|
384
|
+
normalized.tools = body.tools.map((raw) => {
|
|
385
|
+
if (record(raw) && raw.type === "function" && record(raw.function)) {
|
|
386
|
+
return { type: "function", name: raw.function.name, ...(typeof raw.function.description === "string" ? { description: raw.function.description } : {}), parameters: record(raw.function.parameters) ? raw.function.parameters : { type: "object" } };
|
|
387
|
+
}
|
|
388
|
+
return raw;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return normalized;
|
|
392
|
+
}
|
|
393
|
+
// anthropic 源
|
|
394
|
+
if (!Array.isArray(body.messages))
|
|
395
|
+
fail("messages 必须是数组");
|
|
396
|
+
const items = [];
|
|
397
|
+
for (const raw of body.messages) {
|
|
398
|
+
if (!record(raw) || !Array.isArray(raw.content))
|
|
399
|
+
fail("messages 必须是 user/assistant 与内容块数组");
|
|
400
|
+
const calls = [];
|
|
401
|
+
const results = [];
|
|
402
|
+
const parts = [];
|
|
403
|
+
for (const block of raw.content) {
|
|
404
|
+
if (!record(block))
|
|
405
|
+
fail("messages 包含无效内容块");
|
|
406
|
+
if (block.type === "tool_use") {
|
|
407
|
+
calls.push({ type: "function_call", call_id: typeof block.id === "string" ? block.id : "", name: typeof block.name === "string" ? block.name : "", arguments: JSON.stringify(block.input ?? {}) });
|
|
408
|
+
}
|
|
409
|
+
else if (block.type === "tool_result") {
|
|
410
|
+
results.push({ type: "function_call_output", call_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "", output: block.content });
|
|
411
|
+
}
|
|
412
|
+
else if (block.type === "text" || block.type === "image" || block.type === "thinking") {
|
|
413
|
+
if (block.type === "text") {
|
|
414
|
+
parts.push({ type: "input_text", text: typeof block.text === "string" ? block.text : "" });
|
|
415
|
+
}
|
|
416
|
+
else if (block.type === "image") {
|
|
417
|
+
const source = record(block.source) ? block.source : undefined;
|
|
418
|
+
const url = source && source.type === "url" && typeof source.url === "string"
|
|
419
|
+
? source.url
|
|
420
|
+
: `data:${String(source?.media_type ?? "image/png")};base64,${String(source?.data ?? "")}`;
|
|
421
|
+
parts.push({ type: "input_image", image_url: url });
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
parts.push({ type: "input_text", text: `[thinking] ${typeof block.thinking === "string" ? block.thinking : ""}` });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (results.length > 0)
|
|
429
|
+
items.push(...results);
|
|
430
|
+
else if (parts.length > 0)
|
|
431
|
+
items.push({ type: "message", role: raw.role, content: parts });
|
|
432
|
+
if (calls.length > 0)
|
|
433
|
+
items.push(...calls);
|
|
434
|
+
}
|
|
435
|
+
// Anthropic 的 system(字符串或块数组)语义等同 Responses instructions。
|
|
436
|
+
let systemInstruction;
|
|
437
|
+
if (typeof body.system === "string")
|
|
438
|
+
systemInstruction = body.system;
|
|
439
|
+
else if (Array.isArray(body.system)) {
|
|
440
|
+
const texts = [];
|
|
441
|
+
for (const part of body.system) {
|
|
442
|
+
if (record(part) && typeof part.text === "string")
|
|
443
|
+
texts.push(part.text);
|
|
444
|
+
}
|
|
445
|
+
if (texts.length > 0)
|
|
446
|
+
systemInstruction = texts.join("\n\n");
|
|
447
|
+
}
|
|
448
|
+
const normalized = {
|
|
449
|
+
...body,
|
|
450
|
+
input: items,
|
|
451
|
+
...(systemInstruction !== undefined ? { instructions: systemInstruction } : {}),
|
|
452
|
+
...(typeof body.max_tokens === "number" ? { max_output_tokens: body.max_tokens } : {}),
|
|
453
|
+
};
|
|
454
|
+
delete normalized.messages;
|
|
455
|
+
delete normalized.max_tokens;
|
|
456
|
+
delete normalized.system;
|
|
457
|
+
if (record(body.tool_choice)) {
|
|
458
|
+
const choice = body.tool_choice;
|
|
459
|
+
if (choice.type === "tool" && typeof choice.name === "string")
|
|
460
|
+
normalized.tool_choice = { type: "function", name: choice.name };
|
|
461
|
+
else if (choice.type === "any")
|
|
462
|
+
normalized.tool_choice = "required";
|
|
463
|
+
else if (choice.type === "auto" || choice.type === "none")
|
|
464
|
+
normalized.tool_choice = choice.type;
|
|
465
|
+
}
|
|
466
|
+
if (Array.isArray(body.tools)) {
|
|
467
|
+
normalized.tools = body.tools.map((raw) => {
|
|
468
|
+
if (record(raw) && typeof raw.name === "string") {
|
|
469
|
+
return { type: "function", name: raw.name, ...(typeof raw.description === "string" ? { description: raw.description } : {}), parameters: record(raw.input_schema) ? raw.input_schema : { type: "object" } };
|
|
470
|
+
}
|
|
471
|
+
return raw;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return normalized;
|
|
475
|
+
}
|
|
476
|
+
export function translateAgyRequest(body, upstreamModel, to = "responses") {
|
|
272
477
|
if (typeof upstreamModel !== "string" || !upstreamModel)
|
|
273
478
|
fail("上游模型不能为空");
|
|
479
|
+
body = normalizeSourceItems(body, to);
|
|
274
480
|
if (body.previous_response_id !== undefined && body.previous_response_id !== null && body.previous_response_id !== "") {
|
|
275
481
|
fail("Antigravity 不支持 previous_response_id;请提供完整 input 历史");
|
|
276
482
|
}
|
package/dist/response.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClientProtocol } from "@agent-proxy/plugin-api";
|
|
1
2
|
import type { AgyToolMap } from "./request.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Antigravity v1internal streamGenerateContent(SSE)→ Codex Responses 事件流。
|
|
@@ -19,4 +20,9 @@ export interface AgyResponseOptions {
|
|
|
19
20
|
onChunk?: (chunk: string) => void;
|
|
20
21
|
sanitizeError?: (error: Record<string, unknown>) => Record<string, unknown>;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 四种客户端协议统一入口:responses 源沿用既有 Responses 输出;chat-completions /
|
|
25
|
+
* anthropic / gemini 源由 AGY 自产 Responses SSE 独立渲染为对应协议外壳(零桥接依赖)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function createAgyClientResponse(to: ClientProtocol, upstream: Response, options: AgyResponseOptions): Promise<Response>;
|
|
22
28
|
export declare function createAgyResponse(upstream: Response, options: AgyResponseOptions): Promise<Response>;
|
package/dist/response.js
CHANGED
|
@@ -93,6 +93,384 @@ function finishDisposition(finishReason) {
|
|
|
93
93
|
return { status: "failed", failure: "Antigravity 上游报告函数调用格式无效,请检查工具声明后重试" };
|
|
94
94
|
return { status: "failed", failure: `Antigravity 上游返回不支持的 finishReason:${finishReason}` };
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* AGY 自产 Responses SSE → Chat Completions chunks(含 [DONE])。本渲染器属于
|
|
98
|
+
* agy 插件包(不依赖桥接包):解析自身产出的受信事件流并重组为客户端协议外壳。
|
|
99
|
+
*/
|
|
100
|
+
async function renderChatClient(responsesSse, state) {
|
|
101
|
+
const id = `chatcmpl-${crypto.randomUUID().replaceAll("-", "")}`;
|
|
102
|
+
const created = Math.floor(Date.now() / 1000);
|
|
103
|
+
let usage;
|
|
104
|
+
let finish = "stop";
|
|
105
|
+
let model = state.model;
|
|
106
|
+
const frames = [];
|
|
107
|
+
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`);
|
|
109
|
+
};
|
|
110
|
+
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;
|
|
138
|
+
}
|
|
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`);
|
|
144
|
+
}
|
|
145
|
+
frames.push("data: [DONE]\n\n");
|
|
146
|
+
const text = frames.join("");
|
|
147
|
+
safely(() => state.onChunk?.(text));
|
|
148
|
+
if (!state.stream) {
|
|
149
|
+
// 非流式:聚合为完整 completion JSON。
|
|
150
|
+
let content = "";
|
|
151
|
+
const toolCalls = new Map();
|
|
152
|
+
let reasoning = "";
|
|
153
|
+
let refusal = "";
|
|
154
|
+
for (const frame of frames) {
|
|
155
|
+
if (!frame.startsWith("data: ") || frame.includes("[DONE]"))
|
|
156
|
+
continue;
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(frame.slice(6));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const choice = Array.isArray(parsed.choices) ? record(parsed.choices[0] ?? {}, "choice") : {};
|
|
165
|
+
const delta = record(choice.delta ?? {}, "delta");
|
|
166
|
+
if (typeof delta.content === "string")
|
|
167
|
+
content += delta.content;
|
|
168
|
+
if (typeof delta.reasoning_content === "string")
|
|
169
|
+
reasoning += delta.reasoning_content;
|
|
170
|
+
if (typeof delta.refusal === "string")
|
|
171
|
+
refusal += delta.refusal;
|
|
172
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
173
|
+
for (const raw of delta.tool_calls) {
|
|
174
|
+
const call = record(raw, "tool_call");
|
|
175
|
+
const index = typeof call.index === "number" ? call.index : 0;
|
|
176
|
+
const existing = toolCalls.get(index) ?? { id: "", function: { name: "", arguments: "" } };
|
|
177
|
+
if (typeof call.id === "string" && call.id)
|
|
178
|
+
existing.id = call.id;
|
|
179
|
+
const fn = record(call.function ?? {}, "function");
|
|
180
|
+
if (typeof fn.name === "string" && fn.name)
|
|
181
|
+
existing.function.name = fn.name;
|
|
182
|
+
if (typeof fn.arguments === "string")
|
|
183
|
+
existing.function.arguments += fn.arguments;
|
|
184
|
+
toolCalls.set(index, existing);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const completion = {
|
|
189
|
+
id, object: "chat.completion", created, model,
|
|
190
|
+
choices: [{ index: 0, message: { role: "assistant", ...(content === "" && toolCalls.size ? { content: null } : { content }), ...(reasoning ? { reasoning_content: reasoning } : {}), ...(refusal ? { refusal } : {}), ...(toolCalls.size ? { tool_calls: [...toolCalls.entries()].sort(([a], [b]) => a - b).map(([, call]) => ({ id: call.id, type: "function", function: call.function })) } : {}) }, finish_reason: finish }],
|
|
191
|
+
...(usage ? { usage } : {}),
|
|
192
|
+
};
|
|
193
|
+
return Response.json(completion);
|
|
194
|
+
}
|
|
195
|
+
return new Response(strBytes(text), { headers: sseHeaders() });
|
|
196
|
+
}
|
|
197
|
+
/** AGY 自产 Responses SSE → Gemini generateContent SSE / 完整 JSON(零桥接依赖)。 */
|
|
198
|
+
async function renderGeminiClient(responsesSse, state) {
|
|
199
|
+
let finish = "STOP";
|
|
200
|
+
let model = state.model;
|
|
201
|
+
const parts = [];
|
|
202
|
+
const toolArgs = new Map();
|
|
203
|
+
const toolNames = new Map();
|
|
204
|
+
let toolCalled = false;
|
|
205
|
+
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
|
+
? {
|
|
253
|
+
promptTokenCount: inputTokens,
|
|
254
|
+
candidatesTokenCount: outputTokens,
|
|
255
|
+
totalTokenCount: typeof usage.total_tokens === "number" ? usage.total_tokens : inputTokens + outputTokens,
|
|
256
|
+
}
|
|
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("");
|
|
267
|
+
safely(() => state.onChunk?.(text));
|
|
268
|
+
return new Response(strBytes(text), { headers: sseHeaders() });
|
|
269
|
+
}
|
|
270
|
+
const completion = {
|
|
271
|
+
candidates: [{ content: { ...(parts.length > 0 ? { parts } : { parts: [{ text: "" }] }), role: "model" }, finishReason: finish }],
|
|
272
|
+
modelVersion: model,
|
|
273
|
+
...(usageMetadata ? { usageMetadata } : {}),
|
|
274
|
+
};
|
|
275
|
+
safely(() => state.onChunk?.(JSON.stringify(completion)));
|
|
276
|
+
return Response.json(completion);
|
|
277
|
+
}
|
|
278
|
+
/** AGY 自产 Responses SSE → Anthropic Messages 事件(message_start/块生命周期/终态)。 */
|
|
279
|
+
async function renderAnthropicClient(responsesSse, state) {
|
|
280
|
+
const id = `msg_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
281
|
+
let model = state.model;
|
|
282
|
+
const frames = [];
|
|
283
|
+
let started = false;
|
|
284
|
+
let blockIndex = -1;
|
|
285
|
+
let blockKind;
|
|
286
|
+
let textValue = "";
|
|
287
|
+
let thinkingValue = "";
|
|
288
|
+
const openTool = () => {
|
|
289
|
+
blockIndex += 1;
|
|
290
|
+
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`);
|
|
292
|
+
};
|
|
293
|
+
const closeBlock = () => {
|
|
294
|
+
if (blockKind === undefined)
|
|
295
|
+
return;
|
|
296
|
+
frames.push(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: blockIndex })}\n\n`);
|
|
297
|
+
blockKind = undefined;
|
|
298
|
+
};
|
|
299
|
+
let usage = {};
|
|
300
|
+
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") {
|
|
314
|
+
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
|
+
}
|
|
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") {
|
|
325
|
+
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`);
|
|
330
|
+
}
|
|
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
|
+
}
|
|
356
|
+
}
|
|
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
|
+
if (!state.stream) {
|
|
362
|
+
// 非流式:聚合为完整 Message JSON。
|
|
363
|
+
const content = [];
|
|
364
|
+
let current;
|
|
365
|
+
for (const frame of frames) {
|
|
366
|
+
if (!frame.startsWith("event: content_block_start"))
|
|
367
|
+
continue;
|
|
368
|
+
const data = JSON.parse(frame.split("\ndata: ")[1]);
|
|
369
|
+
const block = record(data.content_block, "content_block");
|
|
370
|
+
if (block.type === "text")
|
|
371
|
+
current = { type: "text", text: "" };
|
|
372
|
+
else if (block.type === "thinking")
|
|
373
|
+
current = { type: "thinking", thinking: "", signature: "" };
|
|
374
|
+
else
|
|
375
|
+
current = { type: "tool_use", id: block.id ?? "", name: block.name ?? "", input: {} };
|
|
376
|
+
content.push(current);
|
|
377
|
+
}
|
|
378
|
+
for (const frame of frames) {
|
|
379
|
+
if (!frame.startsWith("event: content_block_delta"))
|
|
380
|
+
continue;
|
|
381
|
+
const data = JSON.parse(frame.split("\ndata: ")[1]);
|
|
382
|
+
const index = typeof data.index === "number" ? data.index : 0;
|
|
383
|
+
const delta = record(data.delta, "delta");
|
|
384
|
+
const target = content[index];
|
|
385
|
+
if (!target)
|
|
386
|
+
continue;
|
|
387
|
+
if (delta.type === "text_delta" && typeof delta.text === "string")
|
|
388
|
+
target.text = `${target.text}${delta.text}`;
|
|
389
|
+
else if (delta.type === "thinking_delta" && typeof delta.thinking === "string")
|
|
390
|
+
target.thinking = `${target.thinking}${delta.thinking}`;
|
|
391
|
+
else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
392
|
+
// 参数按块整段缓存后一次性解析(上游 functionCall 本就整帧到达)。
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(delta.partial_json);
|
|
395
|
+
if (record(parsed, "tool input"))
|
|
396
|
+
target.input = parsed;
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
target.input = {};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const message = {
|
|
404
|
+
id, type: "message", role: "assistant", model,
|
|
405
|
+
content: content.length ? content : [{ type: "text", text: "" }],
|
|
406
|
+
stop_reason: stopReason, stop_sequence: null,
|
|
407
|
+
usage: { input_tokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0, output_tokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0 },
|
|
408
|
+
};
|
|
409
|
+
return Response.json(message);
|
|
410
|
+
}
|
|
411
|
+
return new Response(strBytes(text), { headers: sseHeaders() });
|
|
412
|
+
}
|
|
413
|
+
/** 解析 AGY 自产的 Responses SSE(受信输入:严格 JSON,忽略 event 行)。 */
|
|
414
|
+
async function* iterateOwnSse(body) {
|
|
415
|
+
const reader = body.getReader();
|
|
416
|
+
const decoder = new TextDecoder();
|
|
417
|
+
let buffer = "";
|
|
418
|
+
try {
|
|
419
|
+
while (true) {
|
|
420
|
+
const { done, value } = await reader.read();
|
|
421
|
+
if (done)
|
|
422
|
+
break;
|
|
423
|
+
buffer += decoder.decode(value, { stream: true });
|
|
424
|
+
let boundary = buffer.indexOf("\n\n");
|
|
425
|
+
while (boundary !== -1) {
|
|
426
|
+
const frame = buffer.slice(0, boundary);
|
|
427
|
+
buffer = buffer.slice(boundary + 2);
|
|
428
|
+
const data = frame.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n");
|
|
429
|
+
if (!data || data === "[DONE]")
|
|
430
|
+
continue;
|
|
431
|
+
try {
|
|
432
|
+
yield JSON.parse(data);
|
|
433
|
+
}
|
|
434
|
+
catch { /* 自产流不应出现坏帧;忽略。 */ }
|
|
435
|
+
boundary = buffer.indexOf("\n\n");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
try {
|
|
441
|
+
reader.releaseLock();
|
|
442
|
+
}
|
|
443
|
+
catch { /* 读取已由取消收尾。 */ }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
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
|
+
function sseHeaders() {
|
|
453
|
+
return new Headers({ "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" });
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* 四种客户端协议统一入口:responses 源沿用既有 Responses 输出;chat-completions /
|
|
457
|
+
* anthropic / gemini 源由 AGY 自产 Responses SSE 独立渲染为对应协议外壳(零桥接依赖)。
|
|
458
|
+
*/
|
|
459
|
+
export async function createAgyClientResponse(to, upstream, options) {
|
|
460
|
+
if (to === "responses")
|
|
461
|
+
return createAgyResponse(upstream, options);
|
|
462
|
+
// 非 responses 客户端始终先取 Responses SSE 流(非流式客户端由渲染器聚合),
|
|
463
|
+
// 否则非流式时 createAgyResponse 直接返回聚合 JSON,渲染层无帧可解。
|
|
464
|
+
const responsesOutput = await createAgyResponse(upstream, { ...options, stream: true });
|
|
465
|
+
if (!responsesOutput.body)
|
|
466
|
+
throw new Error("Antigravity 响应缺少 body");
|
|
467
|
+
const state = { model: options.model, stream: options.stream, onChunk: options.onChunk };
|
|
468
|
+
if (to === "chat-completions")
|
|
469
|
+
return renderChatClient(responsesOutput.body, state);
|
|
470
|
+
if (to === "gemini")
|
|
471
|
+
return renderGeminiClient(responsesOutput.body, state);
|
|
472
|
+
return renderAnthropicClient(responsesOutput.body, state);
|
|
473
|
+
}
|
|
96
474
|
export async function createAgyResponse(upstream, options) {
|
|
97
475
|
const contentType = upstream.headers.get("content-type") ?? "";
|
|
98
476
|
if (!contentType.includes("text/event-stream")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-proxy/agy",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Agent Proxy agy 插件;保留基线转换行为。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"prepack": "bun ../../scripts/verify-package.ts ."
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
|
-
"access": "public"
|
|
29
|
+
"access": "public",
|
|
30
|
+
"timeout": 30000
|
|
30
31
|
},
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"repository": {
|
|
@@ -34,10 +35,10 @@
|
|
|
34
35
|
"url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@agent-proxy/models": "0.12.
|
|
38
|
+
"@agent-proxy/models": "0.12.2"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@agent-proxy/plugin-api": "0.12.
|
|
41
|
+
"@agent-proxy/plugin-api": "0.12.2"
|
|
41
42
|
},
|
|
42
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "3461eb517c21cd330f2c3495f6bc01ca4191690c"
|
|
43
44
|
}
|