Merge pull request #101 from lethanhson9901/fix/antigravity-project-id-discovery

fix(antigravity): implement automatic Project ID discovery to resolve 403 errors
This commit is contained in:
何夕2077 2025-12-09 12:03:13 +08:00 committed by GitHub
commit c7e30b5149

View file

@ -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();