@agent-proxy/agy 0.0.0-stage → 0.11.0
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/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 +40 -4
- package/README.md +0 -3
package/dist/request.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AGY_DEFAULT_PROJECT } from "./transport.js";
|
|
3
|
+
/**
|
|
4
|
+
* Codex Responses → Antigravity v1internal generateContent 请求转换。
|
|
5
|
+
*
|
|
6
|
+
* 外层是 v1internal 包装(project/request_id/user_prompt_id/user_agent/request),
|
|
7
|
+
* 内层是 Gemini/Vertex 风格的 contents + systemInstruction + tools + generationConfig。
|
|
8
|
+
* 按现有适配器的严格风格手写全量转换:不支持的字段显式报 400,不支持的服务器内置
|
|
9
|
+
* 工具降级为系统旁白;思考文本历史不回放(thought 属于模型私有态),但响应侧经
|
|
10
|
+
* agn1: 信封下发的 thoughtSignature 会挂回下一个函数调用(上游强制要求)。
|
|
11
|
+
*/
|
|
12
|
+
export class AgyRequestError extends Error {
|
|
13
|
+
constructor(message) { super(message); this.name = "AgyRequestError"; }
|
|
14
|
+
}
|
|
15
|
+
function fail(message) { throw new AgyRequestError(message); }
|
|
16
|
+
function record(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function string(value, field) {
|
|
20
|
+
if (typeof value !== "string")
|
|
21
|
+
fail(`${field} 必须是字符串`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function positiveInteger(value, field) {
|
|
25
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
26
|
+
fail(`${field} 必须是正整数`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function safeToolName(name) {
|
|
30
|
+
if (/^[A-Za-z0-9_-]{1,64}$/.test(name))
|
|
31
|
+
return name;
|
|
32
|
+
const base = name.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+/, "tool_") || "tool";
|
|
33
|
+
return `${base.slice(0, 48)}_${createHash("sha256").update(name).digest("hex").slice(0, 12)}`;
|
|
34
|
+
}
|
|
35
|
+
function textPart(value, field) {
|
|
36
|
+
return { text: string(value.text, `${field}.text`) };
|
|
37
|
+
}
|
|
38
|
+
function systemText(value, field) {
|
|
39
|
+
if (typeof value === "string")
|
|
40
|
+
return value;
|
|
41
|
+
if (!Array.isArray(value))
|
|
42
|
+
fail(`${field} 必须是字符串或内容数组`);
|
|
43
|
+
const parts = [];
|
|
44
|
+
for (const raw of value) {
|
|
45
|
+
if (!record(raw))
|
|
46
|
+
fail(`${field} 包含无效内容块`);
|
|
47
|
+
if (raw.type === "input_text" || raw.type === "output_text" || raw.type === "text") {
|
|
48
|
+
parts.push(string(raw.text, `${field}.text`));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
fail(`${field} 的 system 内容不支持图片等非文本块`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parts.join("\n\n");
|
|
55
|
+
}
|
|
56
|
+
function toolIdentity(rawName, rawNamespace) {
|
|
57
|
+
const original = string(rawName, "工具 name");
|
|
58
|
+
if (!original)
|
|
59
|
+
fail("工具 name 不能为空");
|
|
60
|
+
const namespace = rawNamespace === undefined || rawNamespace === null ? undefined : string(rawNamespace, "工具 namespace");
|
|
61
|
+
if (namespace === "")
|
|
62
|
+
fail("工具 namespace 不能为空");
|
|
63
|
+
const name = safeToolName(namespace === undefined ? original : JSON.stringify([namespace, original]));
|
|
64
|
+
return { name, original, ...(namespace === undefined ? {} : { namespace }) };
|
|
65
|
+
}
|
|
66
|
+
const CUSTOM_TOOL_PARAMETERS = {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: { input: { type: "string" } },
|
|
69
|
+
required: ["input"],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
};
|
|
72
|
+
/** 本地 $ref 的指针形态:`#/$defs/name` 或 `#/definitions/name`。 */
|
|
73
|
+
const LOCAL_REF_PATTERN = /^#\/(?:\$defs|definitions)\/(.+)$/;
|
|
74
|
+
/** Gemini Schema 的已知合法键(protojson 严格模式:白名单外的键一律剥除)。
|
|
75
|
+
* additionalProperties/oneOf 有真实 200 实证;其余为 Gemini 公开 Schema 字段。 */
|
|
76
|
+
const GEMINI_SCHEMA_KEYS = new Set([
|
|
77
|
+
"type", "format", "title", "description", "nullable", "default", "items", "maxItems", "minItems",
|
|
78
|
+
"enum", "properties", "required", "minimum", "maximum", "minLength", "maxLength", "pattern",
|
|
79
|
+
"example", "anyOf", "oneOf", "propertyOrdering", "additionalProperties",
|
|
80
|
+
]);
|
|
81
|
+
/** enum/const 的字面量统一转字符串:Gemini 的 enum 是 repeated string。 */
|
|
82
|
+
function literalToString(value) {
|
|
83
|
+
if (typeof value === "string")
|
|
84
|
+
return value;
|
|
85
|
+
if (value !== null && typeof value === "object")
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
return String(value);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* JSON Schema → 上游可接受的形态。上游是 protojson 严格模式,任何未知字段(`$ref`/
|
|
91
|
+
* `const`/`deprecated`/`x-*` 扩展/`exclusiveMinimum` 等 JSON Schema 词汇)都会 400,
|
|
92
|
+
* 因此按 Gemini Schema 白名单重建 schema:本地 `$ref` 内联展开(悬空/外部/循环按
|
|
93
|
+
* `{}` 兜底),联合 `type` 数组拆解为单值 + nullable 或 anyOf,`const` 转 enum,
|
|
94
|
+
* 元组形 `items` 数组转 anyOf,非字符串 enum 字面量转字符串,白名单外的键剥除。
|
|
95
|
+
*/
|
|
96
|
+
function sanitizeToolSchema(input) {
|
|
97
|
+
const root = structuredClone(input);
|
|
98
|
+
const defs = new Map();
|
|
99
|
+
for (const key of ["$defs", "definitions"]) {
|
|
100
|
+
const section = root[key];
|
|
101
|
+
if (record(section)) {
|
|
102
|
+
for (const [name, schema] of Object.entries(section))
|
|
103
|
+
defs.set(name, schema);
|
|
104
|
+
delete root[key];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const walk = (value, stack) => {
|
|
108
|
+
if (Array.isArray(value))
|
|
109
|
+
return value.map((item) => walk(item, stack));
|
|
110
|
+
if (!record(value))
|
|
111
|
+
return value;
|
|
112
|
+
if (typeof value.$ref === "string") {
|
|
113
|
+
const match = LOCAL_REF_PATTERN.exec(value.$ref);
|
|
114
|
+
const name = match?.[1];
|
|
115
|
+
if (!name || !defs.has(name) || stack.has(name))
|
|
116
|
+
return {};
|
|
117
|
+
stack.add(name);
|
|
118
|
+
const inlined = walk(structuredClone(defs.get(name)), stack);
|
|
119
|
+
stack.delete(name);
|
|
120
|
+
if (!record(inlined))
|
|
121
|
+
return {};
|
|
122
|
+
const merged = { ...value };
|
|
123
|
+
delete merged.$ref;
|
|
124
|
+
return { ...inlined, ...walk(merged, stack) };
|
|
125
|
+
}
|
|
126
|
+
const out = {};
|
|
127
|
+
// JSON Schema 联合类型数组(如 ["string","null"]):拆成单值 type + nullable,
|
|
128
|
+
// 多个非空类型转 anyOf——protojson 的 type 是单值枚举,数组直接 400。
|
|
129
|
+
const typeList = Array.isArray(value.type) ? value.type : undefined;
|
|
130
|
+
if (typeList) {
|
|
131
|
+
const nonNull = typeList.filter((item) => typeof item === "string" && item !== "null" && item !== "");
|
|
132
|
+
if (nonNull.length === 1)
|
|
133
|
+
out.type = nonNull[0];
|
|
134
|
+
else if (nonNull.length > 1 && value.anyOf === undefined) {
|
|
135
|
+
out.anyOf = nonNull.map((type) => ({ type }));
|
|
136
|
+
}
|
|
137
|
+
if (typeList.includes("null"))
|
|
138
|
+
out.nullable = true;
|
|
139
|
+
}
|
|
140
|
+
// const 单字面量约束转 enum(白名单里没有 const)。
|
|
141
|
+
if (value.const !== undefined && value.enum === undefined)
|
|
142
|
+
out.enum = [literalToString(value.const)];
|
|
143
|
+
// 元组形 items 数组(draft-07 位置校验)无法表达,退化为 anyOf 并集。
|
|
144
|
+
const itemsList = Array.isArray(value.items) ? value.items : undefined;
|
|
145
|
+
if (itemsList)
|
|
146
|
+
out.items = { anyOf: itemsList.map((item) => walk(item, stack)) };
|
|
147
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
148
|
+
if (key === "type" && typeList !== undefined)
|
|
149
|
+
continue;
|
|
150
|
+
if (key === "items" && itemsList !== undefined)
|
|
151
|
+
continue;
|
|
152
|
+
if (key === "const")
|
|
153
|
+
continue;
|
|
154
|
+
if (!GEMINI_SCHEMA_KEYS.has(key))
|
|
155
|
+
continue;
|
|
156
|
+
// properties 是 map<string, Schema>:属性名是 map 键不是 schema 键,值按普通 schema 走。
|
|
157
|
+
if (key === "properties") {
|
|
158
|
+
out.properties = record(entry)
|
|
159
|
+
? Object.fromEntries(Object.entries(entry).map(([name, schema]) => [name, walk(schema, stack)]))
|
|
160
|
+
: {};
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// enum 是 repeated string:布尔/数字等非字符串字面量统一转字符串(true -> "true")。
|
|
164
|
+
if (key === "enum" && Array.isArray(entry)) {
|
|
165
|
+
out.enum = entry.map(literalToString);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
out[key] = walk(entry, stack);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
return walk(root, new Set());
|
|
173
|
+
}
|
|
174
|
+
/** Gemini 的 functionResponse.response 必须是对象:对象原样,标量包成 { output }。 */
|
|
175
|
+
function functionResponsePayload(value) {
|
|
176
|
+
if (record(value))
|
|
177
|
+
return value;
|
|
178
|
+
if (typeof value === "string") {
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(value);
|
|
181
|
+
if (record(parsed))
|
|
182
|
+
return parsed;
|
|
183
|
+
}
|
|
184
|
+
catch { /* 非 JSON 文本按原文包装。 */ }
|
|
185
|
+
return value ? { output: value } : {};
|
|
186
|
+
}
|
|
187
|
+
return { output: JSON.stringify(value ?? null) };
|
|
188
|
+
}
|
|
189
|
+
function toolResultParts(value, field) {
|
|
190
|
+
if (typeof value === "string")
|
|
191
|
+
return { text: value, hasContent: value.length > 0 };
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
const texts = [];
|
|
194
|
+
for (const part of value) {
|
|
195
|
+
if (!record(part))
|
|
196
|
+
fail(`${field} 包含无效内容块`);
|
|
197
|
+
if (part.type === "input_text" || part.type === "output_text" || part.type === "text") {
|
|
198
|
+
const text = string(part.text, `${field} 文本`);
|
|
199
|
+
if (text)
|
|
200
|
+
texts.push(text);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Gemini 的 functionResponse 放不了多模态:图片等结果块降级为占位说明。
|
|
204
|
+
texts.push(`[工具输出包含不支持的内容块 ${String(part.type)},已省略]`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { text: texts.join(""), hasContent: texts.length > 0 };
|
|
208
|
+
}
|
|
209
|
+
return { text: "", hasContent: false };
|
|
210
|
+
}
|
|
211
|
+
function serverCallSummary(item) {
|
|
212
|
+
const action = record(item.action) ? item.action : {};
|
|
213
|
+
const query = [action.query, action.url].find((field) => typeof field === "string" && field.length > 0);
|
|
214
|
+
const detail = query ? `(查询:${query})` : "";
|
|
215
|
+
return `[已省略服务器内置工具调用 ${String(item.type)}${detail};本通道不支持该工具]`;
|
|
216
|
+
}
|
|
217
|
+
/** reasoning.effort → Gemini thinkingLevel;unknown 档位映射到最近的低/高档。
|
|
218
|
+
* 档位名同时是多档位家族的上游变体后缀,catalog.ts 复用该映射做 effort → 变体解析。 */
|
|
219
|
+
export const THINKING_LEVELS = {
|
|
220
|
+
minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "high", max: "high",
|
|
221
|
+
};
|
|
222
|
+
function translateTools(value) {
|
|
223
|
+
const tools = [];
|
|
224
|
+
const map = new Map();
|
|
225
|
+
const dropped = new Set();
|
|
226
|
+
function visit(entries, namespace, description) {
|
|
227
|
+
if (!Array.isArray(entries))
|
|
228
|
+
fail("tools 必须是数组");
|
|
229
|
+
for (const raw of entries) {
|
|
230
|
+
if (!record(raw))
|
|
231
|
+
fail("tools 包含无效声明");
|
|
232
|
+
if (raw.type === "namespace") {
|
|
233
|
+
if (namespace !== undefined)
|
|
234
|
+
fail("namespace 内仅支持 function 或 custom 工具");
|
|
235
|
+
const segment = string(raw.name, "namespace.name");
|
|
236
|
+
if (!segment)
|
|
237
|
+
fail("namespace.name 不能为空");
|
|
238
|
+
visit(raw.tools, segment, typeof raw.description === "string" ? raw.description : undefined);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (raw.type !== "function" && raw.type !== "custom") {
|
|
242
|
+
dropped.add(typeof raw.type === "string" ? raw.type : "unknown");
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// namespace 既可能是 type=namespace 的包裹项,也可能是声明上的扁平字段
|
|
246
|
+
// (与 history 调用项的 namespace 字段对应);两者取其一,避免映射名不一致。
|
|
247
|
+
const flat = namespace === undefined && typeof raw.namespace === "string" && raw.namespace
|
|
248
|
+
? raw.namespace : undefined;
|
|
249
|
+
const identity = toolIdentity(raw.name, namespace ?? flat);
|
|
250
|
+
if (map.has(identity.name))
|
|
251
|
+
fail(`工具名映射冲突:${identity.original}`);
|
|
252
|
+
const custom = raw.type === "custom";
|
|
253
|
+
const schema = record(raw.parameters) ? raw.parameters : undefined;
|
|
254
|
+
if (!custom && schema === undefined)
|
|
255
|
+
fail(`function 工具 ${identity.original} 缺少对象 parameters`);
|
|
256
|
+
const details = [description, typeof raw.description === "string" ? raw.description : undefined]
|
|
257
|
+
.filter((item) => item !== undefined);
|
|
258
|
+
tools.push({
|
|
259
|
+
name: identity.name,
|
|
260
|
+
...(details.length ? { description: details.join("\n\n") } : {}),
|
|
261
|
+
// parameters 原样透传会带 $ref/$defs 等 JSON Schema 元字段,上游 protojson 直接 400。
|
|
262
|
+
parameters: custom ? CUSTOM_TOOL_PARAMETERS : sanitizeToolSchema(schema),
|
|
263
|
+
});
|
|
264
|
+
map.set(identity.name, { name: identity.original, custom, ...((namespace ?? flat) === undefined ? {} : { namespace: (namespace ?? flat) }) });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (value !== undefined)
|
|
268
|
+
visit(value);
|
|
269
|
+
return { tools, map, dropped };
|
|
270
|
+
}
|
|
271
|
+
export function translateAgyRequest(body, upstreamModel) {
|
|
272
|
+
if (typeof upstreamModel !== "string" || !upstreamModel)
|
|
273
|
+
fail("上游模型不能为空");
|
|
274
|
+
if (body.previous_response_id !== undefined && body.previous_response_id !== null && body.previous_response_id !== "") {
|
|
275
|
+
fail("Antigravity 不支持 previous_response_id;请提供完整 input 历史");
|
|
276
|
+
}
|
|
277
|
+
if (body.conversation !== undefined && body.conversation !== null && body.conversation !== "") {
|
|
278
|
+
fail("Antigravity 不支持 conversation;请提供完整 input 历史");
|
|
279
|
+
}
|
|
280
|
+
if (body.background === true)
|
|
281
|
+
fail("Antigravity 不支持 background 请求");
|
|
282
|
+
for (const format of [body.response_format, record(body.text) ? body.text.format : undefined]) {
|
|
283
|
+
if (format !== undefined && (!record(format) || format.type !== "text"))
|
|
284
|
+
fail("Antigravity 不支持 Responses 的结构化输出格式");
|
|
285
|
+
}
|
|
286
|
+
if (body.instructions !== undefined && typeof body.instructions !== "string")
|
|
287
|
+
fail("instructions 必须是字符串");
|
|
288
|
+
if (body.parallel_tool_calls !== undefined && typeof body.parallel_tool_calls !== "boolean")
|
|
289
|
+
fail("parallel_tool_calls 必须是布尔值");
|
|
290
|
+
const maxTokens = body.max_output_tokens === undefined ? 16384 : positiveInteger(body.max_output_tokens, "max_output_tokens");
|
|
291
|
+
const convertedTools = translateTools(body.tools);
|
|
292
|
+
const contents = [];
|
|
293
|
+
const system = [];
|
|
294
|
+
const callNames = new Map();
|
|
295
|
+
const results = new Set();
|
|
296
|
+
// 回放历史时待挂回的 thought 签名(来自响应侧 agn1: 信封的 reasoning 项)。
|
|
297
|
+
let pendingSignature;
|
|
298
|
+
let pendingAssistant;
|
|
299
|
+
const assistantContent = () => {
|
|
300
|
+
if (pendingAssistant && contents.at(-1) === pendingAssistant)
|
|
301
|
+
return pendingAssistant;
|
|
302
|
+
pendingAssistant = { role: "model", parts: [] };
|
|
303
|
+
contents.push(pendingAssistant);
|
|
304
|
+
return pendingAssistant;
|
|
305
|
+
};
|
|
306
|
+
const userContent = () => {
|
|
307
|
+
pendingAssistant = undefined;
|
|
308
|
+
const content = { role: "user", parts: [] };
|
|
309
|
+
contents.push(content);
|
|
310
|
+
return content;
|
|
311
|
+
};
|
|
312
|
+
const input = body.input;
|
|
313
|
+
if (typeof input === "string") {
|
|
314
|
+
userContent().parts.push({ text: input });
|
|
315
|
+
}
|
|
316
|
+
else if (Array.isArray(input)) {
|
|
317
|
+
for (const item of input) {
|
|
318
|
+
if (!record(item))
|
|
319
|
+
fail("input 包含非对象 history 项");
|
|
320
|
+
if (item.type === "reasoning") {
|
|
321
|
+
// thought signature 无法跨请求回放;但响应侧经 agn1: 信封下发的 thoughtSignature
|
|
322
|
+
// 必须挂回下一个函数调用(上游强制要求,缺失直接 400)。官方模型的加密内容不带
|
|
323
|
+
// 该前缀,忽略。
|
|
324
|
+
const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
|
|
325
|
+
if (encrypted !== undefined && encrypted.startsWith("agn1:"))
|
|
326
|
+
pendingSignature = encrypted.slice(5);
|
|
327
|
+
pendingAssistant = undefined;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (item.type === "message" || item.type === undefined) {
|
|
331
|
+
if (item.role === "system" || item.role === "developer") {
|
|
332
|
+
pendingAssistant = undefined;
|
|
333
|
+
system.push(systemText(item.content, `${item.role} 消息`));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (item.role !== "user" && item.role !== "assistant")
|
|
337
|
+
fail(`不支持的消息 role ${String(item.role)}`);
|
|
338
|
+
const parts = [];
|
|
339
|
+
const content = item.content;
|
|
340
|
+
if (typeof content === "string")
|
|
341
|
+
parts.push({ text: content });
|
|
342
|
+
else if (Array.isArray(content)) {
|
|
343
|
+
for (const raw of content) {
|
|
344
|
+
if (!record(raw))
|
|
345
|
+
fail(`${item.role} 消息包含无效内容块`);
|
|
346
|
+
if (raw.type === "input_text" || raw.type === "output_text" || raw.type === "text")
|
|
347
|
+
parts.push(textPart(raw, "消息"));
|
|
348
|
+
else if (raw.type === "input_image" || raw.type === "image") {
|
|
349
|
+
const source = raw.image_url ?? raw.url;
|
|
350
|
+
const url = typeof source === "string" ? source : record(source) ? source.url : undefined;
|
|
351
|
+
if (typeof url !== "string" || !url)
|
|
352
|
+
fail("input_image 缺少 image_url");
|
|
353
|
+
// data: URI 走 inlineData(base64 内联),其余 URL 走 fileData.fileUri;
|
|
354
|
+
// master.Part 没有 uri 字段,protojson 会拒绝未知字段。
|
|
355
|
+
if (url.startsWith("data:")) {
|
|
356
|
+
const match = /^data:([^;,]*)(?:;charset=[^;,]*)?;base64,(.*)$/s.exec(url);
|
|
357
|
+
if (!match)
|
|
358
|
+
fail("input_image 的 data URI 必须是 base64 编码");
|
|
359
|
+
parts.push({ inlineData: { mimeType: match[1] || "image/png", data: match[2] } });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
parts.push({ fileData: { fileUri: url } });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else
|
|
366
|
+
fail(`${item.role} 消息包含不支持的内容块类型 ${String(raw.type)}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else
|
|
370
|
+
fail(`${item.role} 消息的 content 必须是字符串或数组`);
|
|
371
|
+
(item.role === "assistant" ? assistantContent() : userContent()).parts.push(...parts);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
375
|
+
const identity = toolIdentity(item.name, item.namespace);
|
|
376
|
+
const custom = item.type === "custom_tool_call";
|
|
377
|
+
const callId = string(item.call_id ?? item.id, `${item.type}.call_id`);
|
|
378
|
+
if (callNames.has(callId))
|
|
379
|
+
fail(`history 中重复的工具调用 ID ${callId}`);
|
|
380
|
+
callNames.set(callId, { mapped: identity.name, custom });
|
|
381
|
+
const rawArguments = custom ? item.input : item.arguments;
|
|
382
|
+
let args;
|
|
383
|
+
if (custom)
|
|
384
|
+
args = { input: string(rawArguments, "custom_tool_call.input") };
|
|
385
|
+
else if (record(rawArguments))
|
|
386
|
+
args = rawArguments;
|
|
387
|
+
else if (typeof rawArguments === "string") {
|
|
388
|
+
try {
|
|
389
|
+
const parsed = JSON.parse(rawArguments);
|
|
390
|
+
if (!record(parsed))
|
|
391
|
+
fail("function_call 的 arguments 必须解码为 JSON 对象");
|
|
392
|
+
args = parsed;
|
|
393
|
+
}
|
|
394
|
+
catch (cause) {
|
|
395
|
+
if (cause instanceof AgyRequestError)
|
|
396
|
+
throw cause;
|
|
397
|
+
fail("function_call 的 arguments 不是有效 JSON 对象");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else
|
|
401
|
+
fail("function_call 的 arguments 必须是 JSON 对象或 JSON 字符串");
|
|
402
|
+
assistantContent().parts.push({
|
|
403
|
+
functionCall: { name: identity.name, args },
|
|
404
|
+
// thoughtSignature 是 Part 级字段(与 functionCall 平级),上游按此校验回放。
|
|
405
|
+
...(pendingSignature !== undefined ? { thoughtSignature: pendingSignature } : {}),
|
|
406
|
+
});
|
|
407
|
+
pendingSignature = undefined;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
411
|
+
const callId = string(item.call_id, `${item.type}.call_id`);
|
|
412
|
+
const call = callNames.get(callId);
|
|
413
|
+
if (!call || (item.type === "function_call_output") === call.custom) {
|
|
414
|
+
fail(`${item.type} 未关联到同一 input 中匹配的工具调用 ${callId}`);
|
|
415
|
+
}
|
|
416
|
+
if (results.has(callId))
|
|
417
|
+
fail(`history 中重复的工具输出 ID ${callId}`);
|
|
418
|
+
results.add(callId);
|
|
419
|
+
const raw = item.output ?? item.content;
|
|
420
|
+
// Gemini 的 functionResponse.response 必须是对象:对象原样、数组取文本部分、
|
|
421
|
+
// 字符串尝试按 JSON 对象解码,其余形态统一包成 { output }。
|
|
422
|
+
const response = record(raw) ? raw
|
|
423
|
+
: Array.isArray(raw) ? { output: toolResultParts(raw, `${item.type}`).text }
|
|
424
|
+
: functionResponsePayload(raw);
|
|
425
|
+
userContent().parts.push({ functionResponse: { name: call.mapped, response } });
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (typeof item.type === "string" && (item.type.endsWith("_call") || item.type === "mcp_list_tools" || item.type === "mcp_list_resources")) {
|
|
429
|
+
// 其余服务器执行的历史调用无法在上游执行,降级为文本摘要。
|
|
430
|
+
assistantContent().parts.push({ text: serverCallSummary(item) });
|
|
431
|
+
pendingAssistant = undefined;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
fail(`不支持的 Responses history 项类型 ${typeof item.type === "string" ? item.type : "unknown"}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
fail("input 必须是字符串或数组");
|
|
439
|
+
}
|
|
440
|
+
if (convertedTools.dropped.size) {
|
|
441
|
+
system.push(`本通道无法执行以下服务器内置工具,已从本请求移除:${[...convertedTools.dropped].join("、")}。请勿声称已使用这些工具。`);
|
|
442
|
+
}
|
|
443
|
+
// thinkingConfig 按 CLI 真实形态发送:includeThoughts 使 thought 部件随流返回,
|
|
444
|
+
// thinkingBudget=-1 交由服务端按模型档位决定;档位用 thinkingLevel 表达。
|
|
445
|
+
const thinkingConfig = { includeThoughts: true, thinkingBudget: -1 };
|
|
446
|
+
const reasoning = record(body.reasoning) ? body.reasoning : undefined;
|
|
447
|
+
const effort = reasoning?.effort;
|
|
448
|
+
if (typeof effort === "string" && THINKING_LEVELS[effort]) {
|
|
449
|
+
thinkingConfig.thinkingLevel = THINKING_LEVELS[effort];
|
|
450
|
+
}
|
|
451
|
+
const generationConfig = { maxOutputTokens: maxTokens, thinkingConfig };
|
|
452
|
+
if (body.temperature !== undefined) {
|
|
453
|
+
if (typeof body.temperature !== "number" || !Number.isFinite(body.temperature))
|
|
454
|
+
fail("temperature 必须是数值");
|
|
455
|
+
generationConfig.temperature = body.temperature;
|
|
456
|
+
}
|
|
457
|
+
if (body.top_p !== undefined) {
|
|
458
|
+
if (typeof body.top_p !== "number" || !Number.isFinite(body.top_p))
|
|
459
|
+
fail("top_p 必须是数值");
|
|
460
|
+
generationConfig.topP = body.top_p;
|
|
461
|
+
}
|
|
462
|
+
const inner = {
|
|
463
|
+
contents,
|
|
464
|
+
...(system.length || body.instructions !== undefined ? {
|
|
465
|
+
systemInstruction: { role: "user", parts: [...(body.instructions !== undefined ? [{ text: body.instructions }] : []), ...system.map((text) => ({ text }))] },
|
|
466
|
+
} : {}),
|
|
467
|
+
...(convertedTools.tools.length ? { tools: [{ functionDeclarations: convertedTools.tools }] } : {}),
|
|
468
|
+
generationConfig,
|
|
469
|
+
};
|
|
470
|
+
if (body.tool_choice !== undefined) {
|
|
471
|
+
const choice = body.tool_choice;
|
|
472
|
+
if (choice === "auto")
|
|
473
|
+
inner.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
|
|
474
|
+
else if (choice === "none")
|
|
475
|
+
inner.toolConfig = { functionCallingConfig: { mode: "NONE" } };
|
|
476
|
+
else if (choice === "required")
|
|
477
|
+
inner.toolConfig = { functionCallingConfig: { mode: "ANY" } };
|
|
478
|
+
else if (record(choice) && (choice.type === "function" || choice.type === "custom")) {
|
|
479
|
+
const identity = toolIdentity(choice.name, choice.namespace);
|
|
480
|
+
if (!convertedTools.map.has(identity.name))
|
|
481
|
+
fail(`工具选择引用了未声明的工具 ${identity.original}`);
|
|
482
|
+
inner.toolConfig = { functionCallingConfig: { mode: "ANY", allowedFunctionNames: [identity.name] } };
|
|
483
|
+
}
|
|
484
|
+
else if (record(choice) && typeof choice.type === "string" && choice.type !== "") {
|
|
485
|
+
// 指向服务器内置工具的选择无法跨格式表达,省略让上游回到默认 auto。
|
|
486
|
+
}
|
|
487
|
+
else
|
|
488
|
+
fail("不支持的 tool_choice");
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
wrapper: {
|
|
492
|
+
// 外层字段名与归属按 MITM 抓包的真实形状:model 在外层、protojson camelCase、
|
|
493
|
+
// sessionId 为随机 int64 字符串;userAgent 固定 "antigravity"。
|
|
494
|
+
project: AGY_DEFAULT_PROJECT,
|
|
495
|
+
requestId: `checkpoint/${crypto.randomUUID()}`,
|
|
496
|
+
model: upstreamModel,
|
|
497
|
+
userAgent: "antigravity",
|
|
498
|
+
requestType: "checkpoint",
|
|
499
|
+
request: { ...inner, sessionId: `${-Math.floor(Math.random() * 9e18)}` },
|
|
500
|
+
},
|
|
501
|
+
tools: convertedTools.map,
|
|
502
|
+
dropped: [...convertedTools.dropped],
|
|
503
|
+
};
|
|
504
|
+
}
|