From 4c2c9bde33e53dd6069855fb9d0120d98bc8c097 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Sun, 20 Jul 2025 20:40:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=A4=E8=AF=81):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=9A=E8=BF=87base64=E7=BC=96=E7=A0=81=E6=88=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=B7=AF=E5=BE=84=E5=8A=A0=E8=BD=BDOAuth=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持通过--oauth-creds-base64参数传入base64编码的凭证,或通过--oauth-creds-file指定凭证文件路径 更新README文档说明新的启动方式 在响应中添加usageMetadata字段传递token用量信息 --- README-EN.md | 20 ++++++++++------ README.md | 20 ++++++++++------ gemini-api-server.js | 33 ++++++++++++++++++++++++--- gemini-core.js | 25 +++++++++++++++++--- openai-api-server.js | 54 ++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 127 insertions(+), 25 deletions(-) diff --git a/README-EN.md b/README-EN.md index a7d84fa..78a350b 100644 --- a/README-EN.md +++ b/README-EN.md @@ -115,6 +115,14 @@ This project consists of three core files, each with its own specific function: ```bash node gemini-api-server.js 0.0.0.0 --port 3001 --api-key your_secret_key --log-prompts file ``` +* **Start with Base64 Encoded Credentials** (e.g., for Docker or CI/CD environments) + ```bash + node gemini-api-server.js --oauth-creds-base64 "YOUR_BASE64_ENCODED_OAUTH_CREDS_JSON" + ``` +* **Start with Specified Credential File Path** (e.g., for custom credential location) + ```bash + node gemini-api-server.js --oauth-creds-file "/path/to/your/oauth_creds.json" + ``` #### 💻 Call the API (Default API Key: `123456`) * **List Models** @@ -123,7 +131,7 @@ This project consists of three core files, each with its own specific function: ``` * **Generate Content (with system prompt)** ```bash - curl "http://localhost:3000/v1beta/models/gemini-1.5-pro-latest:generateContent" \ + curl "http://localhost:3000/v1beta/models/gemini-2.5-pro:generateContent" \ -H "Content-Type: application/json" \ -H "x-goog-api-key: 123456" \ -d '{ @@ -133,7 +141,7 @@ This project consists of three core files, each with its own specific function: ``` * **Stream Generate Content** ```bash - curl "http://localhost:3000/v1beta/models/gemini-1.5-flash-latest:streamGenerateContent?key=123456" \ + curl "http://localhost:3000/v1beta/models/gemini-2.5-flash:streamGenerateContent?key=123456" \ -H "Content-Type: application/json" \ -d '{"contents":[{"parts":[{"text":"Write a five-line poem about the universe"}]}]}' ``` @@ -160,7 +168,7 @@ This project consists of three core files, each with its own specific function: -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-key" \ -d '{ - "model": "gemini-1.5-pro-latest", + "model": "gemini-2.5-pro", "messages": [ {"role": "system", "content": "You are a cat named Neko."}, {"role": "user", "content": "Hello, what is your name?"} @@ -173,7 +181,7 @@ This project consists of three core files, each with its own specific function: -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-key" \ -d '{ - "model": "gemini-1.5-flash-latest", + "model": "gemini-2.5-flash", "messages": [ {"role": "user", "content": "Write a five-line poem about the universe"} ], @@ -194,8 +202,6 @@ This project consists of three core files, each with its own specific function: * **Response Caching**: Add caching logic for frequently repeated questions to reduce API calls and improve response speed. * **Custom Content Filtering**: Add keyword filtering or content review logic before requests are sent or returned to meet compliance requirements. -* **⚖️ Multi-Account Load Balancing (Advanced Usage)**: Run multiple instances of `GeminiCli2API` (each authorized with a different Google account), and then use a load balancer like [gemini-balance](https://github.com/snailyp/gemini-balance/) to achieve load balancing. This can create a huge shared pool of free quota, ideal for teams or high-request scenarios. - --- ## 📄 License @@ -204,4 +210,4 @@ This project is licensed under the [**GNU General Public License v3 (GPLv3)**](h ## 🙏 Acknowledgements -The development of this project was greatly inspired by the official Google Gemini CLI, and it references some of the code implementation from its `gemini-cli.ts` (Cline 3.18.0 version). Sincere thanks to the official Google team for their excellent work! +The development of this project was greatly inspired by the official Google Gemini CLI, and referenced some code implementations of Cline 3.18.0 version `gemini-cli.ts`. I would like to express my sincere gratitude to the Google official team and the Cline development team for their excellent work! diff --git a/README.md b/README.md index e3d0f64..ab15bbc 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,14 @@ ```bash node gemini-api-server.js 0.0.0.0 --port 3001 --api-key your_secret_key --log-prompts file ``` +* **通过 base64 编码的凭证启动** (例如,用于 Docker 或 CI/CD 环境) + ```bash + node gemini-api-server.js --oauth-creds-base64 "YOUR_BASE64_ENCODED_OAUTH_CREDS_JSON" + ``` +* **通过指定凭证文件路径启动** (例如,用于自定义凭证位置) + ```bash + node gemini-api-server.js --oauth-creds-file "/path/to/your/oauth_creds.json" + ``` #### 💻 调用 API (默认 API Key: `123456`) * **列出模型** @@ -123,7 +131,7 @@ ``` * **生成内容 (带系统提示)** ```bash - curl "http://localhost:3000/v1beta/models/gemini-1.5-pro-latest:generateContent" \ + curl "http://localhost:3000/v1beta/models/gemini-2.5-pro:generateContent" \ -H "Content-Type: application/json" \ -H "x-goog-api-key: 123456" \ -d '{ @@ -133,7 +141,7 @@ ``` * **流式生成内容** ```bash - curl "http://localhost:3000/v1beta/models/gemini-1.5-flash-latest:streamGenerateContent?key=123456" \ + curl "http://localhost:3000/v1beta/models/gemini-2.5-flash:streamGenerateContent?key=123456" \ -H "Content-Type: application/json" \ -d '{"contents":[{"parts":[{"text":"写一首关于宇宙的五行短诗"}]}]}' ``` @@ -160,7 +168,7 @@ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-key" \ -d '{ - "model": "gemini-1.5-pro-latest", + "model": "gemini-2.5-pro", "messages": [ {"role": "system", "content": "你是一只名叫 Neko 的猫。"}, {"role": "user", "content": "你好,你叫什么名字?"} @@ -173,7 +181,7 @@ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-key" \ -d '{ - "model": "gemini-1.5-flash-latest", + "model": "gemini-2.5-flash", "messages": [ {"role": "user", "content": "写一首关于宇宙的五行短诗"} ], @@ -194,8 +202,6 @@ * **响应缓存**: 对高频重复问题添加缓存逻辑,降低 API 调用,提升响应速度。 * **自定义内容过滤**: 在请求发送或返回前增加关键词过滤或内容审查逻辑,满足合规要求。 -* **⚖️ 多账号负载均衡 (高级用法)**: 运行多个 `GeminiCli2API` 实例(每个使用不同 Google 账号授权),再通过[gemini-balance](https://github.com/snailyp/gemini-balance/) 等实现负载均衡。这能创建一个巨大的共享免费额度池,非常适合团队或高请求量场景。 - --- ## 📄 开源许可 @@ -204,4 +210,4 @@ ## 🙏 致谢 -本项目的开发受到了官方 Google Gemini CLI 的极大启发,并参考了其 `gemini-cli.ts` (Cline 3.18.0 版本) 的部分代码实现。在此对 Google 官方团队的卓越工作表示衷心的感谢! \ No newline at end of file +本项目的开发受到了官方 Google Gemini CLI 的极大启发,并参考了Cline 3.18.0 版本 `gemini-cli.ts` 的部分代码实现。在此对 Google 官方团队和 Cline 开发团队的卓越工作表示衷心的感谢! diff --git a/gemini-api-server.js b/gemini-api-server.js index cf42cd9..08e2165 100644 --- a/gemini-api-server.js +++ b/gemini-api-server.js @@ -51,6 +51,12 @@ * // 指定 API Key 和端口 (参数顺序无关) * node gemini-api-server-final.js --api-key your_secret_key --port 3001 * + * // 通过 base64 编码的凭证启动 (例如,用于 Docker 或 CI/CD 环境) + * node gemini-api-server.js --oauth-creds-base64 "YOUR_BASE64_ENCODED_OAUTH_CREDS_JSON" + * + * // 通过指定凭证文件路径启动 (例如,用于自定义凭证位置) + * node gemini-api-server.js --oauth-creds-file "/path/to/your/oauth_creds.json" + * * 3. 调用 API 接口 (默认 API Key: 123456): * * // a) 列出可用模型 (GET 请求,密钥在 URL 参数中) @@ -98,6 +104,8 @@ const PROMPT_LOG_BASE_NAME = 'prompts'; let PROMPT_LOG_FILENAME = ''; let REQUIRED_API_KEY = '123456'; // Default API Key let SERVER_PORT = 3000; // Default Port +let OAUTH_CREDS_BASE64 = null; // New variable for base64 encoded OAuth credentials +let OAUTH_CREDS_FILE_PATH = null; // New variable for OAuth credentials file path const args = process.argv.slice(2); const remainingArgs = []; @@ -129,6 +137,20 @@ for (let i = 0; i < args.length; i++) { } else { console.warn(`[Config Warning] --port flag requires a value.`); } + } else if (args[i] === '--oauth-creds-base64') { + if (i + 1 < args.length) { + OAUTH_CREDS_BASE64 = args[i + 1]; + i++; // Skip the value + } else { + console.warn(`[Config Warning] --oauth-creds-base64 flag requires a value.`); + } + } else if (args[i] === '--oauth-creds-file') { + if (i + 1 < args.length) { + OAUTH_CREDS_FILE_PATH = args[i + 1]; + i++; // Skip the value + } else { + console.warn(`[Config Warning] --oauth-creds-file flag requires a value.`); + } } else { remainingArgs.push(args[i]); } @@ -164,7 +186,7 @@ function isAuthorized(req, requestUrl) { let apiServiceInstance = null; async function getApiService() { if (!apiServiceInstance) { - apiServiceInstance = new GeminiApiService(HOST); + apiServiceInstance = new GeminiApiService(HOST, OAUTH_CREDS_BASE64, OAUTH_CREDS_FILE_PATH); await apiServiceInstance.initialize(); } else if (!apiServiceInstance.isInitialized) { await apiServiceInstance.initialize(); @@ -184,6 +206,8 @@ async function handleStreamRequest(res, service, model, requestBody) { } process.stdout.write('\n'); res.end(); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); } async function handleUnaryRequest(res, service, model, requestBody) { const response = await service.generateContent(model, requestBody); @@ -195,6 +219,8 @@ async function handleUnaryRequest(res, service, model, requestBody) { const responseString = JSON.stringify(response); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(responseString); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); } function handleError(res, error) { console.error('\n[Server] Request failed:', error.stack); @@ -218,12 +244,12 @@ async function requestHandler(req, res) { try { const service = await getApiService(); - const expiryDate = service.authClient.credentials.expiry_date; - console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); if (req.method === 'GET' && requestUrl.pathname === '/v1beta/models') { const models = await service.listModels(); res.writeHead(200, { 'Content-Type': 'application/json' }); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); return res.end(JSON.stringify(models)); } @@ -264,6 +290,7 @@ server.listen(SERVER_PORT, HOST, () => { console.log(` Port: ${SERVER_PORT}`); console.log(` Required API Key: ${REQUIRED_API_KEY}`); console.log(` Prompt Logging: ${PROMPT_LOG_MODE}${PROMPT_LOG_MODE === 'file' ? ` (to ${PROMPT_LOG_FILENAME})` : ''}`); + console.log(` OAuth Creds File Path: ${OAUTH_CREDS_FILE_PATH || 'Default'}`); console.log(`--------------------------`); console.log(`\nGemini API Server (Final) running on http://${HOST}:${SERVER_PORT}`); console.log('Initializing service... This may take a moment.'); diff --git a/gemini-core.js b/gemini-core.js index 8087f4e..01ace83 100644 --- a/gemini-core.js +++ b/gemini-core.js @@ -101,6 +101,7 @@ export function extractResponseText(responseObject) { 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; @@ -123,12 +124,14 @@ export async function getRequestBody(req) { // --- Main Service Class --- export class GeminiApiService { - constructor(host = 'localhost') { + constructor(host = 'localhost', oauthCredsBase64 = null, oauthCredsFilePath = null) { this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET); this.projectId = null; this.availableModels = []; this.isInitialized = false; this.host = host; + this.oauthCredsBase64 = oauthCredsBase64; + this.oauthCredsFilePath = oauthCredsFilePath; } async initialize() { @@ -142,7 +145,23 @@ export class GeminiApiService { async initializeAuth(forceRefresh = false) { if (this.authClient.credentials.access_token && !forceRefresh) return; - const credPath = path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE); + + if (this.oauthCredsBase64) { + try { + const decoded = Buffer.from(this.oauthCredsBase64, 'base64').toString('utf8'); + const credentials = JSON.parse(decoded); + this.authClient.setCredentials(credentials); + console.log('[Auth] Authentication configured successfully from base64 string.'); + // If using base64, we don't refresh and save to file automatically + // as the source of truth is the provided string. + return; + } catch (error) { + console.error('[Auth] Failed to parse base64 OAuth credentials:', error); + throw new Error(`Failed to load OAuth credentials from base64 string.`); + } + } + + 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); @@ -157,7 +176,7 @@ export class GeminiApiService { } } catch (error) { if (error.code === 'ENOENT') { - console.log(`[Auth] '${CREDENTIALS_FILE}' not found. Starting new authentication flow...`); + console.log(`[Auth] Credentials file '${credPath}' not found. Starting new authentication flow...`); const newTokens = await this.getNewToken(credPath); this.authClient.setCredentials(newTokens); console.log('[Auth] New token obtained and loaded into memory.'); diff --git a/openai-api-server.js b/openai-api-server.js index 150b668..95b1230 100644 --- a/openai-api-server.js +++ b/openai-api-server.js @@ -61,6 +61,16 @@ * node openai-api-server.js --port 8088 --api-key your_secret_key 0.0.0.0 * ``` * + * - **通过 base64 编码的凭证启动** (例如,用于 Docker 或 CI/CD 环境) + * ```bash + * node openai-api-server.js --oauth-creds-base64 "YOUR_BASE64_ENCODED_OAUTH_CREDS_JSON" + * ``` + * + * - **通过指定凭证文件路径启动** (例如,用于自定义凭证位置) + * ```bash + * node openai-api-server.js --oauth-creds-file "/path/to/your/oauth_creds.json" + * ``` + * * 4. 调用 API 接口 (假设 API Key: `your_secret_key`, 服务运行在 `localhost:8000`): * * - **a) 列出可用模型** @@ -115,6 +125,8 @@ const PROMPT_LOG_BASE_NAME = 'prompts'; let PROMPT_LOG_FILENAME = ''; let REQUIRED_API_KEY = '123456'; // Default API Key let SERVER_PORT = 8000; // Default Port +let OAUTH_CREDS_BASE64 = null; // New variable for base64 encoded OAuth credentials +let OAUTH_CREDS_FILE_PATH = null; // New variable for OAuth credentials file path const args = process.argv.slice(2); const remainingArgs = []; @@ -146,6 +158,20 @@ for (let i = 0; i < args.length; i++) { } else { console.warn(`[Config Warning] --log-prompts flag requires a value.`); } + } else if (args[i] === '--oauth-creds-base64') { + if (i + 1 < args.length) { + OAUTH_CREDS_BASE64 = args[i + 1]; + i++; // Skip the value + } else { + console.warn(`[Config Warning] --oauth-creds-base64 flag requires a value.`); + } + } else if (args[i] === '--oauth-creds-file') { + if (i + 1 < args.length) { + OAUTH_CREDS_FILE_PATH = args[i + 1]; + i++; // Skip the value + } else { + console.warn(`[Config Warning] --oauth-creds-file flag requires a value.`); + } } else { remainingArgs.push(args[i]); } @@ -303,8 +329,12 @@ function toOpenAIChatCompletion(geminiResponse, model) { }, finish_reason: "stop", }], - usage: { - prompt_tokens: 0, // Note: Gemini API doesn't provide token counts + usage: geminiResponse.usageMetadata ? { + prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0, + completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0, + } : { + prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, }, @@ -323,6 +353,15 @@ function toOpenAIStreamChunk(geminiChunk, model) { delta: { content: text }, finish_reason: null, }], + usage: geminiChunk.usageMetadata ? { + prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0, + completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0, + total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0, + } : { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, }; } @@ -359,7 +398,7 @@ function isAuthorized(req, requestUrl) { let apiServiceInstance = null; async function getApiService() { if (!apiServiceInstance) { - apiServiceInstance = new GeminiApiService(HOST); + apiServiceInstance = new GeminiApiService(HOST, OAUTH_CREDS_BASE64, OAUTH_CREDS_FILE_PATH); await apiServiceInstance.initialize(); } else if (!apiServiceInstance.isInitialized) { // Ensure re-initialization if not already initialized await apiServiceInstance.initialize(); @@ -396,6 +435,8 @@ async function handleStreamRequest(res, service, model, requestBody) { res.end(); } } + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); } async function handleUnaryRequest(res, service, model, requestBody) { @@ -409,6 +450,8 @@ async function handleUnaryRequest(res, service, model, requestBody) { process.stdout.write('\n'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(openAIResponse)); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); } function handleError(res, error) { @@ -433,13 +476,13 @@ async function requestHandler(req, res) { try { const service = await getApiService(); - const expiryDate = service.authClient.credentials.expiry_date; - console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); if (req.method === 'GET' && requestUrl.pathname === '/v1/models') { const models = await service.listModels(); const openAIModels = toOpenAIModelList(models.models.map(m => m.name.replace('models/', ''))); res.writeHead(200, { 'Content-Type': 'application/json' }); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); return res.end(JSON.stringify(openAIModels)); } @@ -478,6 +521,7 @@ server.listen(SERVER_PORT, HOST, () => { console.log(` Port: ${SERVER_PORT}`); console.log(` Required API Key: ${REQUIRED_API_KEY}`); console.log(` Prompt Logging: ${PROMPT_LOG_MODE}${PROMPT_LOG_MODE === 'file' ? ` (to ${PROMPT_LOG_FILENAME})` : ''}`); + console.log(` OAuth Creds File Path: ${OAUTH_CREDS_FILE_PATH || 'Default'}`); console.log(`---------------------------------------------`); console.log(`\nServer running on http://${HOST}:${SERVER_PORT}`); console.log('Initializing backend service... This may take a moment.');