feat(认证): 添加通过base64编码或文件路径加载OAuth凭证的功能

支持通过--oauth-creds-base64参数传入base64编码的凭证,或通过--oauth-creds-file指定凭证文件路径
更新README文档说明新的启动方式
在响应中添加usageMetadata字段传递token用量信息
This commit is contained in:
hex2077 2025-07-20 20:40:15 +08:00
parent 0e799798bc
commit 4c2c9bde33
5 changed files with 127 additions and 25 deletions

View file

@ -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!

View file

@ -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 官方团队的卓越工作表示衷心的感谢!
本项目的开发受到了官方 Google Gemini CLI 的极大启发,并参考了Cline 3.18.0 版本 `gemini-cli.ts` 的部分代码实现。在此对 Google 官方团队和 Cline 开发团队的卓越工作表示衷心的感谢!

View file

@ -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.');

View file

@ -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.');

View file

@ -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.');