mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 03:56:45 +08:00
704 lines
21 KiB
TypeScript
704 lines
21 KiB
TypeScript
import type {
|
|
LlmContentPart,
|
|
LlmMessage,
|
|
LlmRequest,
|
|
ProviderInvokeInput,
|
|
ProviderOutput,
|
|
ProviderResult,
|
|
ProviderSupport,
|
|
} from "cursor-byok:provider";
|
|
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
|
|
import { HttpError } from "cursor-byok:protocol/openai-chat";
|
|
import {
|
|
ANTIGRAVITY_CLIENT_HEADERS,
|
|
ANTIGRAVITY_ENDPOINTS,
|
|
ANTIGRAVITY_USER_AGENT,
|
|
antigravityModels,
|
|
} from "./models.ts";
|
|
import {
|
|
type AccountData,
|
|
accountData,
|
|
isTokenExpired,
|
|
quotaExhaustedPatch,
|
|
refreshAccount,
|
|
RESOURCE_TYPE,
|
|
} from "./resources.ts";
|
|
|
|
export function isQuotaError(error: string): boolean {
|
|
const message = error.toLowerCase();
|
|
return message.includes("resource_exhausted") ||
|
|
message.includes("quota_exceeded") ||
|
|
message.includes("quota_exhausted") ||
|
|
message.includes("rate_limit_exceeded") ||
|
|
message.includes("rate limit") ||
|
|
message.includes("model_capacity_exhausted") ||
|
|
message.includes("too many requests") ||
|
|
message.includes("429");
|
|
}
|
|
|
|
function isQuotaHttpError(error: HttpError): boolean {
|
|
if (error.status === 429) return true;
|
|
const body = error.body.toLowerCase();
|
|
return body.includes("resource_exhausted") ||
|
|
body.includes("quota_exceeded") ||
|
|
body.includes("quota_exhausted") ||
|
|
body.includes("rate_limit_exceeded") ||
|
|
body.includes("rate limit") ||
|
|
body.includes("model_capacity_exhausted") ||
|
|
body.includes("user rate limit exceeded") ||
|
|
body.includes("too many requests");
|
|
}
|
|
|
|
function invalidResult(message: string, stateMessage: string): ProviderResult {
|
|
return {
|
|
status: "resource-error",
|
|
message,
|
|
patch: { state: { status: "invalid", message: stateMessage } },
|
|
};
|
|
}
|
|
|
|
async function readBody(lines: AsyncIterable<string>): Promise<string> {
|
|
const collected: string[] = [];
|
|
for await (const line of lines) collected.push(line);
|
|
return collected.join("\n");
|
|
}
|
|
|
|
function resolveAntigravityModel(modelId: string): string {
|
|
const raw = modelId.trim();
|
|
const lower = raw.toLowerCase();
|
|
|
|
// 1. If explicit tier is already specified in the model ID, pass it directly!
|
|
if (
|
|
lower.startsWith("gemini-3.7-flash-") ||
|
|
lower.startsWith("gemini-3.6-flash-") ||
|
|
lower.startsWith("gemini-3.1-pro-") ||
|
|
lower === "gemini-3.7-flash" ||
|
|
lower === "gemini-3.6-flash" ||
|
|
lower === "gemini-2.5-flash" ||
|
|
lower === "gemini-2.5-pro" ||
|
|
lower === "gemini-2.0-flash" ||
|
|
lower === "claude-sonnet-4-6" ||
|
|
lower === "claude-sonnet-4-6-thinking" ||
|
|
lower === "claude-opus-4-6-thinking" ||
|
|
lower === "gemini-3.1-flash-image" ||
|
|
lower === "gpt-oss-120b-medium"
|
|
) {
|
|
if (lower === "gemini-3.1-pro-high") return "gemini-pro-agent";
|
|
return raw;
|
|
}
|
|
|
|
// 2. Canonical Antigravity-Manager mapping for aliases
|
|
if (
|
|
lower === "claude-3-7-sonnet" || lower === "claude-3-5-sonnet" || lower === "claude-sonnet-4-5"
|
|
) {
|
|
return "claude-sonnet-4-6";
|
|
}
|
|
if (lower === "claude-3-5-haiku" || lower === "claude-haiku-4") {
|
|
return "claude-sonnet-4-6";
|
|
}
|
|
if (
|
|
lower === "claude-3-7-opus" || lower === "claude-opus-4" || lower === "claude-opus-4.6" ||
|
|
lower === "claude-opus-4-5-thinking"
|
|
) {
|
|
return "claude-opus-4-6-thinking";
|
|
}
|
|
if (
|
|
lower === "gpt-4" || lower === "gpt-4o" || lower === "gpt-4o-mini" || lower === "gpt-3.5-turbo"
|
|
) {
|
|
return "gemini-2.5-flash";
|
|
}
|
|
if (lower === "gemini-2.5-flash-lite") {
|
|
return "gemini-2.5-flash";
|
|
}
|
|
if (lower === "gemini-3-flash" || lower === "gemini-3.5-flash") {
|
|
return "gemini-3.7-flash";
|
|
}
|
|
if (lower === "gemini-3-pro" || lower === "gemini-3.1-pro") {
|
|
return "gemini-3.1-pro-preview";
|
|
}
|
|
if (lower === "gemini-3-pro-high") {
|
|
return "gemini-pro-agent";
|
|
}
|
|
|
|
return raw;
|
|
}
|
|
|
|
function randomHex(length = 8): string {
|
|
const array = new Uint8Array(Math.ceil(length / 2));
|
|
crypto.getRandomValues(array);
|
|
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
}
|
|
|
|
function generateRequestId(): string {
|
|
return `agent/${Date.now()}/${randomHex(8)}`;
|
|
}
|
|
|
|
// Keys the CloudCode v1internal Schema proto rejects with "Cannot find field"
|
|
const UNSUPPORTED_SCHEMA_KEYS: Record<string, true> = {
|
|
"$schema": true,
|
|
"$ref": true,
|
|
"$defs": true,
|
|
"$comment": true,
|
|
"examples": true,
|
|
"unevaluatedProperties": true,
|
|
"unevaluatedItems": true,
|
|
"patternProperties": true,
|
|
"propertyNames": true,
|
|
"exclusiveMinimum": true,
|
|
"exclusiveMaximum": true,
|
|
"multipleOf": true,
|
|
"dependencies": true,
|
|
"dependentSchemas": true,
|
|
"dependentRequired": true,
|
|
"deprecated": true,
|
|
"readOnly": true,
|
|
"writeOnly": true,
|
|
"x-mcp-header": true,
|
|
"const": true,
|
|
"default": true,
|
|
"additionalProperties": true,
|
|
"title": true,
|
|
"format": true,
|
|
};
|
|
|
|
const PROTO_TYPE_MAP: Record<string, string> = {
|
|
string: "STRING",
|
|
number: "NUMBER",
|
|
integer: "INTEGER",
|
|
boolean: "BOOLEAN",
|
|
array: "ARRAY",
|
|
object: "OBJECT",
|
|
};
|
|
|
|
function enforceUppercaseTypes(value: unknown): unknown {
|
|
if (Array.isArray(value)) return value.map(enforceUppercaseTypes);
|
|
if (value === null || typeof value !== "object") return value;
|
|
const out: Record<string, unknown> = {};
|
|
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
if (UNSUPPORTED_SCHEMA_KEYS[key]) continue;
|
|
if (key === "type" && typeof child === "string") {
|
|
out[key] = PROTO_TYPE_MAP[child.toLowerCase()] ?? child.toUpperCase();
|
|
} else {
|
|
out[key] = enforceUppercaseTypes(child);
|
|
}
|
|
}
|
|
if (!out.type && out.properties) {
|
|
out.type = "OBJECT";
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function sanitizeSchema(value: unknown): unknown {
|
|
if (!value || typeof value !== "object") {
|
|
return { type: "OBJECT", properties: {} };
|
|
}
|
|
const clean = enforceUppercaseTypes(value) as Record<string, unknown>;
|
|
if (!clean.type) clean.type = "OBJECT";
|
|
return clean;
|
|
}
|
|
|
|
function convertToCloudCodeContents(
|
|
instructions: string,
|
|
messages: LlmMessage[],
|
|
): {
|
|
contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
|
|
systemInstruction?: { role: string; parts: Array<{ text: string }> };
|
|
} {
|
|
const rawContents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];
|
|
let systemText = instructions || "";
|
|
|
|
for (const msg of messages) {
|
|
if (msg.role === "system") {
|
|
const txt = msg.content
|
|
.map((p) => (p.type === "text" ? p.text : ""))
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
if (txt) {
|
|
systemText += (systemText ? "\n\n" : "") + txt;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (msg.role === "assistant") {
|
|
const parts: Array<Record<string, unknown>> = [];
|
|
if (msg.text) {
|
|
parts.push({ text: msg.text });
|
|
}
|
|
|
|
const replayVal = msg.replayState?.providerKind === "antigravity"
|
|
? (msg.replayState.value as Record<string, unknown> | null)
|
|
: null;
|
|
const sig = typeof replayVal?.thoughtSignature === "string"
|
|
? replayVal.thoughtSignature
|
|
: null;
|
|
|
|
for (const call of msg.toolCalls) {
|
|
parts.push({
|
|
functionCall: {
|
|
name: call.name,
|
|
args: typeof call.arguments === "object" && call.arguments !== null
|
|
? call.arguments
|
|
: {},
|
|
},
|
|
thoughtSignature: sig || "skip_thought_signature_validator",
|
|
});
|
|
}
|
|
|
|
if (parts.length > 0) {
|
|
rawContents.push({ role: "model", parts });
|
|
}
|
|
} else if (msg.role === "tool") {
|
|
rawContents.push({
|
|
role: "user",
|
|
parts: [
|
|
{
|
|
functionResponse: {
|
|
name: msg.name || "function",
|
|
response: { result: msg.content },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
} else if (msg.role === "user") {
|
|
const parts: Array<Record<string, unknown>> = [];
|
|
for (const p of msg.content) {
|
|
if (p.type === "text") {
|
|
if (p.text) parts.push({ text: p.text });
|
|
} else if (p.type === "image") {
|
|
parts.push({
|
|
inlineData: {
|
|
mimeType: p.mediaType,
|
|
data: p.dataBase64,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
if (parts.length === 0) {
|
|
parts.push({ text: " " });
|
|
}
|
|
rawContents.push({ role: "user", parts });
|
|
}
|
|
}
|
|
|
|
// Merge consecutive same-role messages so contents strictly alternate user -> model -> user -> model
|
|
const contents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];
|
|
for (const item of rawContents) {
|
|
if (item.parts.length === 0) continue;
|
|
const last = contents[contents.length - 1];
|
|
if (last && last.role === item.role) {
|
|
last.parts.push(...item.parts);
|
|
} else {
|
|
contents.push(item);
|
|
}
|
|
}
|
|
|
|
if (contents.length > 0 && contents[0].role !== "user") {
|
|
contents.unshift({ role: "user", parts: [{ text: " " }] });
|
|
}
|
|
|
|
return {
|
|
contents,
|
|
...(systemText.trim()
|
|
? { systemInstruction: { role: "system", parts: [{ text: systemText.trim() }] } }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
async function streamCloudCode(
|
|
accessToken: string,
|
|
projectId: string,
|
|
modelId: string,
|
|
input: ProviderInvokeInput,
|
|
output: ProviderOutput,
|
|
context: PluginContext,
|
|
): Promise<void> {
|
|
const actualModel = resolveAntigravityModel(modelId);
|
|
const { contents, systemInstruction } = convertToCloudCodeContents(
|
|
input.request.instructions,
|
|
input.request.messages,
|
|
);
|
|
|
|
const tools = input.request.tools && input.request.tools.length > 0
|
|
? [
|
|
{
|
|
functionDeclarations: input.request.tools.map((t) => ({
|
|
name: t.name,
|
|
description: t.description || "",
|
|
parameters: sanitizeSchema(t.parameters),
|
|
})),
|
|
},
|
|
]
|
|
: undefined;
|
|
|
|
const toolConfig = tools
|
|
? {
|
|
functionCallingConfig: { mode: "AUTO" },
|
|
}
|
|
: undefined;
|
|
|
|
const payload = {
|
|
project: projectId || "bamboo-precept-lgxtn",
|
|
model: actualModel,
|
|
userAgent: "antigravity",
|
|
requestType: "agent",
|
|
requestId: generateRequestId(),
|
|
enabledCreditTypes: ["GOOGLE_ONE_AI"],
|
|
request: {
|
|
contents,
|
|
...(systemInstruction ? { systemInstruction } : {}),
|
|
...(tools ? { tools } : {}),
|
|
...(toolConfig ? { toolConfig } : {}),
|
|
generationConfig: {
|
|
maxOutputTokens: 65536,
|
|
},
|
|
},
|
|
};
|
|
|
|
const headers: Record<string, string> = {
|
|
authorization: `Bearer ${accessToken}`,
|
|
"content-type": "application/json",
|
|
"user-agent": ANTIGRAVITY_USER_AGENT,
|
|
...ANTIGRAVITY_CLIENT_HEADERS,
|
|
};
|
|
if (actualModel.toLowerCase().includes("claude")) {
|
|
headers["anthropic-beta"] =
|
|
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14";
|
|
}
|
|
|
|
let lastError: Error | null = null;
|
|
let hasEmittedAnyChunk = false;
|
|
|
|
for (const endpoint of ANTIGRAVITY_ENDPOINTS) {
|
|
if (hasEmittedAnyChunk) break;
|
|
|
|
try {
|
|
const response = await context.network.stream(
|
|
`${endpoint}/v1internal:streamGenerateContent?alt=sse`,
|
|
{
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
},
|
|
);
|
|
|
|
if (response.status < 200 || response.status >= 300) {
|
|
const errorBody = await readBody(response.lines);
|
|
lastError = new HttpError(response.status, errorBody);
|
|
if (
|
|
response.status === 503 || response.status === 502 || response.status === 504 ||
|
|
response.status === 404
|
|
) {
|
|
continue;
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
let textStarted = false;
|
|
let thinkingStarted = false;
|
|
let doneEmitted = false;
|
|
let hasTools = false;
|
|
let toolIndex = 0;
|
|
let lastThoughtSignature: string | null = null;
|
|
let finalUsage: {
|
|
inputTokens: number | null;
|
|
outputTokens: number | null;
|
|
totalTokens: number | null;
|
|
} | null = null;
|
|
|
|
for await (const line of response.lines) {
|
|
if (!line.startsWith("data:")) continue;
|
|
const raw = line.slice(5).trim();
|
|
if (!raw || raw === "[DONE]") break;
|
|
|
|
let json: Record<string, unknown>;
|
|
try {
|
|
json = JSON.parse(raw) as Record<string, unknown>;
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
const resp = (json.response as Record<string, unknown> | undefined) ?? json;
|
|
if (!resp) continue;
|
|
|
|
const usage = resp.usageMetadata as Record<string, number> | undefined;
|
|
if (usage) {
|
|
finalUsage = {
|
|
inputTokens: typeof usage.promptTokenCount === "number" ? usage.promptTokenCount : null,
|
|
outputTokens: typeof usage.candidatesTokenCount === "number"
|
|
? usage.candidatesTokenCount
|
|
: null,
|
|
totalTokens: typeof usage.totalTokenCount === "number" ? usage.totalTokenCount : null,
|
|
};
|
|
}
|
|
|
|
const candidates = resp.candidates as Array<Record<string, unknown>> | undefined;
|
|
const candidate = candidates?.[0];
|
|
const content = candidate?.content as Record<string, unknown> | undefined;
|
|
const parts = content?.parts as Array<Record<string, unknown>> | undefined;
|
|
|
|
if (parts) {
|
|
for (const part of parts) {
|
|
const sig = typeof part.thoughtSignature === "string" ? part.thoughtSignature : null;
|
|
if (sig) {
|
|
lastThoughtSignature = sig;
|
|
}
|
|
|
|
const isThought = part.thought === true;
|
|
const textPart = typeof part.text === "string" ? part.text : null;
|
|
|
|
if (isThought && textPart) {
|
|
const cleanThought = textPart.replace(/<\/?think>/gi, "");
|
|
if (cleanThought) {
|
|
hasEmittedAnyChunk = true;
|
|
if (!thinkingStarted) {
|
|
thinkingStarted = true;
|
|
output.emit({ type: "thinking-start" });
|
|
}
|
|
output.emit({ type: "thinking-delta", text: cleanThought });
|
|
}
|
|
} else if (textPart) {
|
|
if (thinkingStarted) {
|
|
thinkingStarted = false;
|
|
output.emit({ type: "thinking-end" });
|
|
}
|
|
const cleanText = textPart.replace(/<\/?think>/gi, "");
|
|
if (cleanText) {
|
|
hasEmittedAnyChunk = true;
|
|
if (!textStarted) {
|
|
textStarted = true;
|
|
output.emit({ type: "text-start" });
|
|
}
|
|
output.emit({ type: "text-delta", text: cleanText });
|
|
}
|
|
}
|
|
|
|
const fnCall = part.functionCall as { name: string; args: unknown } | undefined;
|
|
if (fnCall) {
|
|
hasEmittedAnyChunk = true;
|
|
if (thinkingStarted) {
|
|
thinkingStarted = false;
|
|
output.emit({ type: "thinking-end" });
|
|
}
|
|
if (textStarted) {
|
|
textStarted = false;
|
|
output.emit({ type: "text-end" });
|
|
}
|
|
hasTools = true;
|
|
const currentIdx = toolIndex++;
|
|
const callId = `call_${Date.now()}_${currentIdx}`;
|
|
output.emit({
|
|
type: "tool-call-start",
|
|
index: currentIdx,
|
|
callId,
|
|
name: fnCall.name,
|
|
});
|
|
const argsStr = typeof fnCall.args === "string"
|
|
? fnCall.args
|
|
: JSON.stringify(fnCall.args || {});
|
|
output.emit({
|
|
type: "tool-call-arguments-delta",
|
|
index: currentIdx,
|
|
delta: argsStr,
|
|
});
|
|
output.emit({
|
|
type: "tool-call-end",
|
|
index: currentIdx,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const finishReason = typeof candidate?.finishReason === "string"
|
|
? candidate.finishReason
|
|
: null;
|
|
if (finishReason) {
|
|
if (thinkingStarted) {
|
|
thinkingStarted = false;
|
|
output.emit({ type: "thinking-end" });
|
|
}
|
|
if (textStarted) {
|
|
textStarted = false;
|
|
output.emit({ type: "text-end" });
|
|
}
|
|
if (lastThoughtSignature) {
|
|
output.emit({
|
|
type: "replay-state",
|
|
providerKind: "antigravity",
|
|
value: { thoughtSignature: lastThoughtSignature },
|
|
});
|
|
lastThoughtSignature = null;
|
|
}
|
|
if (finalUsage) {
|
|
output.emit({
|
|
type: "usage",
|
|
usage: {
|
|
inputTokens: finalUsage.inputTokens,
|
|
outputTokens: finalUsage.outputTokens,
|
|
totalTokens: finalUsage.totalTokens,
|
|
cacheReadTokens: null,
|
|
cacheWriteTokens: null,
|
|
reasoningTokens: null,
|
|
},
|
|
});
|
|
finalUsage = null;
|
|
}
|
|
const isTool = finishReason === "STOP" &&
|
|
(hasTools || parts?.some((p) => p.functionCall));
|
|
output.emit({
|
|
type: "done",
|
|
reason: isTool ? "tool-use" : "stop",
|
|
});
|
|
doneEmitted = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (thinkingStarted) {
|
|
output.emit({ type: "thinking-end" });
|
|
}
|
|
if (textStarted) {
|
|
output.emit({ type: "text-end" });
|
|
}
|
|
if (lastThoughtSignature) {
|
|
output.emit({
|
|
type: "replay-state",
|
|
providerKind: "antigravity",
|
|
value: { thoughtSignature: lastThoughtSignature },
|
|
});
|
|
}
|
|
if (finalUsage) {
|
|
output.emit({
|
|
type: "usage",
|
|
usage: {
|
|
inputTokens: finalUsage.inputTokens,
|
|
outputTokens: finalUsage.outputTokens,
|
|
totalTokens: finalUsage.totalTokens,
|
|
cacheReadTokens: null,
|
|
cacheWriteTokens: null,
|
|
reasoningTokens: null,
|
|
},
|
|
});
|
|
}
|
|
if (!doneEmitted) {
|
|
output.emit({
|
|
type: "done",
|
|
reason: hasTools ? "tool-use" : "stop",
|
|
});
|
|
}
|
|
return;
|
|
} catch (err) {
|
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
if (hasEmittedAnyChunk) {
|
|
throw lastError;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lastError) throw lastError;
|
|
}
|
|
|
|
async function invoke(
|
|
input: ProviderInvokeInput,
|
|
output: ProviderOutput,
|
|
context: PluginContext,
|
|
): Promise<ProviderResult> {
|
|
if (!input.resource) {
|
|
return {
|
|
status: "request-error",
|
|
message: "Add a Google Antigravity account or API key before calling Antigravity",
|
|
};
|
|
}
|
|
let data: AccountData;
|
|
try {
|
|
data = accountData(input.resource);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return invalidResult(message, message);
|
|
}
|
|
|
|
let patchData: AccountData | null = null;
|
|
|
|
// Auto-refresh token if expired or close to expiration (skew 5 mins)
|
|
if (data.refreshToken && isTokenExpired(data)) {
|
|
try {
|
|
const refreshed = await refreshAccount(input.resource, context);
|
|
if (refreshed.privateData) {
|
|
data = refreshed.privateData as unknown as AccountData;
|
|
patchData = data;
|
|
}
|
|
} catch {
|
|
// Continue with existing token
|
|
}
|
|
}
|
|
|
|
let projectId = data.projectId ?? "bamboo-precept-lgxtn";
|
|
|
|
try {
|
|
await streamCloudCode(data.accessToken, projectId, input.model.id, input, output, context);
|
|
return patchData
|
|
? {
|
|
status: "completed",
|
|
patch: { privateData: patchData as unknown as JsonValue, state: { status: "ready" } },
|
|
}
|
|
: { status: "completed" };
|
|
} catch (error) {
|
|
if (error instanceof HttpError) {
|
|
if (
|
|
(error.status === 401 || error.status === 403) && data.refreshToken &&
|
|
!isQuotaHttpError(error)
|
|
) {
|
|
try {
|
|
const refreshed = await refreshAccount(input.resource, context);
|
|
if (refreshed.privateData) {
|
|
const freshData = refreshed.privateData as unknown as AccountData;
|
|
const freshProj = freshData.projectId ?? projectId;
|
|
await streamCloudCode(
|
|
freshData.accessToken,
|
|
freshProj,
|
|
input.model.id,
|
|
input,
|
|
output,
|
|
context,
|
|
);
|
|
return {
|
|
status: "completed",
|
|
patch: { privateData: freshData as unknown as JsonValue, state: { status: "ready" } },
|
|
};
|
|
}
|
|
} catch {
|
|
// Failed refresh
|
|
}
|
|
}
|
|
if (isQuotaHttpError(error)) {
|
|
return {
|
|
status: "resource-error",
|
|
message: error.message,
|
|
patch: quotaExhaustedPatch(data, error.body),
|
|
};
|
|
}
|
|
return { status: "request-error", message: error.message };
|
|
}
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
if (isQuotaError(message)) {
|
|
return { status: "resource-error", message, patch: quotaExhaustedPatch(data, message) };
|
|
}
|
|
return { status: "request-error", message };
|
|
}
|
|
}
|
|
|
|
export const antigravityProvider: ProviderSupport = {
|
|
id: "antigravity",
|
|
displayName: {
|
|
"en-US": "Google Antigravity",
|
|
"zh-CN": "Google Antigravity",
|
|
},
|
|
description: {
|
|
"en-US": "Google Antigravity / Gemini model access with hybrid reasoning & agent tools.",
|
|
"zh-CN": "通过 Google Antigravity / Gemini API 使用混合推理与 Agent 工具。",
|
|
},
|
|
providerType: "google",
|
|
resourceType: RESOURCE_TYPE,
|
|
models: antigravityModels,
|
|
invoke,
|
|
};
|