fix(antigravity): implement automatic project ID discovery and prevent overwrite
This commit introduces automatic Cloud Code project discovery for the Antigravity provider. It ports the onboarding logic from gemini-core.js, handles missing credentials by triggering the auth flow, and ensures the discovered Project ID is correctly passed to the request payload builder, fixing the 403 Permission Denied error caused by random ID generation.
This commit is contained in:
parent
3ec219bcc7
commit
abf32f6cb5
1 changed files with 172 additions and 99 deletions
|
|
@ -91,29 +91,29 @@ function generateProjectID() {
|
||||||
/**
|
/**
|
||||||
* 将 Gemini 格式请求转换为 Antigravity 格式
|
* 将 Gemini 格式请求转换为 Antigravity 格式
|
||||||
*/
|
*/
|
||||||
function geminiToAntigravity(modelName, payload) {
|
function geminiToAntigravity(modelName, payload, projectId) {
|
||||||
// 深拷贝请求体,避免修改原始对象
|
// 深拷贝请求体,避免修改原始对象
|
||||||
let template = JSON.parse(JSON.stringify(payload));
|
let template = JSON.parse(JSON.stringify(payload));
|
||||||
|
|
||||||
// 设置基本字段
|
// 设置基本字段
|
||||||
template.model = modelName;
|
template.model = modelName;
|
||||||
template.userAgent = 'antigravity';
|
template.userAgent = 'antigravity';
|
||||||
template.project = generateProjectID();
|
template.project = projectId || generateProjectID();
|
||||||
template.requestId = generateRequestID();
|
template.requestId = generateRequestID();
|
||||||
|
|
||||||
// 确保 request 对象存在
|
// 确保 request 对象存在
|
||||||
if (!template.request) {
|
if (!template.request) {
|
||||||
template.request = {};
|
template.request = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置会话ID
|
// 设置会话ID
|
||||||
template.request.sessionId = generateSessionID();
|
template.request.sessionId = generateSessionID();
|
||||||
|
|
||||||
// 删除安全设置
|
// 删除安全设置
|
||||||
if (template.request.safetySettings) {
|
if (template.request.safetySettings) {
|
||||||
delete template.request.safetySettings;
|
delete template.request.safetySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置工具配置
|
// 设置工具配置
|
||||||
if (template.request.toolConfig) {
|
if (template.request.toolConfig) {
|
||||||
if (!template.request.toolConfig.functionCallingConfig) {
|
if (!template.request.toolConfig.functionCallingConfig) {
|
||||||
|
|
@ -121,12 +121,12 @@ function geminiToAntigravity(modelName, payload) {
|
||||||
}
|
}
|
||||||
template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED';
|
template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除 maxOutputTokens
|
// 删除 maxOutputTokens
|
||||||
if (template.request.generationConfig && template.request.generationConfig.maxOutputTokens) {
|
if (template.request.generationConfig && template.request.generationConfig.maxOutputTokens) {
|
||||||
delete template.request.generationConfig.maxOutputTokens;
|
delete template.request.generationConfig.maxOutputTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 Thinking 配置
|
// 处理 Thinking 配置
|
||||||
if (!modelName.startsWith('gemini-3-')) {
|
if (!modelName.startsWith('gemini-3-')) {
|
||||||
if (template.request.generationConfig &&
|
if (template.request.generationConfig &&
|
||||||
|
|
@ -136,7 +136,7 @@ function geminiToAntigravity(modelName, payload) {
|
||||||
template.request.generationConfig.thinkingConfig.thinkingBudget = -1;
|
template.request.generationConfig.thinkingConfig.thinkingBudget = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 Claude Sonnet 模型的工具声明
|
// 处理 Claude Sonnet 模型的工具声明
|
||||||
if (modelName.startsWith('claude-sonnet-')) {
|
if (modelName.startsWith('claude-sonnet-')) {
|
||||||
if (template.request.tools && Array.isArray(template.request.tools)) {
|
if (template.request.tools && Array.isArray(template.request.tools)) {
|
||||||
|
|
@ -153,7 +153,7 @@ function geminiToAntigravity(modelName, payload) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,23 +162,23 @@ function geminiToAntigravity(modelName, payload) {
|
||||||
*/
|
*/
|
||||||
function toGeminiApiResponse(antigravityResponse) {
|
function toGeminiApiResponse(antigravityResponse) {
|
||||||
if (!antigravityResponse) return null;
|
if (!antigravityResponse) return null;
|
||||||
|
|
||||||
const compliantResponse = {
|
const compliantResponse = {
|
||||||
candidates: antigravityResponse.candidates
|
candidates: antigravityResponse.candidates
|
||||||
};
|
};
|
||||||
|
|
||||||
if (antigravityResponse.usageMetadata) {
|
if (antigravityResponse.usageMetadata) {
|
||||||
compliantResponse.usageMetadata = antigravityResponse.usageMetadata;
|
compliantResponse.usageMetadata = antigravityResponse.usageMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (antigravityResponse.promptFeedback) {
|
if (antigravityResponse.promptFeedback) {
|
||||||
compliantResponse.promptFeedback = antigravityResponse.promptFeedback;
|
compliantResponse.promptFeedback = antigravityResponse.promptFeedback;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (antigravityResponse.automaticFunctionCallingHistory) {
|
if (antigravityResponse.automaticFunctionCallingHistory) {
|
||||||
compliantResponse.automaticFunctionCallingHistory = antigravityResponse.automaticFunctionCallingHistory;
|
compliantResponse.automaticFunctionCallingHistory = antigravityResponse.automaticFunctionCallingHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
return compliantResponse;
|
return compliantResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,16 +187,16 @@ function toGeminiApiResponse(antigravityResponse) {
|
||||||
*/
|
*/
|
||||||
function ensureRolesInContents(requestBody) {
|
function ensureRolesInContents(requestBody) {
|
||||||
delete requestBody.model;
|
delete requestBody.model;
|
||||||
|
|
||||||
if (requestBody.system_instruction) {
|
if (requestBody.system_instruction) {
|
||||||
requestBody.systemInstruction = requestBody.system_instruction;
|
requestBody.systemInstruction = requestBody.system_instruction;
|
||||||
delete requestBody.system_instruction;
|
delete requestBody.system_instruction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestBody.systemInstruction && !requestBody.systemInstruction.role) {
|
if (requestBody.systemInstruction && !requestBody.systemInstruction.role) {
|
||||||
requestBody.systemInstruction.role = 'user';
|
requestBody.systemInstruction.role = 'user';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestBody.contents && Array.isArray(requestBody.contents)) {
|
if (requestBody.contents && Array.isArray(requestBody.contents)) {
|
||||||
requestBody.contents.forEach(content => {
|
requestBody.contents.forEach(content => {
|
||||||
if (!content.role) {
|
if (!content.role) {
|
||||||
|
|
@ -204,7 +204,7 @@ function ensureRolesInContents(requestBody) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestBody;
|
return requestBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,14 +213,14 @@ export class AntigravityApiService {
|
||||||
this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET);
|
this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET);
|
||||||
this.availableModels = [];
|
this.availableModels = [];
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.host = config.HOST;
|
this.host = config.HOST;
|
||||||
this.oauthCredsFilePath = config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH;
|
this.oauthCredsFilePath = config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH;
|
||||||
this.baseURL = ANTIGRAVITY_BASE_URL_DAILY; // 使用通用 GEMINI_BASE_URL 配置
|
this.baseURL = ANTIGRAVITY_BASE_URL_DAILY; // 使用通用 GEMINI_BASE_URL 配置
|
||||||
this.userAgent = DEFAULT_USER_AGENT; // 支持通用 USER_AGENT 配置
|
this.userAgent = DEFAULT_USER_AGENT; // 支持通用 USER_AGENT 配置
|
||||||
this.projectId = config.PROJECT_ID;
|
this.projectId = config.PROJECT_ID;
|
||||||
|
|
||||||
// 多环境降级顺序
|
// 多环境降级顺序
|
||||||
this.baseURLs = this.baseURL ? [this.baseURL] : [
|
this.baseURLs = this.baseURL ? [this.baseURL] : [
|
||||||
ANTIGRAVITY_BASE_URL_DAILY,
|
ANTIGRAVITY_BASE_URL_DAILY,
|
||||||
|
|
@ -228,44 +228,42 @@ export class AntigravityApiService {
|
||||||
// ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释
|
// ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
console.log('[Antigravity] Initializing Antigravity API Service...');
|
console.log('[Antigravity] Initializing Antigravity API Service...');
|
||||||
await this.initializeAuth();
|
await this.initializeAuth();
|
||||||
|
|
||||||
if (!this.projectId) {
|
if (!this.projectId) {
|
||||||
this.projectId = generateProjectID();
|
this.projectId = await this.discoverProjectAndModels();
|
||||||
console.log(`[Antigravity] Generated Project ID: ${this.projectId}`);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Antigravity] Using provided Project ID: ${this.projectId}`);
|
console.log(`[Antigravity] Using provided Project ID: ${this.projectId}`);
|
||||||
|
// 获取可用模型
|
||||||
|
await this.fetchAvailableModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可用模型
|
|
||||||
await this.fetchAvailableModels();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log(`[Antigravity] Initialization complete. Project ID: ${this.projectId}`);
|
console.log(`[Antigravity] Initialization complete. Project ID: ${this.projectId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeAuth(forceRefresh = false) {
|
async initializeAuth(forceRefresh = false) {
|
||||||
// 检查是否需要刷新 Token
|
// 检查是否需要刷新 Token
|
||||||
const needsRefresh = forceRefresh || this.isTokenExpiringSoon();
|
const needsRefresh = forceRefresh || this.isTokenExpiringSoon();
|
||||||
|
|
||||||
if (this.authClient.credentials.access_token && !needsRefresh) {
|
if (this.authClient.credentials.access_token && !needsRefresh) {
|
||||||
// Token 有效且不需要刷新
|
// Token 有效且不需要刷新
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Antigravity 不支持 base64 配置,直接使用文件路径
|
// Antigravity 不支持 base64 配置,直接使用文件路径
|
||||||
|
|
||||||
const credPath = this.oauthCredsFilePath || path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
|
const credPath = this.oauthCredsFilePath || path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(credPath, "utf8");
|
const data = await fs.readFile(credPath, "utf8");
|
||||||
const credentials = JSON.parse(data);
|
const credentials = JSON.parse(data);
|
||||||
this.authClient.setCredentials(credentials);
|
this.authClient.setCredentials(credentials);
|
||||||
console.log('[Antigravity Auth] Authentication configured successfully from file.');
|
console.log('[Antigravity Auth] Authentication configured successfully from file.');
|
||||||
|
|
||||||
if (needsRefresh) {
|
if (needsRefresh) {
|
||||||
console.log('[Antigravity Auth] Token expiring soon or force refresh requested. Refreshing token...');
|
console.log('[Antigravity Auth] Token expiring soon or force refresh requested. Refreshing token...');
|
||||||
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
||||||
|
|
@ -287,7 +285,7 @@ export class AntigravityApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNewToken(credPath) {
|
async getNewToken(credPath) {
|
||||||
let host = this.host;
|
let host = this.host;
|
||||||
if (!host || host === 'undefined') {
|
if (!host || host === 'undefined') {
|
||||||
|
|
@ -295,21 +293,21 @@ export class AntigravityApiService {
|
||||||
}
|
}
|
||||||
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
|
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
|
||||||
this.authClient.redirectUri = redirectUri;
|
this.authClient.redirectUri = redirectUri;
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const authUrl = this.authClient.generateAuthUrl({
|
const authUrl = this.authClient.generateAuthUrl({
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
scope: ['https://www.googleapis.com/auth/cloud-platform']
|
scope: ['https://www.googleapis.com/auth/cloud-platform']
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...');
|
console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...');
|
||||||
console.log('[Antigravity Auth] 授权链接:', authUrl, '\n');
|
console.log('[Antigravity Auth] 授权链接:', authUrl, '\n');
|
||||||
|
|
||||||
// 自动打开浏览器
|
// 自动打开浏览器
|
||||||
const showFallbackMessage = () => {
|
const showFallbackMessage = () => {
|
||||||
console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
|
console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.config) {
|
if (this.config) {
|
||||||
try {
|
try {
|
||||||
const childProcess = await open(authUrl);
|
const childProcess = await open(authUrl);
|
||||||
|
|
@ -322,19 +320,19 @@ export class AntigravityApiService {
|
||||||
} else {
|
} else {
|
||||||
showFallbackMessage();
|
showFallbackMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url, redirectUri);
|
const url = new URL(req.url, redirectUri);
|
||||||
const code = url.searchParams.get('code');
|
const code = url.searchParams.get('code');
|
||||||
const errorParam = url.searchParams.get('error');
|
const errorParam = url.searchParams.get('error');
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
console.log(`[Antigravity Auth] Received successful callback from Google: ${req.url}`);
|
console.log(`[Antigravity Auth] Received successful callback from Google: ${req.url}`);
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
res.end('Authentication successful! You can close this browser tab.');
|
res.end('Authentication successful! You can close this browser tab.');
|
||||||
server.close();
|
server.close();
|
||||||
|
|
||||||
const { tokens } = await this.authClient.getToken(code);
|
const { tokens } = await this.authClient.getToken(code);
|
||||||
await fs.mkdir(path.dirname(credPath), { recursive: true });
|
await fs.mkdir(path.dirname(credPath), { recursive: true });
|
||||||
await fs.writeFile(credPath, JSON.stringify(tokens, null, 2));
|
await fs.writeFile(credPath, JSON.stringify(tokens, null, 2));
|
||||||
|
|
@ -356,7 +354,7 @@ export class AntigravityApiService {
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', (err) => {
|
server.on('error', (err) => {
|
||||||
if (err.code === 'EADDRINUSE') {
|
if (err.code === 'EADDRINUSE') {
|
||||||
const errorMessage = `[Antigravity Auth] Port ${AUTH_REDIRECT_PORT} on ${host} is already in use.`;
|
const errorMessage = `[Antigravity Auth] Port ${AUTH_REDIRECT_PORT} on ${host} is already in use.`;
|
||||||
|
|
@ -366,11 +364,11 @@ export class AntigravityApiService {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(AUTH_REDIRECT_PORT, host);
|
server.listen(AUTH_REDIRECT_PORT, host);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isTokenExpiringSoon() {
|
isTokenExpiringSoon() {
|
||||||
if (!this.authClient.credentials.expiry_date) {
|
if (!this.authClient.credentials.expiry_date) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -380,10 +378,85 @@ export class AntigravityApiService {
|
||||||
const refreshSkewMs = REFRESH_SKEW * 1000;
|
const refreshSkewMs = REFRESH_SKEW * 1000;
|
||||||
return expiryTime <= (currentTime + refreshSkewMs);
|
return expiryTime <= (currentTime + refreshSkewMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async discoverProjectAndModels() {
|
||||||
|
if (this.projectId) {
|
||||||
|
console.log(`[Antigravity] Using pre-configured Project ID: ${this.projectId}`);
|
||||||
|
return this.projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Antigravity] Discovering Project ID...');
|
||||||
|
try {
|
||||||
|
const initialProjectId = "";
|
||||||
|
// Prepare client metadata
|
||||||
|
const clientMetadata = {
|
||||||
|
ideType: "IDE_UNSPECIFIED",
|
||||||
|
platform: "PLATFORM_UNSPECIFIED",
|
||||||
|
pluginType: "GEMINI",
|
||||||
|
duetProject: initialProjectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call loadCodeAssist to discover the actual project ID
|
||||||
|
const loadRequest = {
|
||||||
|
cloudaicompanionProject: initialProjectId,
|
||||||
|
metadata: clientMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadResponse = await this.callApi('loadCodeAssist', loadRequest);
|
||||||
|
|
||||||
|
// Check if we already have a project ID from the response
|
||||||
|
if (loadResponse.cloudaicompanionProject) {
|
||||||
|
console.log(`[Antigravity] Discovered existing Project ID: ${loadResponse.cloudaicompanionProject}`);
|
||||||
|
// 获取可用模型
|
||||||
|
await this.fetchAvailableModels();
|
||||||
|
return loadResponse.cloudaicompanionProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no existing project, we need to onboard
|
||||||
|
const defaultTier = loadResponse.allowedTiers?.find(tier => tier.isDefault);
|
||||||
|
const tierId = defaultTier?.id || 'free-tier';
|
||||||
|
|
||||||
|
const onboardRequest = {
|
||||||
|
tierId: tierId,
|
||||||
|
cloudaicompanionProject: initialProjectId,
|
||||||
|
metadata: clientMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
let lroResponse = await this.callApi('onboardUser', onboardRequest);
|
||||||
|
|
||||||
|
// Poll until operation is complete with timeout protection
|
||||||
|
const MAX_RETRIES = 30; // Maximum number of retries (60 seconds total)
|
||||||
|
let retryCount = 0;
|
||||||
|
|
||||||
|
while (!lroResponse.done && retryCount < MAX_RETRIES) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
lroResponse = await this.callApi('onboardUser', onboardRequest);
|
||||||
|
retryCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lroResponse.done) {
|
||||||
|
throw new Error('Onboarding timeout: Operation did not complete within expected time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId;
|
||||||
|
console.log(`[Antigravity] Onboarded and discovered Project ID: ${discoveredProjectId}`);
|
||||||
|
// 获取可用模型
|
||||||
|
await this.fetchAvailableModels();
|
||||||
|
return discoveredProjectId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Antigravity] Failed to discover Project ID:', error.response?.data || error.message);
|
||||||
|
console.log('[Antigravity] Falling back to generated Project ID as last resort...');
|
||||||
|
const fallbackProjectId = generateProjectID();
|
||||||
|
console.log(`[Antigravity] Generated fallback Project ID: ${fallbackProjectId}`);
|
||||||
|
// 获取可用模型
|
||||||
|
await this.fetchAvailableModels();
|
||||||
|
return fallbackProjectId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchAvailableModels() {
|
async fetchAvailableModels() {
|
||||||
console.log('[Antigravity] Fetching available models...');
|
console.log('[Antigravity] Fetching available models...');
|
||||||
|
|
||||||
for (const baseURL of this.baseURLs) {
|
for (const baseURL of this.baseURLs) {
|
||||||
try {
|
try {
|
||||||
const modelsURL = `${baseURL}/${ANTIGRAVITY_API_VERSION}:fetchAvailableModels`;
|
const modelsURL = `${baseURL}/${ANTIGRAVITY_API_VERSION}:fetchAvailableModels`;
|
||||||
|
|
@ -397,15 +470,15 @@ export class AntigravityApiService {
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({})
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await this.authClient.request(requestOptions);
|
const res = await this.authClient.request(requestOptions);
|
||||||
|
|
||||||
if (res.data && res.data.models) {
|
if (res.data && res.data.models) {
|
||||||
const models = Object.keys(res.data.models);
|
const models = Object.keys(res.data.models);
|
||||||
this.availableModels = models
|
this.availableModels = models
|
||||||
.map(modelName2Alias)
|
.map(modelName2Alias)
|
||||||
.filter(alias => alias !== '');
|
.filter(alias => alias !== '');
|
||||||
|
|
||||||
console.log(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`);
|
console.log(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -413,20 +486,20 @@ export class AntigravityApiService {
|
||||||
console.error(`[Antigravity] Failed to fetch models from ${baseURL}:`, error.message);
|
console.error(`[Antigravity] Failed to fetch models from ${baseURL}:`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('[Antigravity] Failed to fetch models from all endpoints. Using default models.');
|
console.warn('[Antigravity] Failed to fetch models from all endpoints. Using default models.');
|
||||||
this.availableModels = ANTIGRAVITY_MODELS;
|
this.availableModels = ANTIGRAVITY_MODELS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listModels() {
|
async listModels() {
|
||||||
if (!this.isInitialized) await this.initialize();
|
if (!this.isInitialized) await this.initialize();
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const formattedModels = this.availableModels.map(modelId => {
|
const formattedModels = this.availableModels.map(modelId => {
|
||||||
const displayName = modelId.split('-').map(word =>
|
const displayName = modelId.split('-').map(word =>
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
).join(' ');
|
).join(' ');
|
||||||
|
|
||||||
const modelInfo = {
|
const modelInfo = {
|
||||||
name: `models/${modelId}`,
|
name: `models/${modelId}`,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
|
|
@ -440,7 +513,7 @@ export class AntigravityApiService {
|
||||||
ownedBy: 'antigravity',
|
ownedBy: 'antigravity',
|
||||||
type: 'antigravity'
|
type: 'antigravity'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (modelId.endsWith('-thinking') || modelId.includes('-thinking-')) {
|
if (modelId.endsWith('-thinking') || modelId.includes('-thinking-')) {
|
||||||
modelInfo.thinking = {
|
modelInfo.thinking = {
|
||||||
min: 1024,
|
min: 1024,
|
||||||
|
|
@ -449,23 +522,23 @@ export class AntigravityApiService {
|
||||||
dynamicAllowed: true
|
dynamicAllowed: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return modelInfo;
|
return modelInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { models: formattedModels };
|
return { models: formattedModels };
|
||||||
}
|
}
|
||||||
|
|
||||||
async callApi(method, body, isRetry = false, retryCount = 0, baseURLIndex = 0) {
|
async callApi(method, body, isRetry = false, retryCount = 0, baseURLIndex = 0) {
|
||||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||||
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
|
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
|
||||||
|
|
||||||
if (baseURLIndex >= this.baseURLs.length) {
|
if (baseURLIndex >= this.baseURLs.length) {
|
||||||
throw new Error('All Antigravity base URLs failed');
|
throw new Error('All Antigravity base URLs failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = this.baseURLs[baseURLIndex];
|
const baseURL = this.baseURLs[baseURLIndex];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
url: `${baseURL}/${ANTIGRAVITY_API_VERSION}:${method}`,
|
url: `${baseURL}/${ANTIGRAVITY_API_VERSION}:${method}`,
|
||||||
|
|
@ -477,18 +550,18 @@ export class AntigravityApiService {
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await this.authClient.request(requestOptions);
|
const res = await this.authClient.request(requestOptions);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, error.response?.status, error.message);
|
console.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, error.response?.status, error.message);
|
||||||
|
|
||||||
if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) {
|
if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) {
|
||||||
console.log('[Antigravity API] Received 401/400. Refreshing auth and retrying...');
|
console.log('[Antigravity API] Received 401/400. Refreshing auth and retrying...');
|
||||||
await this.initializeAuth(true);
|
await this.initializeAuth(true);
|
||||||
return this.callApi(method, body, true, retryCount, baseURLIndex);
|
return this.callApi(method, body, true, retryCount, baseURLIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.response?.status === 429) {
|
if (error.response?.status === 429) {
|
||||||
if (baseURLIndex + 1 < this.baseURLs.length) {
|
if (baseURLIndex + 1 < this.baseURLs.length) {
|
||||||
console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`);
|
console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`);
|
||||||
|
|
@ -500,33 +573,33 @@ export class AntigravityApiService {
|
||||||
return this.callApi(method, body, isRetry, retryCount + 1, 0);
|
return this.callApi(method, body, isRetry, retryCount + 1, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!error.response && baseURLIndex + 1 < this.baseURLs.length) {
|
if (!error.response && baseURLIndex + 1 < this.baseURLs.length) {
|
||||||
console.log(`[Antigravity API] Network error on ${baseURL}. Trying next base URL...`);
|
console.log(`[Antigravity API] Network error on ${baseURL}. Trying next base URL...`);
|
||||||
return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1);
|
return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) {
|
if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) {
|
||||||
const delay = baseDelay * Math.pow(2, retryCount);
|
const delay = baseDelay * Math.pow(2, retryCount);
|
||||||
console.log(`[Antigravity API] Server error ${error.response.status}. Retrying in ${delay}ms...`);
|
console.log(`[Antigravity API] Server error ${error.response.status}. Retrying in ${delay}ms...`);
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
return this.callApi(method, body, isRetry, retryCount + 1, baseURLIndex);
|
return this.callApi(method, body, isRetry, retryCount + 1, baseURLIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async * streamApi(method, body, isRetry = false, retryCount = 0, baseURLIndex = 0) {
|
async * streamApi(method, body, isRetry = false, retryCount = 0, baseURLIndex = 0) {
|
||||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||||
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
|
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
|
||||||
|
|
||||||
if (baseURLIndex >= this.baseURLs.length) {
|
if (baseURLIndex >= this.baseURLs.length) {
|
||||||
throw new Error('All Antigravity base URLs failed');
|
throw new Error('All Antigravity base URLs failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = this.baseURLs[baseURLIndex];
|
const baseURL = this.baseURLs[baseURLIndex];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
url: `${baseURL}/${ANTIGRAVITY_API_VERSION}:${method}`,
|
url: `${baseURL}/${ANTIGRAVITY_API_VERSION}:${method}`,
|
||||||
|
|
@ -540,9 +613,9 @@ export class AntigravityApiService {
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await this.authClient.request(requestOptions);
|
const res = await this.authClient.request(requestOptions);
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
let errorBody = '';
|
let errorBody = '';
|
||||||
for await (const chunk of res.data) {
|
for await (const chunk of res.data) {
|
||||||
|
|
@ -550,18 +623,18 @@ export class AntigravityApiService {
|
||||||
}
|
}
|
||||||
throw new Error(`Upstream API Error (Status ${res.status}): ${errorBody}`);
|
throw new Error(`Upstream API Error (Status ${res.status}): ${errorBody}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* this.parseSSEStream(res.data);
|
yield* this.parseSSEStream(res.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, error.response?.status, error.message);
|
console.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, error.response?.status, error.message);
|
||||||
|
|
||||||
if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) {
|
if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) {
|
||||||
console.log('[Antigravity API] Received 401/400 during stream. Refreshing auth and retrying...');
|
console.log('[Antigravity API] Received 401/400 during stream. Refreshing auth and retrying...');
|
||||||
await this.initializeAuth(true);
|
await this.initializeAuth(true);
|
||||||
yield* this.streamApi(method, body, true, retryCount, baseURLIndex);
|
yield* this.streamApi(method, body, true, retryCount, baseURLIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.response?.status === 429) {
|
if (error.response?.status === 429) {
|
||||||
if (baseURLIndex + 1 < this.baseURLs.length) {
|
if (baseURLIndex + 1 < this.baseURLs.length) {
|
||||||
console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`);
|
console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`);
|
||||||
|
|
@ -575,13 +648,13 @@ export class AntigravityApiService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!error.response && baseURLIndex + 1 < this.baseURLs.length) {
|
if (!error.response && baseURLIndex + 1 < this.baseURLs.length) {
|
||||||
console.log(`[Antigravity API] Network error on ${baseURL}. Trying next base URL...`);
|
console.log(`[Antigravity API] Network error on ${baseURL}. Trying next base URL...`);
|
||||||
yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1);
|
yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) {
|
if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) {
|
||||||
const delay = baseDelay * Math.pow(2, retryCount);
|
const delay = baseDelay * Math.pow(2, retryCount);
|
||||||
console.log(`[Antigravity API] Server error ${error.response.status} during stream. Retrying in ${delay}ms...`);
|
console.log(`[Antigravity API] Server error ${error.response.status} during stream. Retrying in ${delay}ms...`);
|
||||||
|
|
@ -589,17 +662,17 @@ export class AntigravityApiService {
|
||||||
yield* this.streamApi(method, body, isRetry, retryCount + 1, baseURLIndex);
|
yield* this.streamApi(method, body, isRetry, retryCount + 1, baseURLIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async * parseSSEStream(stream) {
|
async * parseSSEStream(stream) {
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: stream,
|
input: stream,
|
||||||
crlfDelay: Infinity
|
crlfDelay: Infinity
|
||||||
});
|
});
|
||||||
|
|
||||||
let buffer = [];
|
let buffer = [];
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
|
|
@ -613,7 +686,7 @@ export class AntigravityApiService {
|
||||||
buffer = [];
|
buffer = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.length > 0) {
|
if (buffer.length > 0) {
|
||||||
try {
|
try {
|
||||||
yield JSON.parse(buffer.join('\n'));
|
yield JSON.parse(buffer.join('\n'));
|
||||||
|
|
@ -622,55 +695,55 @@ export class AntigravityApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateContent(model, requestBody) {
|
async generateContent(model, requestBody) {
|
||||||
console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
|
console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
|
||||||
|
|
||||||
let selectedModel = model;
|
let selectedModel = model;
|
||||||
if (!this.availableModels.includes(model)) {
|
if (!this.availableModels.includes(model)) {
|
||||||
console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`);
|
console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`);
|
||||||
selectedModel = this.availableModels[0];
|
selectedModel = this.availableModels[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 深拷贝请求体
|
// 深拷贝请求体
|
||||||
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)));
|
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)));
|
||||||
const actualModelName = alias2ModelName(selectedModel);
|
const actualModelName = alias2ModelName(selectedModel);
|
||||||
|
|
||||||
// 将处理后的请求体转换为 Antigravity 格式
|
// 将处理后的请求体转换为 Antigravity 格式
|
||||||
const payload = geminiToAntigravity(actualModelName, { request: processedRequestBody });
|
const payload = geminiToAntigravity(actualModelName, { request: processedRequestBody }, this.projectId);
|
||||||
|
|
||||||
// 设置模型名称为实际模型名
|
// 设置模型名称为实际模型名
|
||||||
payload.model = actualModelName;
|
payload.model = actualModelName;
|
||||||
|
|
||||||
const response = await this.callApi('generateContent', payload);
|
const response = await this.callApi('generateContent', payload);
|
||||||
return toGeminiApiResponse(response.response);
|
return toGeminiApiResponse(response.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async * generateContentStream(model, requestBody) {
|
async * generateContentStream(model, requestBody) {
|
||||||
console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
|
console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
|
||||||
|
|
||||||
let selectedModel = model;
|
let selectedModel = model;
|
||||||
if (!this.availableModels.includes(model)) {
|
if (!this.availableModels.includes(model)) {
|
||||||
console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`);
|
console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`);
|
||||||
selectedModel = this.availableModels[0];
|
selectedModel = this.availableModels[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 深拷贝请求体
|
// 深拷贝请求体
|
||||||
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)));
|
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)));
|
||||||
const actualModelName = alias2ModelName(selectedModel);
|
const actualModelName = alias2ModelName(selectedModel);
|
||||||
|
|
||||||
// 将处理后的请求体转换为 Antigravity 格式
|
// 将处理后的请求体转换为 Antigravity 格式
|
||||||
const payload = geminiToAntigravity(actualModelName, { request: processedRequestBody });
|
const payload = geminiToAntigravity(actualModelName, { request: processedRequestBody }, this.projectId);
|
||||||
|
|
||||||
// 设置模型名称为实际模型名
|
// 设置模型名称为实际模型名
|
||||||
payload.model = actualModelName;
|
payload.model = actualModelName;
|
||||||
|
|
||||||
const stream = this.streamApi('streamGenerateContent', payload);
|
const stream = this.streamApi('streamGenerateContent', payload);
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
yield toGeminiApiResponse(chunk.response);
|
yield toGeminiApiResponse(chunk.response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isExpiryDateNear() {
|
isExpiryDateNear() {
|
||||||
try {
|
try {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue