feat(认证): 添加通过base64编码或文件路径加载OAuth凭证的功能
支持通过--oauth-creds-base64参数传入base64编码的凭证,或通过--oauth-creds-file指定凭证文件路径 更新README文档说明新的启动方式 在响应中添加usageMetadata字段传递token用量信息
This commit is contained in:
parent
0e799798bc
commit
4c2c9bde33
5 changed files with 127 additions and 25 deletions
20
README-EN.md
20
README-EN.md
|
|
@ -115,6 +115,14 @@ This project consists of three core files, each with its own specific function:
|
||||||
```bash
|
```bash
|
||||||
node gemini-api-server.js 0.0.0.0 --port 3001 --api-key your_secret_key --log-prompts file
|
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`)
|
#### 💻 Call the API (Default API Key: `123456`)
|
||||||
* **List Models**
|
* **List Models**
|
||||||
|
|
@ -123,7 +131,7 @@ This project consists of three core files, each with its own specific function:
|
||||||
```
|
```
|
||||||
* **Generate Content (with system prompt)**
|
* **Generate Content (with system prompt)**
|
||||||
```bash
|
```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 "Content-Type: application/json" \
|
||||||
-H "x-goog-api-key: 123456" \
|
-H "x-goog-api-key: 123456" \
|
||||||
-d '{
|
-d '{
|
||||||
|
|
@ -133,7 +141,7 @@ This project consists of three core files, each with its own specific function:
|
||||||
```
|
```
|
||||||
* **Stream Generate Content**
|
* **Stream Generate Content**
|
||||||
```bash
|
```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" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"contents":[{"parts":[{"text":"Write a five-line poem about the universe"}]}]}'
|
-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 "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer sk-your-key" \
|
-H "Authorization: Bearer sk-your-key" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "gemini-1.5-pro-latest",
|
"model": "gemini-2.5-pro",
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": "You are a cat named Neko."},
|
{"role": "system", "content": "You are a cat named Neko."},
|
||||||
{"role": "user", "content": "Hello, what is your name?"}
|
{"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 "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer sk-your-key" \
|
-H "Authorization: Bearer sk-your-key" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "gemini-1.5-flash-latest",
|
"model": "gemini-2.5-flash",
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "Write a five-line poem about the universe"}
|
{"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.
|
* **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.
|
* **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
|
## 📄 License
|
||||||
|
|
@ -204,4 +210,4 @@ This project is licensed under the [**GNU General Public License v3 (GPLv3)**](h
|
||||||
|
|
||||||
## 🙏 Acknowledgements
|
## 🙏 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!
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -115,6 +115,14 @@
|
||||||
```bash
|
```bash
|
||||||
node gemini-api-server.js 0.0.0.0 --port 3001 --api-key your_secret_key --log-prompts file
|
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`)
|
#### 💻 调用 API (默认 API Key: `123456`)
|
||||||
* **列出模型**
|
* **列出模型**
|
||||||
|
|
@ -123,7 +131,7 @@
|
||||||
```
|
```
|
||||||
* **生成内容 (带系统提示)**
|
* **生成内容 (带系统提示)**
|
||||||
```bash
|
```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 "Content-Type: application/json" \
|
||||||
-H "x-goog-api-key: 123456" \
|
-H "x-goog-api-key: 123456" \
|
||||||
-d '{
|
-d '{
|
||||||
|
|
@ -133,7 +141,7 @@
|
||||||
```
|
```
|
||||||
* **流式生成内容**
|
* **流式生成内容**
|
||||||
```bash
|
```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" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"contents":[{"parts":[{"text":"写一首关于宇宙的五行短诗"}]}]}'
|
-d '{"contents":[{"parts":[{"text":"写一首关于宇宙的五行短诗"}]}]}'
|
||||||
```
|
```
|
||||||
|
|
@ -160,7 +168,7 @@
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer sk-your-key" \
|
-H "Authorization: Bearer sk-your-key" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "gemini-1.5-pro-latest",
|
"model": "gemini-2.5-pro",
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": "你是一只名叫 Neko 的猫。"},
|
{"role": "system", "content": "你是一只名叫 Neko 的猫。"},
|
||||||
{"role": "user", "content": "你好,你叫什么名字?"}
|
{"role": "user", "content": "你好,你叫什么名字?"}
|
||||||
|
|
@ -173,7 +181,7 @@
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer sk-your-key" \
|
-H "Authorization: Bearer sk-your-key" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "gemini-1.5-flash-latest",
|
"model": "gemini-2.5-flash",
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "写一首关于宇宙的五行短诗"}
|
{"role": "user", "content": "写一首关于宇宙的五行短诗"}
|
||||||
],
|
],
|
||||||
|
|
@ -194,8 +202,6 @@
|
||||||
* **响应缓存**: 对高频重复问题添加缓存逻辑,降低 API 调用,提升响应速度。
|
* **响应缓存**: 对高频重复问题添加缓存逻辑,降低 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 开发团队的卓越工作表示衷心的感谢!
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@
|
||||||
* // 指定 API Key 和端口 (参数顺序无关)
|
* // 指定 API Key 和端口 (参数顺序无关)
|
||||||
* node gemini-api-server-final.js --api-key your_secret_key --port 3001
|
* 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):
|
* 3. 调用 API 接口 (默认 API Key: 123456):
|
||||||
*
|
*
|
||||||
* // a) 列出可用模型 (GET 请求,密钥在 URL 参数中)
|
* // a) 列出可用模型 (GET 请求,密钥在 URL 参数中)
|
||||||
|
|
@ -98,6 +104,8 @@ const PROMPT_LOG_BASE_NAME = 'prompts';
|
||||||
let PROMPT_LOG_FILENAME = '';
|
let PROMPT_LOG_FILENAME = '';
|
||||||
let REQUIRED_API_KEY = '123456'; // Default API Key
|
let REQUIRED_API_KEY = '123456'; // Default API Key
|
||||||
let SERVER_PORT = 3000; // Default Port
|
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 args = process.argv.slice(2);
|
||||||
const remainingArgs = [];
|
const remainingArgs = [];
|
||||||
|
|
@ -129,6 +137,20 @@ for (let i = 0; i < args.length; i++) {
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Config Warning] --port flag requires a value.`);
|
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 {
|
} else {
|
||||||
remainingArgs.push(args[i]);
|
remainingArgs.push(args[i]);
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +186,7 @@ function isAuthorized(req, requestUrl) {
|
||||||
let apiServiceInstance = null;
|
let apiServiceInstance = null;
|
||||||
async function getApiService() {
|
async function getApiService() {
|
||||||
if (!apiServiceInstance) {
|
if (!apiServiceInstance) {
|
||||||
apiServiceInstance = new GeminiApiService(HOST);
|
apiServiceInstance = new GeminiApiService(HOST, OAUTH_CREDS_BASE64, OAUTH_CREDS_FILE_PATH);
|
||||||
await apiServiceInstance.initialize();
|
await apiServiceInstance.initialize();
|
||||||
} else if (!apiServiceInstance.isInitialized) {
|
} else if (!apiServiceInstance.isInitialized) {
|
||||||
await apiServiceInstance.initialize();
|
await apiServiceInstance.initialize();
|
||||||
|
|
@ -184,6 +206,8 @@ async function handleStreamRequest(res, service, model, requestBody) {
|
||||||
}
|
}
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
res.end();
|
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) {
|
async function handleUnaryRequest(res, service, model, requestBody) {
|
||||||
const response = await service.generateContent(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);
|
const responseString = JSON.stringify(response);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(responseString);
|
res.end(responseString);
|
||||||
|
const expiryDate = service.authClient.credentials.expiry_date;
|
||||||
|
console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`);
|
||||||
}
|
}
|
||||||
function handleError(res, error) {
|
function handleError(res, error) {
|
||||||
console.error('\n[Server] Request failed:', error.stack);
|
console.error('\n[Server] Request failed:', error.stack);
|
||||||
|
|
@ -218,12 +244,12 @@ async function requestHandler(req, res) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const service = await getApiService();
|
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') {
|
if (req.method === 'GET' && requestUrl.pathname === '/v1beta/models') {
|
||||||
const models = await service.listModels();
|
const models = await service.listModels();
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
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));
|
return res.end(JSON.stringify(models));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,6 +290,7 @@ server.listen(SERVER_PORT, HOST, () => {
|
||||||
console.log(` Port: ${SERVER_PORT}`);
|
console.log(` Port: ${SERVER_PORT}`);
|
||||||
console.log(` Required API Key: ${REQUIRED_API_KEY}`);
|
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(` 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(`--------------------------`);
|
||||||
console.log(`\nGemini API Server (Final) running on http://${HOST}:${SERVER_PORT}`);
|
console.log(`\nGemini API Server (Final) running on http://${HOST}:${SERVER_PORT}`);
|
||||||
console.log('Initializing service... This may take a moment.');
|
console.log('Initializing service... This may take a moment.');
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ export function extractResponseText(responseObject) {
|
||||||
function toGeminiApiResponse(codeAssistResponse) {
|
function toGeminiApiResponse(codeAssistResponse) {
|
||||||
if (!codeAssistResponse) return null;
|
if (!codeAssistResponse) return null;
|
||||||
const compliantResponse = { candidates: codeAssistResponse.candidates };
|
const compliantResponse = { candidates: codeAssistResponse.candidates };
|
||||||
|
if (codeAssistResponse.usageMetadata) compliantResponse.usageMetadata = codeAssistResponse.usageMetadata;
|
||||||
if (codeAssistResponse.promptFeedback) compliantResponse.promptFeedback = codeAssistResponse.promptFeedback;
|
if (codeAssistResponse.promptFeedback) compliantResponse.promptFeedback = codeAssistResponse.promptFeedback;
|
||||||
if (codeAssistResponse.automaticFunctionCallingHistory) compliantResponse.automaticFunctionCallingHistory = codeAssistResponse.automaticFunctionCallingHistory;
|
if (codeAssistResponse.automaticFunctionCallingHistory) compliantResponse.automaticFunctionCallingHistory = codeAssistResponse.automaticFunctionCallingHistory;
|
||||||
return compliantResponse;
|
return compliantResponse;
|
||||||
|
|
@ -123,12 +124,14 @@ export async function getRequestBody(req) {
|
||||||
|
|
||||||
// --- Main Service Class ---
|
// --- Main Service Class ---
|
||||||
export class GeminiApiService {
|
export class GeminiApiService {
|
||||||
constructor(host = 'localhost') {
|
constructor(host = 'localhost', oauthCredsBase64 = null, oauthCredsFilePath = null) {
|
||||||
this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET);
|
this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET);
|
||||||
this.projectId = null;
|
this.projectId = null;
|
||||||
this.availableModels = [];
|
this.availableModels = [];
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
|
this.oauthCredsBase64 = oauthCredsBase64;
|
||||||
|
this.oauthCredsFilePath = oauthCredsFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
|
@ -142,7 +145,23 @@ export class GeminiApiService {
|
||||||
|
|
||||||
async initializeAuth(forceRefresh = false) {
|
async initializeAuth(forceRefresh = false) {
|
||||||
if (this.authClient.credentials.access_token && !forceRefresh) return;
|
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 {
|
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);
|
||||||
|
|
@ -157,7 +176,7 @@ export class GeminiApiService {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
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);
|
const newTokens = await this.getNewToken(credPath);
|
||||||
this.authClient.setCredentials(newTokens);
|
this.authClient.setCredentials(newTokens);
|
||||||
console.log('[Auth] New token obtained and loaded into memory.');
|
console.log('[Auth] New token obtained and loaded into memory.');
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,16 @@
|
||||||
* node openai-api-server.js --port 8088 --api-key your_secret_key 0.0.0.0
|
* 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`):
|
* 4. 调用 API 接口 (假设 API Key: `your_secret_key`, 服务运行在 `localhost:8000`):
|
||||||
*
|
*
|
||||||
* - **a) 列出可用模型**
|
* - **a) 列出可用模型**
|
||||||
|
|
@ -115,6 +125,8 @@ const PROMPT_LOG_BASE_NAME = 'prompts';
|
||||||
let PROMPT_LOG_FILENAME = '';
|
let PROMPT_LOG_FILENAME = '';
|
||||||
let REQUIRED_API_KEY = '123456'; // Default API Key
|
let REQUIRED_API_KEY = '123456'; // Default API Key
|
||||||
let SERVER_PORT = 8000; // Default Port
|
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 args = process.argv.slice(2);
|
||||||
const remainingArgs = [];
|
const remainingArgs = [];
|
||||||
|
|
@ -146,6 +158,20 @@ for (let i = 0; i < args.length; i++) {
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Config Warning] --log-prompts flag requires a value.`);
|
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 {
|
} else {
|
||||||
remainingArgs.push(args[i]);
|
remainingArgs.push(args[i]);
|
||||||
}
|
}
|
||||||
|
|
@ -303,8 +329,12 @@ function toOpenAIChatCompletion(geminiResponse, model) {
|
||||||
},
|
},
|
||||||
finish_reason: "stop",
|
finish_reason: "stop",
|
||||||
}],
|
}],
|
||||||
usage: {
|
usage: geminiResponse.usageMetadata ? {
|
||||||
prompt_tokens: 0, // Note: Gemini API doesn't provide token counts
|
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
|
||||||
|
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
|
||||||
|
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0,
|
||||||
|
} : {
|
||||||
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
total_tokens: 0,
|
total_tokens: 0,
|
||||||
},
|
},
|
||||||
|
|
@ -323,6 +353,15 @@ function toOpenAIStreamChunk(geminiChunk, model) {
|
||||||
delta: { content: text },
|
delta: { content: text },
|
||||||
finish_reason: null,
|
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;
|
let apiServiceInstance = null;
|
||||||
async function getApiService() {
|
async function getApiService() {
|
||||||
if (!apiServiceInstance) {
|
if (!apiServiceInstance) {
|
||||||
apiServiceInstance = new GeminiApiService(HOST);
|
apiServiceInstance = new GeminiApiService(HOST, OAUTH_CREDS_BASE64, OAUTH_CREDS_FILE_PATH);
|
||||||
await apiServiceInstance.initialize();
|
await apiServiceInstance.initialize();
|
||||||
} else if (!apiServiceInstance.isInitialized) { // Ensure re-initialization if not already initialized
|
} else if (!apiServiceInstance.isInitialized) { // Ensure re-initialization if not already initialized
|
||||||
await apiServiceInstance.initialize();
|
await apiServiceInstance.initialize();
|
||||||
|
|
@ -396,6 +435,8 @@ async function handleStreamRequest(res, service, model, requestBody) {
|
||||||
res.end();
|
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) {
|
async function handleUnaryRequest(res, service, model, requestBody) {
|
||||||
|
|
@ -409,6 +450,8 @@ async function handleUnaryRequest(res, service, model, requestBody) {
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify(openAIResponse));
|
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) {
|
function handleError(res, error) {
|
||||||
|
|
@ -433,13 +476,13 @@ async function requestHandler(req, res) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const service = await getApiService();
|
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') {
|
if (req.method === 'GET' && requestUrl.pathname === '/v1/models') {
|
||||||
const models = await service.listModels();
|
const models = await service.listModels();
|
||||||
const openAIModels = toOpenAIModelList(models.models.map(m => m.name.replace('models/', '')));
|
const openAIModels = toOpenAIModelList(models.models.map(m => m.name.replace('models/', '')));
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
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));
|
return res.end(JSON.stringify(openAIModels));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -478,6 +521,7 @@ server.listen(SERVER_PORT, HOST, () => {
|
||||||
console.log(` Port: ${SERVER_PORT}`);
|
console.log(` Port: ${SERVER_PORT}`);
|
||||||
console.log(` Required API Key: ${REQUIRED_API_KEY}`);
|
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(` 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(`---------------------------------------------`);
|
||||||
console.log(`\nServer running on http://${HOST}:${SERVER_PORT}`);
|
console.log(`\nServer running on http://${HOST}:${SERVER_PORT}`);
|
||||||
console.log('Initializing backend service... This may take a moment.');
|
console.log('Initializing backend service... This may take a moment.');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue