From 63a7c2514b9b7f2976d0019628a6b69eaf00814a Mon Sep 17 00:00:00 2001 From: shaw Date: Sat, 29 Nov 2025 10:02:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dgemini-api=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E5=85=B1=E4=BA=AB=E6=B1=A0=E6=97=A0=E6=B3=95=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/handlers/geminiHandlers.js | 219 ++++++++++++++++++++++--- src/services/unifiedGeminiScheduler.js | 40 ++++- 2 files changed, 228 insertions(+), 31 deletions(-) diff --git a/src/handlers/geminiHandlers.js b/src/handlers/geminiHandlers.js index 6aa85746..e70fd634 100644 --- a/src/handlers/geminiHandlers.js +++ b/src/handlers/geminiHandlers.js @@ -630,15 +630,22 @@ async function handleModels(req, res) { }) } - // 选择账户获取模型列表 + // 选择账户获取模型列表(允许 API 账户) let account = null + let isApiAccount = false try { const accountSelection = await unifiedGeminiScheduler.selectAccountForApiKey( apiKeyData, null, - null + null, + { allowApiAccounts: true } ) - account = await geminiAccountService.getAccount(accountSelection.accountId) + isApiAccount = accountSelection.accountType === 'gemini-api' + if (isApiAccount) { + account = await geminiApiAccountService.getAccount(accountSelection.accountId) + } else { + account = await geminiAccountService.getAccount(accountSelection.accountId) + } } catch (error) { logger.warn('Failed to select Gemini account for models endpoint:', error) } @@ -659,7 +666,45 @@ async function handleModels(req, res) { } // 获取模型列表 - const models = await getAvailableModels(account.accessToken, account.proxy) + let models + if (isApiAccount) { + // API Key 账户:使用 API Key 获取模型列表 + const proxyConfig = parseProxyConfig(account) + try { + const apiUrl = `${account.baseUrl}/v1beta/models?key=${account.apiKey}` + const axiosConfig = { + method: 'GET', + url: apiUrl, + headers: { 'Content-Type': 'application/json' } + } + if (proxyConfig) { + const proxyHelper = new ProxyHelper() + axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig) + axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig) + } + const response = await axios(axiosConfig) + models = (response.data.models || []).map((m) => ({ + id: m.name?.replace('models/', '') || m.name, + object: 'model', + created: Date.now() / 1000, + owned_by: 'google' + })) + } catch (error) { + logger.warn('Failed to fetch models from Gemini API:', error.message) + // 返回默认模型列表 + models = [ + { + id: 'gemini-2.5-flash', + object: 'model', + created: Date.now() / 1000, + owned_by: 'google' + } + ] + } + } else { + // OAuth 账户:使用 OAuth token 获取模型列表 + models = await getAvailableModels(account.accessToken, account.proxy) + } res.json({ object: 'list', @@ -786,12 +831,36 @@ function handleSimpleEndpoint(apiMethod) { // 从路径参数或请求体中获取模型名 const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash' - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, requestedModel ) + const { accountId, accountType } = schedulerResult + + // v1internal 路由只支持 OAuth 账户,不支持 API Key 账户 + if (accountType === 'gemini-api') { + logger.error( + `❌ v1internal routes do not support Gemini API accounts. Account: ${accountId}` + ) + return res.status(400).json({ + error: { + message: + 'This endpoint only supports Gemini OAuth accounts. Gemini API Key accounts are not compatible with v1internal format.', + type: 'invalid_account_type' + } + }) + } + const account = await geminiAccountService.getAccount(accountId) + if (!account) { + return res.status(404).json({ + error: { + message: 'Gemini account not found', + type: 'account_not_found' + } + }) + } const { accessToken, refreshToken } = account const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' @@ -842,12 +911,34 @@ async function handleLoadCodeAssist(req, res) { // 从路径参数或请求体中获取模型名 const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash' - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, requestedModel ) + const { accountId, accountType } = schedulerResult + + // v1internal 路由只支持 OAuth 账户,不支持 API Key 账户 + if (accountType === 'gemini-api') { + logger.error(`❌ v1internal routes do not support Gemini API accounts. Account: ${accountId}`) + return res.status(400).json({ + error: { + message: + 'This endpoint only supports Gemini OAuth accounts. Gemini API Key accounts are not compatible with v1internal format.', + type: 'invalid_account_type' + } + }) + } + const account = await geminiAccountService.getAccount(accountId) + if (!account) { + return res.status(404).json({ + error: { + message: 'Gemini account not found', + type: 'account_not_found' + } + }) + } const { accessToken, refreshToken, projectId } = account const { metadata, cloudaicompanionProject } = req.body @@ -919,12 +1010,34 @@ async function handleOnboardUser(req, res) { // 从路径参数或请求体中获取模型名 const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash' - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, requestedModel ) + const { accountId, accountType } = schedulerResult + + // v1internal 路由只支持 OAuth 账户,不支持 API Key 账户 + if (accountType === 'gemini-api') { + logger.error(`❌ v1internal routes do not support Gemini API accounts. Account: ${accountId}`) + return res.status(400).json({ + error: { + message: + 'This endpoint only supports Gemini OAuth accounts. Gemini API Key accounts are not compatible with v1internal format.', + type: 'invalid_account_type' + } + }) + } + const account = await geminiAccountService.getAccount(accountId) + if (!account) { + return res.status(404).json({ + error: { + message: 'Gemini account not found', + type: 'account_not_found' + } + }) + } const { accessToken, refreshToken, projectId } = account const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' @@ -1013,31 +1126,93 @@ async function handleCountTokens(req, res) { }) } - // 使用统一调度选择账号 - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + // 使用统一调度选择账号(允许 API 账户) + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, - model - ) - const account = await geminiAccountService.getAccount(accountId) - const { accessToken, refreshToken } = account - - const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' - logger.info(`CountTokens request (${version})`, { model, - contentsLength: contents.length, - apiKeyId: req.apiKey?.id || 'unknown' - }) + { allowApiAccounts: true } + ) + const { accountId, accountType } = schedulerResult + const isApiAccount = accountType === 'gemini-api' + + let account + if (isApiAccount) { + account = await geminiApiAccountService.getAccount(accountId) + } else { + account = await geminiAccountService.getAccount(accountId) + } + + if (!account) { + return res.status(404).json({ + error: { + message: `${isApiAccount ? 'Gemini API' : 'Gemini'} account not found`, + type: 'account_not_found' + } + }) + } + + const version = req.path.includes('v1beta') ? 'v1beta' : 'v1' + logger.info( + `CountTokens request (${version}) - ${isApiAccount ? 'API Key' : 'OAuth'} Account`, + { + model, + contentsLength: contents.length, + accountId, + apiKeyId: req.apiKey?.id || 'unknown' + } + ) // 解析账户的代理配置 const proxyConfig = parseProxyConfig(account) - const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig) - const response = await geminiAccountService.countTokens(client, contents, model, proxyConfig) + let response + if (isApiAccount) { + // API Key 账户:直接使用 API Key 请求 + const modelPath = model.startsWith('models/') ? model : `models/${model}` + const apiUrl = `${account.baseUrl}/v1beta/${modelPath}:countTokens?key=${account.apiKey}` + + const axiosConfig = { + method: 'POST', + url: apiUrl, + data: { contents }, + headers: { 'Content-Type': 'application/json' } + } + + if (proxyConfig) { + const proxyHelper = new ProxyHelper() + axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig) + axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig) + } + + try { + const apiResponse = await axios(axiosConfig) + response = { + totalTokens: apiResponse.data.totalTokens || 0, + totalBillableCharacters: apiResponse.data.totalBillableCharacters || 0, + ...apiResponse.data + } + } catch (error) { + logger.error('Gemini API countTokens request failed:', { + status: error.response?.status, + data: error.response?.data + }) + throw error + } + } else { + // OAuth 账户 + const { accessToken, refreshToken } = account + const client = await geminiAccountService.getOauthClient( + accessToken, + refreshToken, + proxyConfig + ) + response = await geminiAccountService.countTokens(client, contents, model, proxyConfig) + } res.json(response) } catch (error) { - const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' + const version = req.path.includes('v1beta') ? 'v1beta' : 'v1' logger.error(`Error in countTokens endpoint (${version})`, { error: error.message }) res.status(500).json({ error: { diff --git a/src/services/unifiedGeminiScheduler.js b/src/services/unifiedGeminiScheduler.js index 1b71d344..33193501 100644 --- a/src/services/unifiedGeminiScheduler.js +++ b/src/services/unifiedGeminiScheduler.js @@ -19,6 +19,12 @@ class UnifiedGeminiScheduler { return schedulable !== false && schedulable !== 'false' } + // 🔧 辅助方法:检查账户是否激活(兼容字符串和布尔值) + _isActive(isActive) { + // 兼容布尔值 true 和字符串 'true' + return isActive === true || isActive === 'true' + } + // 🎯 统一调度Gemini账号 async selectAccountForApiKey( apiKeyData, @@ -35,7 +41,11 @@ class UnifiedGeminiScheduler { if (apiKeyData.geminiAccountId.startsWith('api:')) { const accountId = apiKeyData.geminiAccountId.replace('api:', '') const boundAccount = await geminiApiAccountService.getAccount(accountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + if ( + boundAccount && + this._isActive(boundAccount.isActive) && + boundAccount.status !== 'error' + ) { logger.info( `🎯 Using bound Gemini-API account: ${boundAccount.name} (${accountId}) for API key ${apiKeyData.name}` ) @@ -68,7 +78,11 @@ class UnifiedGeminiScheduler { // 普通 Gemini OAuth 专属账户 else { const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + if ( + boundAccount && + this._isActive(boundAccount.isActive) && + boundAccount.status !== 'error' + ) { logger.info( `🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}` ) @@ -184,7 +198,11 @@ class UnifiedGeminiScheduler { if (apiKeyData.geminiAccountId.startsWith('api:')) { const accountId = apiKeyData.geminiAccountId.replace('api:', '') const boundAccount = await geminiApiAccountService.getAccount(accountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + if ( + boundAccount && + this._isActive(boundAccount.isActive) && + boundAccount.status !== 'error' + ) { const isRateLimited = await this.isAccountRateLimited(accountId) if (!isRateLimited) { // 检查模型支持 @@ -231,7 +249,11 @@ class UnifiedGeminiScheduler { // 普通 Gemini OAuth 账户 else if (!apiKeyData.geminiAccountId.startsWith('group:')) { const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + if ( + boundAccount && + this._isActive(boundAccount.isActive) && + boundAccount.status !== 'error' + ) { const isRateLimited = await this.isAccountRateLimited(boundAccount.id) if (!isRateLimited) { // 检查模型支持 @@ -276,7 +298,7 @@ class UnifiedGeminiScheduler { const geminiAccounts = await geminiAccountService.getAllAccounts() for (const account of geminiAccounts) { if ( - account.isActive === 'true' && + this._isActive(account.isActive) && account.status !== 'error' && (account.accountType === 'shared' || !account.accountType) && // 兼容旧数据 this._isSchedulable(account.schedulable) @@ -326,7 +348,7 @@ class UnifiedGeminiScheduler { const geminiApiAccounts = await geminiApiAccountService.getAllAccounts() for (const account of geminiApiAccounts) { if ( - account.isActive === 'true' && + this._isActive(account.isActive) && account.status !== 'error' && (account.accountType === 'shared' || !account.accountType) && this._isSchedulable(account.schedulable) @@ -386,7 +408,7 @@ class UnifiedGeminiScheduler { try { if (accountType === 'gemini') { const account = await geminiAccountService.getAccount(accountId) - if (!account || account.isActive !== 'true' || account.status === 'error') { + if (!account || !this._isActive(account.isActive) || account.status === 'error') { return false } // 检查是否可调度 @@ -397,7 +419,7 @@ class UnifiedGeminiScheduler { return !(await this.isAccountRateLimited(accountId)) } else if (accountType === 'gemini-api') { const account = await geminiApiAccountService.getAccount(accountId) - if (!account || account.isActive !== 'true' || account.status === 'error') { + if (!account || !this._isActive(account.isActive) || account.status === 'error') { return false } // 检查是否可调度 @@ -643,7 +665,7 @@ class UnifiedGeminiScheduler { // 检查账户是否可用 if ( - account.isActive === 'true' && + this._isActive(account.isActive) && account.status !== 'error' && this._isSchedulable(account.schedulable) ) {