import { OAuth2Client } from 'google-auth-library'; import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; import open from 'open'; import { configureTLSSidecar } from '../../utils/proxy-utils.js'; import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; import { MODEL_PROVIDER } from '../../utils/common.js'; // --- Constants --- const AUTH_REDIRECT_PORT = 8085; const CREDENTIALS_DIR = '.gemini'; const CREDENTIALS_FILE = 'oauth_creds.json'; const DEFAULT_CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com'; const DEFAULT_CODE_ASSIST_API_VERSION = 'v1internal'; const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'; const OAUTH_CLIENT_SECRET = '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'; const GEMINI_MODELS = getProviderModels(MODEL_PROVIDER.GEMINI_CLI); const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`); const GEMINI_CLI_VERSION = '0.31.0'; const GEMINI_CLI_API_CLIENT_HEADER = 'google-genai-sdk/1.41.0 gl-node/v22.19.0'; /** * 设置 Gemini CLI 所需的特定请求头 * @param {Object} headers - 请求头对象 * @param {string} model - 模型名称 */ function applyGeminiCLIHeaders(headers, model) { const platform = os.platform(); let arch = os.arch(); if (arch === 'ia32') arch = 'x86'; const modelName = model || 'unknown'; if (model !== 'load-code-assist' && model !== 'onboard-user') { headers['User-Agent'] = `GeminiCLI/${GEMINI_CLI_VERSION}/${modelName} (${platform}; ${arch})`; } headers['X-Goog-Api-Client'] = GEMINI_CLI_API_CLIENT_HEADER; } /** * 从 Google API 的 429 错误响应中提取重试延迟 * @param {Object|string} errorBody - 错误响应体 * @returns {number|null} 延迟毫秒数 */ function parseRetryDelay(errorBody) { try { const data = typeof errorBody === 'string' ? JSON.parse(errorBody) : errorBody; const details = data?.error?.details; if (Array.isArray(details)) { for (const detail of details) { if (detail['@type'] === 'type.googleapis.com/google.rpc.RetryInfo') { const retryDelay = detail.retryDelay; if (retryDelay) { const match = retryDelay.match(/^([\d.]+)s$/); if (match) return parseFloat(match[1]) * 1000; } } } for (const detail of details) { if (detail['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo') { const quotaResetDelay = detail.metadata?.quotaResetDelay; if (quotaResetDelay) { const match = quotaResetDelay.match(/^([\d.]+)(ms|s)$/); if (match) { let ms = parseFloat(match[1]); if (match[2] === 's') ms *= 1000; return ms; } } } } } const message = data?.error?.message; if (message) { const match = message.match(/after\s+(\d+)s\.?/); if (match) return parseInt(match[1]) * 1000; } } catch (e) {} return null; } function is_anti_truncation_model(model) { return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel)); } // 从防截断模型名中提取实际模型名 function extract_model_from_anti_model(model) { if (model.startsWith('anti-')) { const originalModel = model.substring(5); // 移除 'anti-' 前缀 if (GEMINI_MODELS.includes(originalModel)) { return originalModel; } } return model; // 如果不是anti-前缀或不在原模型列表中,则返回原模型名 } function modelSupportsThinking(modelName) { if (!modelName) return false; const name = String(modelName).toLowerCase(); return name.startsWith('gemini-3') || name.startsWith('gemini-2.5-') || name.includes('-thinking'); } function normalizeGeminiThinkingRequest(modelName, requestBody) { if (!modelSupportsThinking(modelName)) return requestBody; const thinkingConfig = requestBody?.generationConfig?.thinkingConfig; if (!thinkingConfig) return requestBody; const thinkingLevel = thinkingConfig.thinkingLevel; const budget = thinkingConfig.thinkingBudget; const thinkingRequested = thinkingLevel !== undefined || (budget !== undefined && budget !== 0); // Gemini 只有在 includeThoughts=true 时才会稳定返回 thought parts。 // 上游在 gemini-3 thinkingLevel 场景下可能没有显式传这个字段,这里兜底补齐。 if (thinkingRequested && thinkingConfig.includeThoughts === undefined) { thinkingConfig.includeThoughts = true; } return requestBody; } function toGeminiApiResponse(codeAssistResponse) { if (!codeAssistResponse) return null; const compliantResponse = { candidates: codeAssistResponse.candidates }; if (codeAssistResponse.usageMetadata) compliantResponse.usageMetadata = codeAssistResponse.usageMetadata; if (codeAssistResponse.promptFeedback) compliantResponse.promptFeedback = codeAssistResponse.promptFeedback; if (codeAssistResponse.automaticFunctionCallingHistory) compliantResponse.automaticFunctionCallingHistory = codeAssistResponse.automaticFunctionCallingHistory; return compliantResponse; } /** * Ensures that all content parts in a request body have a 'role' property. * If 'systemInstruction' is present and lacks a role, it defaults to 'user'. * If any 'contents' entry lacks a role, it defaults to 'user'. * @param {Object} requestBody - The request body object. * @returns {Object} The modified request body with roles ensured. */ function ensureRolesInContents(requestBody) { delete requestBody.model; // delete requestBody.system_instruction; // delete requestBody.systemInstruction; if (requestBody.system_instruction) { requestBody.systemInstruction = requestBody.system_instruction; delete requestBody.system_instruction; } if (requestBody.systemInstruction && !requestBody.systemInstruction.role) { requestBody.systemInstruction.role = 'user'; } if (requestBody.contents && Array.isArray(requestBody.contents)) { requestBody.contents.forEach(content => { if (!content.role) { content.role = 'user'; } }); // 如果存在 systemInstruction,将其放在 contents 索引 0 的位置 // if (requestBody.systemInstruction) { // // 检查 contents[0] 是否与 systemInstruction 内容相同 // const firstContent = requestBody.contents[0]; // let isSame = false; // if (firstContent && firstContent.parts && requestBody.systemInstruction.parts) { // // 比较 parts 数组的内容 // const firstContentText = firstContent.parts // .filter(p => p?.text) // .map(p => p.text) // .join('\n'); // const systemInstructionText = requestBody.systemInstruction.parts // .filter(p => p?.text) // .map(p => p.text) // .join('\n'); // isSame = firstContentText === systemInstructionText; // } // // 如果内容不同,则将 systemInstruction 插入到索引 0 的位置 // if (!isSame) { // requestBody.contents.unshift({ // role: requestBody.systemInstruction.role || 'user', // parts: requestBody.systemInstruction.parts // }); // } // delete requestBody.systemInstruction; // } } return requestBody; } async function* apply_anti_truncation_to_stream(service, model, requestBody) { let currentRequest = { ...requestBody }; let allGeneratedText = ''; while (true) { // 发送请求并处理流式响应 const apiRequest = { model: model, project: service.projectId, request: currentRequest }; const stream = service.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest, false, 0, model); let lastChunk = null; let hasContent = false; for await (const chunk of stream) { const response = toGeminiApiResponse(chunk.response); if (response && response.candidates && response.candidates[0]) { yield response; lastChunk = response; hasContent = true; } } // 检查是否因为达到token限制而截断 if (lastChunk && lastChunk.candidates && lastChunk.candidates[0] && lastChunk.candidates[0].finishReason === 'MAX_TOKENS') { // 提取已生成的文本内容 if (lastChunk.candidates[0].content && Array.isArray(lastChunk.candidates[0].content.parts)) { const generatedParts = lastChunk.candidates[0].content.parts .filter(part => part?.text) .map(part => part.text); if (generatedParts.length > 0) { const currentGeneratedText = generatedParts.join(''); allGeneratedText += currentGeneratedText; // 构建新的请求,包含之前的对话历史和继续指令 const newContents = [...requestBody.contents]; // 添加之前生成的内容作为模型响应 newContents.push({ role: 'model', parts: [{ text: currentGeneratedText }] }); // 添加继续生成的指令 newContents.push({ role: 'user', parts: [{ text: 'Please continue from where you left off.' }] }); currentRequest = { ...requestBody, contents: newContents }; // 继续下一轮请求 continue; } } } // 如果没有截断或无法继续,则退出循环 break; } } export class GeminiApiService { constructor(config) { // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 this.httpAgent = new http.Agent({ keepAlive: true, maxSockets: 100, maxFreeSockets: 5, timeout: 120000, }); this.httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 100, maxFreeSockets: 5, timeout: 120000, }); // 检查是否需要使用代理 const proxyConfig = getGoogleAuthProxyConfig(config, config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI); // 配置 OAuth2Client 使用自定义的 HTTP agent const oauth2Options = { clientId: OAUTH_CLIENT_ID, clientSecret: OAUTH_CLIENT_SECRET, }; if (proxyConfig) { oauth2Options.transporterOptions = proxyConfig; logger.info('[Gemini] Using proxy for OAuth2Client'); } else { oauth2Options.transporterOptions = { agent: this.httpsAgent, }; } this.authClient = new OAuth2Client(oauth2Options); this.availableModels = []; this.isInitialized = false; this.config = config; this.host = config.HOST; this.uuid = config.uuid; this.oauthCredsBase64 = config.GEMINI_OAUTH_CREDS_BASE64; this.oauthCredsFilePath = config.GEMINI_OAUTH_CREDS_FILE_PATH; this.projectId = config.PROJECT_ID; this.codeAssistEndpoint = config.GEMINI_BASE_URL || DEFAULT_CODE_ASSIST_ENDPOINT; this.apiVersion = DEFAULT_CODE_ASSIST_API_VERSION; // 保存代理配置供后续使用 this.proxyConfig = getProxyConfigForProvider(config, config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI); } async initialize() { if (this.isInitialized) return; logger.info('[Gemini] Initializing Gemini API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); if (!this.projectId) { this.projectId = await this.discoverProjectAndModels(); } else { logger.info(`[Gemini] Using provided Project ID: ${this.projectId}`); this.availableModels = GEMINI_MODELS; logger.info(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); } if (this.projectId === 'default') { throw new Error("Error: 'default' is not a valid project ID. Please provide a valid Google Cloud Project ID using the --project-id argument."); } this.isInitialized = true; logger.info(`[Gemini] Initialization complete. Project ID: ${this.projectId}`); } _applySidecar(requestOptions) { return configureTLSSidecar(requestOptions, this.config, this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI); } /** * 加载凭证信息(不执行刷新) */ async loadCredentials() { if (this.oauthCredsBase64) { try { const decoded = Buffer.from(this.oauthCredsBase64, 'base64').toString('utf8'); const credentials = JSON.parse(decoded); this.authClient.setCredentials(credentials); logger.info('[Gemini Auth] Credentials loaded successfully from base64 string.'); return; } catch (error) { logger.error('[Gemini Auth] Failed to parse base64 OAuth credentials:', error); } } const credPath = this.oauthCredsFilePath || path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE); try { const data = await fs.readFile(credPath, "utf8"); const credentials = JSON.parse(data); this.authClient.setCredentials(credentials); logger.info('[Gemini Auth] Credentials loaded successfully from file.'); } catch (error) { if (error.code === 'ENOENT') { logger.debug(`[Gemini Auth] Credentials file not found: ${credPath}`); } else { logger.warn(`[Gemini Auth] Failed to load credentials from file: ${error.message}`); } } } async initializeAuth(forceRefresh = false) { // 首先执行基础凭证加载 await this.loadCredentials(); // 检查是否需要刷新 Token(加载凭证后评估) const needsRefresh = forceRefresh || this.isTokenExpiringSoon(); if (this.authClient.credentials.access_token && !needsRefresh) { // Token 有效且不需要刷新 return; } // 只有在明确要求刷新,或者 AccessToken 确实缺失时,才执行刷新/认证 // 注意:在 V2 架构下,此方法主要由 PoolManager 的后台队列调用 if (needsRefresh || !this.authClient.credentials.access_token) { const credPath = this.oauthCredsFilePath || path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE); try { if (this.authClient.credentials.refresh_token) { logger.info('[Gemini Auth] Token expiring soon or force refresh requested. Refreshing token...'); const { credentials: newCredentials } = await this.authClient.refreshAccessToken(); this.authClient.setCredentials(newCredentials); // 如果不是从 base64 加载的,则保存到文件 if (!this.oauthCredsBase64) { await this._saveCredentialsToFile(credPath, newCredentials); logger.info('[Gemini Auth] Token refreshed and saved successfully.'); } else { logger.info('[Gemini Auth] Token refreshed successfully (Base64 source).'); } // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { poolManager.resetProviderRefreshStatus(this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI, this.uuid); } } else { logger.info(`[Gemini Auth] No access token or refresh token. Starting new authentication flow...`); const newTokens = await this.getNewToken(credPath); this.authClient.setCredentials(newTokens); logger.info('[Gemini Auth] New token obtained and loaded into memory.'); // 认证成功,重置状态 const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { poolManager.resetProviderRefreshStatus(this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI, this.uuid); } } } catch (error) { logger.error('[Gemini Auth] Failed to initialize authentication:', error); throw new Error(`Failed to load OAuth credentials.`); } } } async getNewToken(credPath) { // 使用统一的 OAuth 处理方法 const { authUrl, authInfo } = await handleGeminiCliOAuth(this.config); logger.info('\n[Gemini Auth] 正在自动打开浏览器进行授权...'); logger.info('[Gemini Auth] 授权链接:', authUrl, '\n'); // 自动打开浏览器 const showFallbackMessage = () => { logger.info('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); }; if (this.config) { try { const childProcess = await open(authUrl); if (childProcess) { childProcess.on('error', () => showFallbackMessage()); } } catch (_err) { showFallbackMessage(); } } else { showFallbackMessage(); } // 等待 OAuth 回调完成并读取保存的凭据 return new Promise((resolve, reject) => { const checkInterval = setInterval(async () => { try { const data = await fs.readFile(credPath, 'utf8'); const credentials = JSON.parse(data); if (credentials.access_token) { clearInterval(checkInterval); logger.info('[Gemini Auth] New token obtained successfully.'); resolve(credentials); } } catch (error) { // 文件尚未创建或无效,继续等待 } }, 1000); // 设置超时(5分钟) setTimeout(() => { clearInterval(checkInterval); reject(new Error('[Gemini Auth] OAuth 授权超时')); }, 5 * 60 * 1000); }); } async discoverProjectAndModels() { if (this.projectId) { logger.info(`[Gemini] Using pre-configured Project ID: ${this.projectId}`); return this.projectId; } logger.info('[Gemini] Discovering Project ID...'); this.availableModels = GEMINI_MODELS; logger.info(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); 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, false, 0, 'load-code-assist'); // Check if we already have a project ID from the response if (loadResponse.cloudaicompanionProject) { 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, false, 0, 'onboard-user'); // 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, false, 0, 'onboard-user'); retryCount++; } if (!lroResponse.done) { throw new Error('Onboarding timeout: Operation did not complete within expected time.'); } const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId; return discoveredProjectId; } catch (error) { logger.error('[Gemini] Failed to discover Project ID:', error.response?.data || error.message); throw new Error('Could not discover a valid Google Cloud Project ID.'); } } async listModels() { if (!this.isInitialized) await this.initialize(); const formattedModels = this.availableModels.map(modelId => { const displayName = modelId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); return { name: `models/${modelId}`, version: "1.0.0", displayName: displayName, description: `A generative model for text and chat generation. ID: ${modelId}`, inputTokenLimit: 1024000, outputTokenLimit: 65535, supportedGenerationMethods: ["generateContent", "streamGenerateContent"], }; }); return { models: formattedModels }; } async callApi(method, body, isRetry = false, retryCount = 0, model = 'unknown') { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const headers = { "Content-Type": "application/json" }; applyGeminiCLIHeaders(headers, model); const requestOptions = { url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`, method: "POST", headers: headers, responseType: "json", body: JSON.stringify(body), }; this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); return res.data; } catch (error) { const status = error.response?.status; const errorCode = error.code; const errorMessage = error.message || ''; // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); logger.error(`[Gemini API] Error calling (Status: ${status}, Code: ${errorCode}):`, errorMessage); // Handle 401 (Unauthorized) - refresh auth and retry once if ((status === 400 || status === 401) && !isRetry) { logger.info('[Gemini API] Received 401/400. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { logger.info(`[Gemini] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized`); poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); error.credentialMarkedUnhealthy = true; } // Mark error for credential switch without recording error count error.shouldSwitchCredential = true; error.skipErrorCount = true; throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = parseRetryDelay(error.response?.data) || (baseDelay * Math.pow(2, retryCount)); logger.info(`[Gemini API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, model); } // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); logger.info(`[Gemini API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, model); } // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); logger.info(`[Gemini API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, model); } throw error; } } async * streamApi(method, body, isRetry = false, retryCount = 0, model = 'unknown') { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const headers = { "Content-Type": "application/json" }; applyGeminiCLIHeaders(headers, model); const requestOptions = { url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`, method: "POST", params: { alt: "sse" }, headers: headers, responseType: "stream", body: JSON.stringify(body), }; this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); if (res.status !== 200) { let errorBody = ''; for await (const chunk of res.data) errorBody += chunk.toString(); throw new Error(`Upstream API Error (Status ${res.status}): ${errorBody}`); } yield* this.parseSSEStream(res.data); } catch (error) { const status = error.response?.status; const errorCode = error.code; const errorMessage = error.message || ''; // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); logger.error(`[Gemini API] Error during stream (Status: ${status}, Code: ${errorCode}):`, errorMessage); // Handle 401 (Unauthorized) - refresh auth and retry once if ((status === 400 || status === 401) && !isRetry) { logger.info('[Gemini API] Received 401/400 during stream. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { logger.info(`[Gemini] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized in stream`); poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); error.credentialMarkedUnhealthy = true; } // Mark error for credential switch without recording error count error.shouldSwitchCredential = true; error.skipErrorCount = true; throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = parseRetryDelay(error.response?.data) || (baseDelay * Math.pow(2, retryCount)); logger.info(`[Gemini API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, model); return; } // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); logger.info(`[Gemini API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, model); return; } // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); logger.info(`[Gemini API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, model); return; } throw error; } } async * parseSSEStream(stream) { const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); let buffer = []; for await (const line of rl) { if (line.startsWith("data: ")) buffer.push(line.slice(6)); else if (line === "" && buffer.length > 0) { try { yield JSON.parse(buffer.join('\n')); } catch (e) { logger.error("[Stream] Failed to parse JSON chunk:", buffer.join('\n')); } buffer = []; } } if (buffer.length > 0) { try { yield JSON.parse(buffer.join('\n')); } catch (e) { logger.error("[Stream] Failed to parse final JSON chunk:", buffer.join('\n')); } } } async generateContent(model, requestBody) { logger.info(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); // 临时存储 monitorRequestId if (requestBody._monitorRequestId) { this.config._monitorRequestId = requestBody._monitorRequestId; delete requestBody._monitorRequestId; } if (requestBody._requestBaseUrl) { delete requestBody._requestBaseUrl; } // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { logger.info(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); } } let baseModel = model; if (!GEMINI_MODELS.includes(model)) { logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); baseModel = GEMINI_MODELS[0]; } const processedRequestBody = normalizeGeminiThinkingRequest( baseModel, ensureRolesInContents({ ...requestBody }) ); const apiRequest = { model: baseModel, project: this.projectId, request: processedRequestBody }; const response = await this.callApi(API_ACTIONS.GENERATE_CONTENT, apiRequest, false, 0, baseModel); return toGeminiApiResponse(response.response); } async * generateContentStream(model, requestBody) { logger.info(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); // 临时存储 monitorRequestId if (requestBody._monitorRequestId) { this.config._monitorRequestId = requestBody._monitorRequestId; delete requestBody._monitorRequestId; } if (requestBody._requestBaseUrl) { delete requestBody._requestBaseUrl; } // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { logger.info(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); } } // 检查是否为防截断模型 if (is_anti_truncation_model(model)) { // 从防截断模型名中提取实际模型名 const actualModel = extract_model_from_anti_model(model); // 使用防截断流处理 const processedRequestBody = normalizeGeminiThinkingRequest( actualModel, ensureRolesInContents({ ...requestBody }) ); yield* apply_anti_truncation_to_stream(this, actualModel, processedRequestBody); return; } let baseModel = model; if (!GEMINI_MODELS.includes(model)) { logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); baseModel = GEMINI_MODELS[0]; } const processedRequestBody = normalizeGeminiThinkingRequest( baseModel, ensureRolesInContents({ ...requestBody }) ); const apiRequest = { model: baseModel, project: this.projectId, request: processedRequestBody }; const stream = this.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest, false, 0, baseModel); for await (const chunk of stream) { yield toGeminiApiResponse(chunk.response); } } /** * Checks if the given expiry date is within the next 10 minutes from now. * @returns {boolean} True if the expiry date is within the next 10 minutes, false otherwise. */ isExpiryDateNear() { try { const nearMinutes = 20; const { message, isNearExpiry } = formatExpiryLog('Gemini', this.authClient.credentials.expiry_date, nearMinutes); logger.info(message); return isNearExpiry; } catch (error) { logger.error(`[Gemini] Error checking expiry date: ${error.message}`); return false; } } isTokenExpiringSoon() { if (!this.authClient.credentials.expiry_date) { return false; } const currentTime = Date.now(); const expiryTime = this.authClient.credentials.expiry_date; const REFRESH_SKEW = 3000; // 50分钟 const refreshSkewMs = REFRESH_SKEW * 1000; return expiryTime <= (currentTime + refreshSkewMs); } /** * 保存凭证到文件 * @param {string} filePath - 凭证文件路径 * @param {Object} credentials - 凭证数据 */ async _saveCredentialsToFile(filePath, credentials) { try { await fs.writeFile(filePath, JSON.stringify(credentials, null, 2)); logger.info(`[Gemini Auth] Credentials saved to ${filePath}`); } catch (error) { logger.error(`[Gemini Auth] Failed to save credentials to ${filePath}: ${error.message}`); throw error; } } /** * 获取模型配额信息 * @returns {Promise} 模型配额信息 */ async getUsageLimits() { if (!this.isInitialized) await this.initialize(); // 注意:V2 架构下不再在 getUsageLimits 中同步刷新 token // 如果 token 过期,PoolManager 后台会自动处理 // if (this.isExpiryDateNear()) { // logger.info('[Gemini] Token is near expiry, refreshing before getUsageLimits request...'); // await this.initializeAuth(true); // } try { const modelsWithQuotas = await this.getModelsWithQuotas(); return modelsWithQuotas; } catch (error) { logger.error('[Gemini] Failed to get usage limits:', error.message); throw error; } } /** * 获取带配额信息的模型列表 * @returns {Promise} 模型配额信息 */ async getModelsWithQuotas() { try { // 解析模型配额信息 const result = { lastUpdated: Date.now(), models: {} }; // 调用 retrieveUserQuota 接口获取用户配额信息 try { const quotaURL = `${this.codeAssistEndpoint}/${this.apiVersion}:retrieveUserQuota`; const requestBody = { project: `${this.projectId}` }; const requestOptions = { url: quotaURL, method: 'POST', headers: { 'Content-Type': 'application/json' }, responseType: 'json', body: JSON.stringify(requestBody) }; this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); // logger.info(`[Gemini] retrieveUserQuota success`, JSON.stringify(res.data)); if (res.data && res.data.buckets) { const buckets = res.data.buckets; // 遍历 buckets 数组,提取配额信息 for (const bucket of buckets) { const modelId = bucket.modelId; // 检查模型是否在支持的模型列表中 if (!GEMINI_MODELS.includes(modelId)) continue; const modelInfo = { remaining: bucket.remainingFraction || 0, resetTime: bucket.resetTime || null, resetTimeRaw: bucket.resetTime }; result.models[modelId] = modelInfo; } // 对模型按名称排序 const sortedModels = {}; Object.keys(result.models).sort().forEach(key => { sortedModels[key] = result.models[key]; }); result.models = sortedModels; // logger.info(`[Gemini] Sorted Models:`, sortedModels); logger.info(`[Gemini] Successfully fetched quotas for ${Object.keys(result.models).length} models`); } } catch (fetchError) { logger.error(`[Gemini] Failed to fetch user quota:`, fetchError.message); // 如果 retrieveUserQuota 失败,回退到使用固定模型列表 for (const modelId of GEMINI_MODELS) { result.models[modelId] = { remaining: 0, resetTime: null, resetTimeRaw: null }; } } return result; } catch (error) { logger.error('[Gemini] Failed to get models with quotas:', error.message); throw error; } } }