mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
feat(gemini): add Gemini API key endpoints
This commit is contained in:
@@ -95,7 +95,7 @@ If a plaintext key is detected in the config at startup, it will be bcrypt‑has
|
|||||||
```
|
```
|
||||||
- Response:
|
- Response:
|
||||||
```json
|
```json
|
||||||
{"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"generative-language-api-key":["AI...01","AI...02","AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api","proxy-url":"socks5://proxy.example.com:1080","models":[{"name":"claude-3-5-sonnet-20241022","alias":"claude-sonnet-latest"}]},{"api-key":"cr...e3","base-url":"http://example.com:3000/api","proxy-url":""},{"api-key":"sk-...q2","base-url":"https://example.com","proxy-url":""}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1","proxy-url":""}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk...01","proxy-url":""}],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-key-entries":[{"api-key":"sk...7e","proxy-url":"socks5://proxy.example.com:1080"}],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]}
|
{"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"gemini-api-key":[{"api-key":"AI...01","base-url":"https://generativelanguage.googleapis.com","headers":{"X-Custom-Header":"custom-value"},"proxy-url":""},{"api-key":"AI...02","proxy-url":"socks5://proxy.example.com:1080"}],"generative-language-api-key":["AI...01","AI...02","AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api","proxy-url":"socks5://proxy.example.com:1080","models":[{"name":"claude-3-5-sonnet-20241022","alias":"claude-sonnet-latest"}]},{"api-key":"cr...e3","base-url":"http://example.com:3000/api","proxy-url":""},{"api-key":"sk-...q2","base-url":"https://example.com","proxy-url":""}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1","proxy-url":""}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk...01","proxy-url":""}],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-key-entries":[{"api-key":"sk...7e","proxy-url":"socks5://proxy.example.com:1080"}],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debug
|
### Debug
|
||||||
@@ -283,7 +283,69 @@ These endpoints update the inline `config-api-key` provider inside the `auth.pro
|
|||||||
{ "status": "ok" }
|
{ "status": "ok" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gemini API Key (Generative Language)
|
### Gemini API Key
|
||||||
|
- GET `/gemini-api-key`
|
||||||
|
- Request:
|
||||||
|
```bash
|
||||||
|
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"gemini-api-key": [
|
||||||
|
{"api-key":"AIzaSy...01","base-url":"https://generativelanguage.googleapis.com","headers":{"X-Custom-Header":"custom-value"},"proxy-url":""},
|
||||||
|
{"api-key":"AIzaSy...02","proxy-url":"socks5://proxy.example.com:1080"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- PUT `/gemini-api-key`
|
||||||
|
- Request (array form):
|
||||||
|
```bash
|
||||||
|
curl -X PUT -H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer <MANAGEMENT_KEY>' \
|
||||||
|
-d '[{"api-key":"AIzaSy-1","headers":{"X-Custom-Header":"vendor-value"}},{"api-key":"AIzaSy-2","base-url":"https://custom.example.com"}]' \
|
||||||
|
http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- Response:
|
||||||
|
```json
|
||||||
|
{ "status": "ok" }
|
||||||
|
```
|
||||||
|
- PATCH `/gemini-api-key`
|
||||||
|
- Request (update by index):
|
||||||
|
```bash
|
||||||
|
curl -X PATCH -H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer <MANAGEMENT_KEY>' \
|
||||||
|
-d '{"index":0,"value":{"api-key":"AIzaSy-1","base-url":"https://custom.example.com","headers":{"X-Custom-Header":"custom-value"},"proxy-url":""}}' \
|
||||||
|
http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- Request (update by api-key match):
|
||||||
|
```bash
|
||||||
|
curl -X PATCH -H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer <MANAGEMENT_KEY>' \
|
||||||
|
-d '{"match":"AIzaSy-1","value":{"api-key":"AIzaSy-1","headers":{"X-Custom-Header":"custom-value"},"proxy-url":"socks5://proxy.example.com:1080"}}' \
|
||||||
|
http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- Response:
|
||||||
|
```json
|
||||||
|
{ "status": "ok" }
|
||||||
|
```
|
||||||
|
- DELETE `/gemini-api-key`
|
||||||
|
- Request (by api-key):
|
||||||
|
```bash
|
||||||
|
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' -X DELETE \
|
||||||
|
'http://localhost:8317/v0/management/gemini-api-key?api-key=AIzaSy-1'
|
||||||
|
```
|
||||||
|
- Request (by index):
|
||||||
|
```bash
|
||||||
|
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' -X DELETE \
|
||||||
|
'http://localhost:8317/v0/management/gemini-api-key?index=0'
|
||||||
|
```
|
||||||
|
- Response:
|
||||||
|
```json
|
||||||
|
{ "status": "ok" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generative Language API Key (Legacy)
|
||||||
- GET `/generative-language-api-key`
|
- GET `/generative-language-api-key`
|
||||||
- Request:
|
- Request:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
```
|
```
|
||||||
- 响应:
|
- 响应:
|
||||||
```json
|
```json
|
||||||
{"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"generative-language-api-key":["AI...01","AI...02","AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api","proxy-url":"socks5://proxy.example.com:1080","models":[{"name":"claude-3-5-sonnet-20241022","alias":"claude-sonnet-latest"}]},{"api-key":"cr...e3","base-url":"http://example.com:3000/api","proxy-url":""},{"api-key":"sk-...q2","base-url":"https://example.com","proxy-url":""}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1","proxy-url":""}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk...01","proxy-url":""}],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-key-entries":[{"api-key":"sk...7e","proxy-url":"socks5://proxy.example.com:1080"}],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]}
|
{"debug":true,"proxy-url":"","api-keys":["1...5","JS...W"],"quota-exceeded":{"switch-project":true,"switch-preview-model":true},"gemini-api-key":[{"api-key":"AI...01","base-url":"https://generativelanguage.googleapis.com","headers":{"X-Custom-Header":"custom-value"},"proxy-url":""},{"api-key":"AI...02","proxy-url":"socks5://proxy.example.com:1080"}],"generative-language-api-key":["AI...01","AI...02","AI...03"],"request-log":true,"request-retry":3,"claude-api-key":[{"api-key":"cr...56","base-url":"https://example.com/api","proxy-url":"socks5://proxy.example.com:1080","models":[{"name":"claude-3-5-sonnet-20241022","alias":"claude-sonnet-latest"}]},{"api-key":"cr...e3","base-url":"http://example.com:3000/api","proxy-url":""},{"api-key":"sk-...q2","base-url":"https://example.com","proxy-url":""}],"codex-api-key":[{"api-key":"sk...01","base-url":"https://example/v1","proxy-url":""}],"openai-compatibility":[{"name":"openrouter","base-url":"https://openrouter.ai/api/v1","api-key-entries":[{"api-key":"sk...01","proxy-url":""}],"models":[{"name":"moonshotai/kimi-k2:free","alias":"kimi-k2"}]},{"name":"iflow","base-url":"https://apis.iflow.cn/v1","api-key-entries":[{"api-key":"sk...7e","proxy-url":"socks5://proxy.example.com:1080"}],"models":[{"name":"deepseek-v3.1","alias":"deepseek-v3.1"},{"name":"glm-4.5","alias":"glm-4.5"},{"name":"kimi-k2","alias":"kimi-k2"}]}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debug
|
### Debug
|
||||||
@@ -283,7 +283,69 @@
|
|||||||
{ "status": "ok" }
|
{ "status": "ok" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gemini API Key(生成式语言)
|
### Gemini API Key
|
||||||
|
- GET `/gemini-api-key`
|
||||||
|
- 请求:
|
||||||
|
```bash
|
||||||
|
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- 响应:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"gemini-api-key": [
|
||||||
|
{"api-key":"AIzaSy...01","base-url":"https://generativelanguage.googleapis.com","headers":{"X-Custom-Header":"custom-value"},"proxy-url":""},
|
||||||
|
{"api-key":"AIzaSy...02","proxy-url":"socks5://proxy.example.com:1080"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- PUT `/gemini-api-key`
|
||||||
|
- 请求(数组形式):
|
||||||
|
```bash
|
||||||
|
curl -X PUT -H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer <MANAGEMENT_KEY>' \
|
||||||
|
-d '[{"api-key":"AIzaSy-1","headers":{"X-Custom-Header":"vendor-value"}},{"api-key":"AIzaSy-2","base-url":"https://custom.example.com"}]' \
|
||||||
|
http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- 响应:
|
||||||
|
```json
|
||||||
|
{ "status": "ok" }
|
||||||
|
```
|
||||||
|
- PATCH `/gemini-api-key`
|
||||||
|
- 请求(按索引更新):
|
||||||
|
```bash
|
||||||
|
curl -X PATCH -H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer <MANAGEMENT_KEY>' \
|
||||||
|
-d '{"index":0,"value":{"api-key":"AIzaSy-1","base-url":"https://custom.example.com","headers":{"X-Custom-Header":"custom-value"},"proxy-url":""}}' \
|
||||||
|
http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- 请求(按 api-key 匹配更新):
|
||||||
|
```bash
|
||||||
|
curl -X PATCH -H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer <MANAGEMENT_KEY>' \
|
||||||
|
-d '{"match":"AIzaSy-1","value":{"api-key":"AIzaSy-1","headers":{"X-Custom-Header":"custom-value"},"proxy-url":"socks5://proxy.example.com:1080"}}' \
|
||||||
|
http://localhost:8317/v0/management/gemini-api-key
|
||||||
|
```
|
||||||
|
- 响应:
|
||||||
|
```json
|
||||||
|
{ "status": "ok" }
|
||||||
|
```
|
||||||
|
- DELETE `/gemini-api-key`
|
||||||
|
- 请求(按 api-key 删除):
|
||||||
|
```bash
|
||||||
|
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' -X DELETE \
|
||||||
|
'http://localhost:8317/v0/management/gemini-api-key?api-key=AIzaSy-1'
|
||||||
|
```
|
||||||
|
- 请求(按索引删除):
|
||||||
|
```bash
|
||||||
|
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' -X DELETE \
|
||||||
|
'http://localhost:8317/v0/management/gemini-api-key?index=0'
|
||||||
|
```
|
||||||
|
- 响应:
|
||||||
|
```json
|
||||||
|
{ "status": "ok" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generative Language API Key(兼容接口)
|
||||||
- GET `/generative-language-api-key`
|
- GET `/generative-language-api-key`
|
||||||
- 请求:
|
- 请求:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -321,7 +321,12 @@ The server uses a YAML configuration file (`config.yaml`) located in the project
|
|||||||
| `logging-to-file` | boolean | true | Write application logs to rotating files instead of stdout. Set to `false` to log to stdout/stderr. |
|
| `logging-to-file` | boolean | true | Write application logs to rotating files instead of stdout. Set to `false` to log to stdout/stderr. |
|
||||||
| `usage-statistics-enabled` | boolean | true | Enable in-memory usage aggregation for management APIs. Disable to drop all collected usage metrics. |
|
| `usage-statistics-enabled` | boolean | true | Enable in-memory usage aggregation for management APIs. Disable to drop all collected usage metrics. |
|
||||||
| `api-keys` | string[] | [] | Legacy shorthand for inline API keys. Values are mirrored into the `config-api-key` provider for backwards compatibility. |
|
| `api-keys` | string[] | [] | Legacy shorthand for inline API keys. Values are mirrored into the `config-api-key` provider for backwards compatibility. |
|
||||||
| `generative-language-api-key` | string[] | [] | List of Generative Language API keys. |
|
| `gemini-api-key` | object[] | [] | Gemini API key entries with optional per-key `base-url` and `proxy-url` overrides. |
|
||||||
|
| `gemini-api-key.*.api-key` | string | "" | Gemini API key. |
|
||||||
|
| `gemini-api-key.*.base-url` | string | "" | Optional Gemini API endpoint override. |
|
||||||
|
| `gemini-api-key.*.headers` | object | {} | Optional extra HTTP headers sent to the overridden Gemini endpoint only. |
|
||||||
|
| `gemini-api-key.*.proxy-url` | string | "" | Optional per-key proxy override for the Gemini API key. |
|
||||||
|
| `generative-language-api-key` | string[] | [] | (Legacy) List of Generative Language API keys without per-key overrides. |
|
||||||
| `codex-api-key` | object | {} | List of Codex API keys. |
|
| `codex-api-key` | object | {} | List of Codex API keys. |
|
||||||
| `codex-api-key.api-key` | string | "" | Codex API key. |
|
| `codex-api-key.api-key` | string | "" | Codex API key. |
|
||||||
| `codex-api-key.base-url` | string | "" | Custom Codex API endpoint, if you use a third-party API endpoint. |
|
| `codex-api-key.base-url` | string | "" | Custom Codex API endpoint, if you use a third-party API endpoint. |
|
||||||
@@ -394,12 +399,18 @@ quota-exceeded:
|
|||||||
switch-project: true # Whether to automatically switch to another project when a quota is exceeded
|
switch-project: true # Whether to automatically switch to another project when a quota is exceeded
|
||||||
switch-preview-model: true # Whether to automatically switch to a preview model when a quota is exceeded
|
switch-preview-model: true # Whether to automatically switch to a preview model when a quota is exceeded
|
||||||
|
|
||||||
# API keys for official Generative Language API
|
# Gemini API keys (preferred)
|
||||||
|
gemini-api-key:
|
||||||
|
- api-key: "AIzaSy...01"
|
||||||
|
base-url: "https://generativelanguage.googleapis.com"
|
||||||
|
headers:
|
||||||
|
X-Custom-Header: "custom-value"
|
||||||
|
proxy-url: "socks5://proxy.example.com:1080"
|
||||||
|
- api-key: "AIzaSy...02"
|
||||||
|
|
||||||
|
# API keys for official Generative Language API (legacy compatibility)
|
||||||
generative-language-api-key:
|
generative-language-api-key:
|
||||||
- "AIzaSy...01"
|
- "AIzaSy...01"
|
||||||
- "AIzaSy...02"
|
|
||||||
- "AIzaSy...03"
|
|
||||||
- "AIzaSy...04"
|
|
||||||
|
|
||||||
# Codex API keys
|
# Codex API keys
|
||||||
codex-api-key:
|
codex-api-key:
|
||||||
@@ -558,9 +569,9 @@ By default, WebSocket connections to CLIProxyAPI do not require authentication.
|
|||||||
|
|
||||||
The `auth-dir` parameter specifies where authentication tokens are stored. When you run the login command, the application will create JSON files in this directory containing the authentication tokens for your Google accounts. Multiple accounts can be used for load balancing.
|
The `auth-dir` parameter specifies where authentication tokens are stored. When you run the login command, the application will create JSON files in this directory containing the authentication tokens for your Google accounts. Multiple accounts can be used for load balancing.
|
||||||
|
|
||||||
### Official Generative Language API
|
### Gemini API Configuration
|
||||||
|
|
||||||
The `generative-language-api-key` parameter allows you to define a list of API keys that can be used to authenticate requests to the official Generative Language API.
|
Use the `gemini-api-key` parameter to configure Gemini API keys. Each entry accepts optional `base-url`, `headers`, and `proxy-url` values; headers are only attached to requests sent to the overridden Gemini endpoint and are never forwarded to proxy servers. When `base-url` is omitted the server behaves the same as the legacy `generative-language-api-key` list. The legacy array remains supported for backwards compatibility and is automatically mirrored into the new structure.
|
||||||
|
|
||||||
## Hot Reloading
|
## Hot Reloading
|
||||||
|
|
||||||
|
|||||||
25
README_CN.md
25
README_CN.md
@@ -334,7 +334,12 @@ console.log(await claudeResponse.json());
|
|||||||
| `logging-to-file` | boolean | true | 是否将应用日志写入滚动文件;设为 false 时输出到 stdout/stderr。 |
|
| `logging-to-file` | boolean | true | 是否将应用日志写入滚动文件;设为 false 时输出到 stdout/stderr。 |
|
||||||
| `usage-statistics-enabled` | boolean | true | 是否启用内存中的使用统计;设为 false 时直接丢弃所有统计数据。 |
|
| `usage-statistics-enabled` | boolean | true | 是否启用内存中的使用统计;设为 false 时直接丢弃所有统计数据。 |
|
||||||
| `api-keys` | string[] | [] | 兼容旧配置的简写,会自动同步到默认 `config-api-key` 提供方。 |
|
| `api-keys` | string[] | [] | 兼容旧配置的简写,会自动同步到默认 `config-api-key` 提供方。 |
|
||||||
| `generative-language-api-key` | string[] | [] | 生成式语言API密钥列表。 |
|
| `gemini-api-key` | object[] | [] | Gemini API 密钥配置,支持为每个密钥设置可选的 `base-url` 与 `proxy-url`。 |
|
||||||
|
| `gemini-api-key.*.api-key` | string | "" | Gemini API 密钥。 |
|
||||||
|
| `gemini-api-key.*.base-url` | string | "" | 可选的 Gemini API 端点覆盖地址。 |
|
||||||
|
| `gemini-api-key.*.headers` | object | {} | 可选的额外 HTTP 头部,仅在访问覆盖后的 Gemini 端点时发送。 |
|
||||||
|
| `gemini-api-key.*.proxy-url` | string | "" | 可选的单独代理设置,会覆盖全局 `proxy-url`。 |
|
||||||
|
| `generative-language-api-key` | string[] | [] | (兼容项)不带扩展配置的生成式语言 API 密钥列表。 |
|
||||||
| `codex-api-key` | object | {} | Codex API密钥列表。 |
|
| `codex-api-key` | object | {} | Codex API密钥列表。 |
|
||||||
| `codex-api-key.api-key` | string | "" | Codex API密钥。 |
|
| `codex-api-key.api-key` | string | "" | Codex API密钥。 |
|
||||||
| `codex-api-key.base-url` | string | "" | 自定义的Codex API端点 |
|
| `codex-api-key.base-url` | string | "" | 自定义的Codex API端点 |
|
||||||
@@ -407,12 +412,18 @@ quota-exceeded:
|
|||||||
switch-project: true # 当配额超限时是否自动切换到另一个项目
|
switch-project: true # 当配额超限时是否自动切换到另一个项目
|
||||||
switch-preview-model: true # 当配额超限时是否自动切换到预览模型
|
switch-preview-model: true # 当配额超限时是否自动切换到预览模型
|
||||||
|
|
||||||
# AIStduio Gemini API 的 API 密钥
|
# Gemini API 密钥(推荐)
|
||||||
|
gemini-api-key:
|
||||||
|
- api-key: "AIzaSy...01"
|
||||||
|
base-url: "https://generativelanguage.googleapis.com"
|
||||||
|
headers:
|
||||||
|
X-Custom-Header: "custom-value"
|
||||||
|
proxy-url: "socks5://proxy.example.com:1080"
|
||||||
|
- api-key: "AIzaSy...02"
|
||||||
|
|
||||||
|
# AIStudio Gemini API 的遗留密钥配置
|
||||||
generative-language-api-key:
|
generative-language-api-key:
|
||||||
- "AIzaSy...01"
|
- "AIzaSy...01"
|
||||||
- "AIzaSy...02"
|
|
||||||
- "AIzaSy...03"
|
|
||||||
- "AIzaSy...04"
|
|
||||||
|
|
||||||
# Codex API 密钥
|
# Codex API 密钥
|
||||||
codex-api-key:
|
codex-api-key:
|
||||||
@@ -569,9 +580,9 @@ openai-compatibility:
|
|||||||
|
|
||||||
`auth-dir` 参数指定身份验证令牌的存储位置。当您运行登录命令时,应用程序将在此目录中创建包含 Google 账户身份验证令牌的 JSON 文件。多个账户可用于轮询。
|
`auth-dir` 参数指定身份验证令牌的存储位置。当您运行登录命令时,应用程序将在此目录中创建包含 Google 账户身份验证令牌的 JSON 文件。多个账户可用于轮询。
|
||||||
|
|
||||||
### 官方生成式语言 API
|
### Gemini API 配置
|
||||||
|
|
||||||
`generative-language-api-key` 参数允许您定义可用于验证对官方 AIStudio Gemini API 请求的 API 密钥列表。
|
使用 `gemini-api-key` 参数来配置 Gemini API 密钥;每个条目都可以选择性地提供 `base-url`、`headers` 与 `proxy-url`。`headers` 仅会附加到访问覆盖后 Gemini 端点的请求,不会转发给代理服务器。当 `base-url` 留空时,其行为与遗留的 `generative-language-api-key` 列表一致。旧字段仍受支持,会自动同步到新的结构中以保持兼容性。
|
||||||
|
|
||||||
## 热更新
|
## 热更新
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ quota-exceeded:
|
|||||||
# When true, enable authentication for the WebSocket API (/v1/ws).
|
# When true, enable authentication for the WebSocket API (/v1/ws).
|
||||||
ws-auth: false
|
ws-auth: false
|
||||||
|
|
||||||
# API keys for official Generative Language API
|
# Gemini API keys (preferred)
|
||||||
|
#gemini-api-key:
|
||||||
|
# - api-key: "AIzaSy...01"
|
||||||
|
# # base-url: "https://generativelanguage.googleapis.com"
|
||||||
|
# # headers:
|
||||||
|
# # X-Custom-Header: "custom-value"
|
||||||
|
# # proxy-url: "socks5://proxy.example.com:1080"
|
||||||
|
# - api-key: "AIzaSy...02"
|
||||||
|
|
||||||
|
# API keys for official Generative Language API (legacy compatibility)
|
||||||
#generative-language-api-key:
|
#generative-language-api-key:
|
||||||
# - "AIzaSy...01"
|
# - "AIzaSy...01"
|
||||||
# - "AIzaSy...02"
|
# - "AIzaSy...02"
|
||||||
# - "AIzaSy...03"
|
|
||||||
# - "AIzaSy...04"
|
|
||||||
|
|
||||||
# Codex API keys
|
# Codex API keys
|
||||||
#codex-api-key:
|
#codex-api-key:
|
||||||
|
|||||||
@@ -124,10 +124,137 @@ func (h *Handler) GetGlKeys(c *gin.Context) {
|
|||||||
c.JSON(200, gin.H{"generative-language-api-key": h.cfg.GlAPIKey})
|
c.JSON(200, gin.H{"generative-language-api-key": h.cfg.GlAPIKey})
|
||||||
}
|
}
|
||||||
func (h *Handler) PutGlKeys(c *gin.Context) {
|
func (h *Handler) PutGlKeys(c *gin.Context) {
|
||||||
h.putStringList(c, func(v []string) { h.cfg.GlAPIKey = v }, nil)
|
h.putStringList(c, func(v []string) {
|
||||||
|
h.cfg.GlAPIKey = append([]string(nil), v...)
|
||||||
|
}, func() {
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func (h *Handler) PatchGlKeys(c *gin.Context) {
|
||||||
|
h.patchStringList(c, &h.cfg.GlAPIKey, func() { h.cfg.SyncGeminiKeys() })
|
||||||
|
}
|
||||||
|
func (h *Handler) DeleteGlKeys(c *gin.Context) {
|
||||||
|
h.deleteFromStringList(c, &h.cfg.GlAPIKey, func() { h.cfg.SyncGeminiKeys() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// gemini-api-key: []GeminiKey
|
||||||
|
func (h *Handler) GetGeminiKeys(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{"gemini-api-key": h.cfg.GeminiKey})
|
||||||
|
}
|
||||||
|
func (h *Handler) PutGeminiKeys(c *gin.Context) {
|
||||||
|
data, err := c.GetRawData()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "failed to read body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var arr []config.GeminiKey
|
||||||
|
if err = json.Unmarshal(data, &arr); err != nil {
|
||||||
|
var obj struct {
|
||||||
|
Items []config.GeminiKey `json:"items"`
|
||||||
|
}
|
||||||
|
if err2 := json.Unmarshal(data, &obj); err2 != nil || len(obj.Items) == 0 {
|
||||||
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
arr = obj.Items
|
||||||
|
}
|
||||||
|
h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...)
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
}
|
||||||
|
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
Index *int `json:"index"`
|
||||||
|
Match *string `json:"match"`
|
||||||
|
Value *config.GeminiKey `json:"value"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
||||||
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := *body.Value
|
||||||
|
value.APIKey = strings.TrimSpace(value.APIKey)
|
||||||
|
value.BaseURL = strings.TrimSpace(value.BaseURL)
|
||||||
|
value.ProxyURL = strings.TrimSpace(value.ProxyURL)
|
||||||
|
if value.APIKey == "" {
|
||||||
|
// Treat empty API key as delete.
|
||||||
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||||
|
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:*body.Index], h.cfg.GeminiKey[*body.Index+1:]...)
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Match != nil {
|
||||||
|
match := strings.TrimSpace(*body.Match)
|
||||||
|
if match != "" {
|
||||||
|
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
||||||
|
removed := false
|
||||||
|
for i := range h.cfg.GeminiKey {
|
||||||
|
if !removed && h.cfg.GeminiKey[i].APIKey == match {
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, h.cfg.GeminiKey[i])
|
||||||
|
}
|
||||||
|
if removed {
|
||||||
|
h.cfg.GeminiKey = out
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||||
|
h.cfg.GeminiKey[*body.Index] = value
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Match != nil {
|
||||||
|
match := strings.TrimSpace(*body.Match)
|
||||||
|
for i := range h.cfg.GeminiKey {
|
||||||
|
if h.cfg.GeminiKey[i].APIKey == match {
|
||||||
|
h.cfg.GeminiKey[i] = value
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
|
}
|
||||||
|
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||||
|
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||||
|
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
||||||
|
for _, v := range h.cfg.GeminiKey {
|
||||||
|
if v.APIKey != val {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) != len(h.cfg.GeminiKey) {
|
||||||
|
h.cfg.GeminiKey = out
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
} else {
|
||||||
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if idxStr := c.Query("index"); idxStr != "" {
|
||||||
|
var idx int
|
||||||
|
if _, err := fmt.Sscanf(idxStr, "%d", &idx); err == nil && idx >= 0 && idx < len(h.cfg.GeminiKey) {
|
||||||
|
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:idx], h.cfg.GeminiKey[idx+1:]...)
|
||||||
|
h.cfg.SyncGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(400, gin.H{"error": "missing api-key or index"})
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchGlKeys(c *gin.Context) { h.patchStringList(c, &h.cfg.GlAPIKey, nil) }
|
|
||||||
func (h *Handler) DeleteGlKeys(c *gin.Context) { h.deleteFromStringList(c, &h.cfg.GlAPIKey, nil) }
|
|
||||||
|
|
||||||
// claude-api-key: []ClaudeKey
|
// claude-api-key: []ClaudeKey
|
||||||
func (h *Handler) GetClaudeKeys(c *gin.Context) {
|
func (h *Handler) GetClaudeKeys(c *gin.Context) {
|
||||||
|
|||||||
@@ -474,6 +474,11 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys)
|
mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys)
|
||||||
mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys)
|
mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys)
|
||||||
|
|
||||||
|
mgmt.GET("/gemini-api-key", s.mgmt.GetGeminiKeys)
|
||||||
|
mgmt.PUT("/gemini-api-key", s.mgmt.PutGeminiKeys)
|
||||||
|
mgmt.PATCH("/gemini-api-key", s.mgmt.PatchGeminiKey)
|
||||||
|
mgmt.DELETE("/gemini-api-key", s.mgmt.DeleteGeminiKey)
|
||||||
|
|
||||||
mgmt.GET("/logs", s.mgmt.GetLogs)
|
mgmt.GET("/logs", s.mgmt.GetLogs)
|
||||||
mgmt.DELETE("/logs", s.mgmt.DeleteLogs)
|
mgmt.DELETE("/logs", s.mgmt.DeleteLogs)
|
||||||
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
||||||
@@ -847,7 +852,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
|||||||
|
|
||||||
// Count client sources from configuration and auth directory
|
// Count client sources from configuration and auth directory
|
||||||
authFiles := util.CountAuthFiles(cfg.AuthDir)
|
authFiles := util.CountAuthFiles(cfg.AuthDir)
|
||||||
glAPIKeyCount := len(cfg.GlAPIKey)
|
geminiAPIKeyCount := len(cfg.GeminiKey)
|
||||||
claudeAPIKeyCount := len(cfg.ClaudeKey)
|
claudeAPIKeyCount := len(cfg.ClaudeKey)
|
||||||
codexAPIKeyCount := len(cfg.CodexKey)
|
codexAPIKeyCount := len(cfg.CodexKey)
|
||||||
openAICompatCount := 0
|
openAICompatCount := 0
|
||||||
@@ -860,11 +865,11 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
|||||||
openAICompatCount += len(entry.APIKeys)
|
openAICompatCount += len(entry.APIKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
total := authFiles + geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||||
fmt.Printf("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)\n",
|
fmt.Printf("server clients and configuration updated: %d clients (%d auth files + %d Gemini API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)\n",
|
||||||
total,
|
total,
|
||||||
authFiles,
|
authFiles,
|
||||||
glAPIKeyCount,
|
geminiAPIKeyCount,
|
||||||
claudeAPIKeyCount,
|
claudeAPIKeyCount,
|
||||||
codexAPIKeyCount,
|
codexAPIKeyCount,
|
||||||
openAICompatCount,
|
openAICompatCount,
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ type Config struct {
|
|||||||
// WebsocketAuth enables or disables authentication for the WebSocket API.
|
// WebsocketAuth enables or disables authentication for the WebSocket API.
|
||||||
WebsocketAuth bool `yaml:"ws-auth" json:"ws-auth"`
|
WebsocketAuth bool `yaml:"ws-auth" json:"ws-auth"`
|
||||||
|
|
||||||
// GlAPIKey is the API key for the generative language API.
|
// GlAPIKey exposes the legacy generative language API key list for backward compatibility.
|
||||||
GlAPIKey []string `yaml:"generative-language-api-key" json:"generative-language-api-key"`
|
GlAPIKey []string `yaml:"generative-language-api-key" json:"generative-language-api-key"`
|
||||||
|
|
||||||
|
// GeminiKey defines Gemini API key configurations with optional routing overrides.
|
||||||
|
GeminiKey []GeminiKey `yaml:"gemini-api-key" json:"gemini-api-key"`
|
||||||
|
|
||||||
// RequestRetry defines the retry times when the request failed.
|
// RequestRetry defines the retry times when the request failed.
|
||||||
RequestRetry int `yaml:"request-retry" json:"request-retry"`
|
RequestRetry int `yaml:"request-retry" json:"request-retry"`
|
||||||
|
|
||||||
@@ -122,6 +125,22 @@ type CodexKey struct {
|
|||||||
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeminiKey represents the configuration for a Gemini API key,
|
||||||
|
// including optional overrides for upstream base URL, proxy routing, and headers.
|
||||||
|
type GeminiKey struct {
|
||||||
|
// APIKey is the authentication key for accessing Gemini API services.
|
||||||
|
APIKey string `yaml:"api-key" json:"api-key"`
|
||||||
|
|
||||||
|
// BaseURL optionally overrides the Gemini API endpoint.
|
||||||
|
BaseURL string `yaml:"base-url,omitempty" json:"base-url,omitempty"`
|
||||||
|
|
||||||
|
// ProxyURL optionally overrides the global proxy for this API key.
|
||||||
|
ProxyURL string `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"`
|
||||||
|
|
||||||
|
// Headers optionally adds extra HTTP headers for requests sent with this key.
|
||||||
|
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// OpenAICompatibility represents the configuration for OpenAI API compatibility
|
// OpenAICompatibility represents the configuration for OpenAI API compatibility
|
||||||
// with external providers, allowing model aliases to be routed through OpenAI API format.
|
// with external providers, allowing model aliases to be routed through OpenAI API format.
|
||||||
type OpenAICompatibility struct {
|
type OpenAICompatibility struct {
|
||||||
@@ -227,6 +246,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
// Sync request authentication providers with inline API keys for backwards compatibility.
|
// Sync request authentication providers with inline API keys for backwards compatibility.
|
||||||
syncInlineAccessProvider(&cfg)
|
syncInlineAccessProvider(&cfg)
|
||||||
|
|
||||||
|
// Normalize Gemini API key configuration and migrate legacy entries.
|
||||||
|
cfg.SyncGeminiKeys()
|
||||||
|
|
||||||
// Sanitize OpenAI compatibility providers: drop entries without base-url
|
// Sanitize OpenAI compatibility providers: drop entries without base-url
|
||||||
sanitizeOpenAICompatibility(&cfg)
|
sanitizeOpenAICompatibility(&cfg)
|
||||||
|
|
||||||
@@ -276,6 +298,63 @@ func sanitizeCodexKeys(cfg *Config) {
|
|||||||
cfg.CodexKey = out
|
cfg.CodexKey = out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) SyncGeminiKeys() {
|
||||||
|
if cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.GeminiKey) > 0 {
|
||||||
|
out := make([]GeminiKey, 0, len(cfg.GeminiKey))
|
||||||
|
for i := range cfg.GeminiKey {
|
||||||
|
entry := cfg.GeminiKey[i]
|
||||||
|
entry.APIKey = strings.TrimSpace(entry.APIKey)
|
||||||
|
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||||
|
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||||
|
if entry.APIKey == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(entry.Headers) > 0 {
|
||||||
|
clean := make(map[string]string, len(entry.Headers))
|
||||||
|
for hk, hv := range entry.Headers {
|
||||||
|
key := strings.TrimSpace(hk)
|
||||||
|
val := strings.TrimSpace(hv)
|
||||||
|
if key == "" || val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clean[key] = val
|
||||||
|
}
|
||||||
|
if len(clean) == 0 {
|
||||||
|
entry.Headers = nil
|
||||||
|
} else {
|
||||||
|
entry.Headers = clean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, entry)
|
||||||
|
}
|
||||||
|
cfg.GeminiKey = out
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.GeminiKey) == 0 && len(cfg.GlAPIKey) > 0 {
|
||||||
|
out := make([]GeminiKey, 0, len(cfg.GlAPIKey))
|
||||||
|
for i := range cfg.GlAPIKey {
|
||||||
|
key := strings.TrimSpace(cfg.GlAPIKey[i])
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, GeminiKey{APIKey: key})
|
||||||
|
}
|
||||||
|
cfg.GeminiKey = out
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.GlAPIKey = cfg.GlAPIKey[:0]
|
||||||
|
if len(cfg.GeminiKey) > 0 {
|
||||||
|
cfg.GlAPIKey = make([]string, 0, len(cfg.GeminiKey))
|
||||||
|
for i := range cfg.GeminiKey {
|
||||||
|
cfg.GlAPIKey = append(cfg.GlAPIKey, cfg.GeminiKey[i].APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func syncInlineAccessProvider(cfg *Config) {
|
func syncInlineAccessProvider(cfg *Config) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
@@ -94,7 +95,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
action = "countTokens"
|
action = "countTokens"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, action)
|
baseURL := resolveGeminiBaseURL(auth)
|
||||||
|
url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, action)
|
||||||
if opts.Alt != "" && action != "countTokens" {
|
if opts.Alt != "" && action != "countTokens" {
|
||||||
url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
|
url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
|
||||||
}
|
}
|
||||||
@@ -111,6 +113,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
} else if bearer != "" {
|
} else if bearer != "" {
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
||||||
}
|
}
|
||||||
|
applyGeminiHeaders(httpReq, auth)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -180,7 +183,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
body = util.StripThinkingConfigIfUnsupported(req.Model, body)
|
||||||
body = fixGeminiImageAspectRatio(req.Model, body)
|
body = fixGeminiImageAspectRatio(req.Model, body)
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, "streamGenerateContent")
|
baseURL := resolveGeminiBaseURL(auth)
|
||||||
|
url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, "streamGenerateContent")
|
||||||
if opts.Alt == "" {
|
if opts.Alt == "" {
|
||||||
url = url + "?alt=sse"
|
url = url + "?alt=sse"
|
||||||
} else {
|
} else {
|
||||||
@@ -199,6 +203,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
} else {
|
} else {
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
||||||
}
|
}
|
||||||
|
applyGeminiHeaders(httpReq, auth)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -290,7 +295,8 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
|
translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
|
||||||
translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
|
translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, "countTokens")
|
baseURL := resolveGeminiBaseURL(auth)
|
||||||
|
url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, req.Model, "countTokens")
|
||||||
|
|
||||||
requestBody := bytes.NewReader(translatedReq)
|
requestBody := bytes.NewReader(translatedReq)
|
||||||
|
|
||||||
@@ -304,6 +310,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
} else {
|
} else {
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
httpReq.Header.Set("Authorization", "Bearer "+bearer)
|
||||||
}
|
}
|
||||||
|
applyGeminiHeaders(httpReq, auth)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -473,6 +480,60 @@ func geminiCreds(a *cliproxyauth.Auth) (apiKey, bearer string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveGeminiBaseURL(auth *cliproxyauth.Auth) string {
|
||||||
|
base := glEndpoint
|
||||||
|
if auth != nil && auth.Attributes != nil {
|
||||||
|
if custom := strings.TrimSpace(auth.Attributes["base_url"]); custom != "" {
|
||||||
|
base = strings.TrimRight(custom, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if base == "" {
|
||||||
|
return glEndpoint
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyGeminiHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
||||||
|
if req == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headers := geminiCustomHeaders(auth)
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range headers {
|
||||||
|
if k == "" || v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func geminiCustomHeaders(auth *cliproxyauth.Auth) map[string]string {
|
||||||
|
if auth == nil || auth.Attributes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
headers := make(map[string]string, len(auth.Attributes))
|
||||||
|
for k, v := range auth.Attributes {
|
||||||
|
if !strings.HasPrefix(k, "header:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(strings.TrimPrefix(k, "header:"))
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(v)
|
||||||
|
if val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
headers[name] = val
|
||||||
|
}
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
||||||
if modelName == "gemini-2.5-flash-image-preview" {
|
if modelName == "gemini-2.5-flash-image-preview" {
|
||||||
aspectRatioResult := gjson.GetBytes(rawJSON, "generationConfig.imageConfig.aspectRatio")
|
aspectRatioResult := gjson.GetBytes(rawJSON, "generationConfig.imageConfig.aspectRatio")
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ func MaskAuthorizationHeader(value string) string {
|
|||||||
func MaskSensitiveHeaderValue(key, value string) string {
|
func MaskSensitiveHeaderValue(key, value string) string {
|
||||||
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
||||||
switch {
|
switch {
|
||||||
case lowerKey == "authorization":
|
case strings.Contains(lowerKey, "authorization"):
|
||||||
return MaskAuthorizationHeader(value)
|
return MaskAuthorizationHeader(value)
|
||||||
case strings.Contains(lowerKey, "api-key"),
|
case strings.Contains(lowerKey, "api-key"),
|
||||||
strings.Contains(lowerKey, "apikey"),
|
strings.Contains(lowerKey, "apikey"),
|
||||||
|
|||||||
@@ -604,8 +604,8 @@ func (w *Watcher) reloadClients(rescanAuth bool) {
|
|||||||
// no legacy clients to unregister
|
// no legacy clients to unregister
|
||||||
|
|
||||||
// Create new API key clients based on the new config
|
// Create new API key clients based on the new config
|
||||||
glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount := BuildAPIKeyClients(cfg)
|
geminiAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount := BuildAPIKeyClients(cfg)
|
||||||
totalAPIKeyClients := glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
totalAPIKeyClients := geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||||
log.Debugf("loaded %d API key clients", totalAPIKeyClients)
|
log.Debugf("loaded %d API key clients", totalAPIKeyClients)
|
||||||
|
|
||||||
var authFileCount int
|
var authFileCount int
|
||||||
@@ -648,7 +648,7 @@ func (w *Watcher) reloadClients(rescanAuth bool) {
|
|||||||
w.clientsMutex.Unlock()
|
w.clientsMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
totalNewClients := authFileCount + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
totalNewClients := authFileCount + geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
|
||||||
|
|
||||||
// Ensure consumers observe the new configuration before auth updates dispatch.
|
// Ensure consumers observe the new configuration before auth updates dispatch.
|
||||||
if w.reloadCallback != nil {
|
if w.reloadCallback != nil {
|
||||||
@@ -658,10 +658,10 @@ func (w *Watcher) reloadClients(rescanAuth bool) {
|
|||||||
|
|
||||||
w.refreshAuthState()
|
w.refreshAuthState()
|
||||||
|
|
||||||
log.Infof("full client load complete - %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
|
log.Infof("full client load complete - %d clients (%d auth files + %d Gemini API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
|
||||||
totalNewClients,
|
totalNewClients,
|
||||||
authFileCount,
|
authFileCount,
|
||||||
glAPIKeyCount,
|
geminiAPIKeyCount,
|
||||||
claudeAPIKeyCount,
|
claudeAPIKeyCount,
|
||||||
codexAPIKeyCount,
|
codexAPIKeyCount,
|
||||||
openAICompatCount,
|
openAICompatCount,
|
||||||
@@ -746,23 +746,41 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
|
|||||||
w.clientsMutex.RUnlock()
|
w.clientsMutex.RUnlock()
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
// Gemini official API keys -> synthesize auths
|
// Gemini official API keys -> synthesize auths
|
||||||
for i := range cfg.GlAPIKey {
|
for i := range cfg.GeminiKey {
|
||||||
k := strings.TrimSpace(cfg.GlAPIKey[i])
|
entry := cfg.GeminiKey[i]
|
||||||
if k == "" {
|
key := strings.TrimSpace(entry.APIKey)
|
||||||
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
id, token := idGen.next("gemini:apikey", k)
|
base := strings.TrimSpace(entry.BaseURL)
|
||||||
|
proxyURL := strings.TrimSpace(entry.ProxyURL)
|
||||||
|
id, token := idGen.next("gemini:apikey", key, base)
|
||||||
|
attrs := map[string]string{
|
||||||
|
"source": fmt.Sprintf("config:gemini[%s]", token),
|
||||||
|
"api_key": key,
|
||||||
|
}
|
||||||
|
if base != "" {
|
||||||
|
attrs["base_url"] = base
|
||||||
|
}
|
||||||
|
if len(entry.Headers) > 0 {
|
||||||
|
for hk, hv := range entry.Headers {
|
||||||
|
key := strings.TrimSpace(hk)
|
||||||
|
val := strings.TrimSpace(hv)
|
||||||
|
if key == "" || val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attrs["header:"+key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
a := &coreauth.Auth{
|
a := &coreauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: "gemini",
|
Provider: "gemini",
|
||||||
Label: "gemini-apikey",
|
Label: "gemini-apikey",
|
||||||
Status: coreauth.StatusActive,
|
Status: coreauth.StatusActive,
|
||||||
Attributes: map[string]string{
|
ProxyURL: proxyURL,
|
||||||
"source": fmt.Sprintf("config:gemini[%s]", token),
|
Attributes: attrs,
|
||||||
"api_key": k,
|
CreatedAt: now,
|
||||||
},
|
UpdatedAt: now,
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
}
|
||||||
out = append(out, a)
|
out = append(out, a)
|
||||||
}
|
}
|
||||||
@@ -1030,14 +1048,14 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
|
func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
|
||||||
glAPIKeyCount := 0
|
geminiAPIKeyCount := 0
|
||||||
claudeAPIKeyCount := 0
|
claudeAPIKeyCount := 0
|
||||||
codexAPIKeyCount := 0
|
codexAPIKeyCount := 0
|
||||||
openAICompatCount := 0
|
openAICompatCount := 0
|
||||||
|
|
||||||
if len(cfg.GlAPIKey) > 0 {
|
if len(cfg.GeminiKey) > 0 {
|
||||||
// Stateless executor handles Gemini API keys; avoid constructing legacy clients.
|
// Stateless executor handles Gemini API keys; avoid constructing legacy clients.
|
||||||
glAPIKeyCount += len(cfg.GlAPIKey)
|
geminiAPIKeyCount += len(cfg.GeminiKey)
|
||||||
}
|
}
|
||||||
if len(cfg.ClaudeKey) > 0 {
|
if len(cfg.ClaudeKey) > 0 {
|
||||||
claudeAPIKeyCount += len(cfg.ClaudeKey)
|
claudeAPIKeyCount += len(cfg.ClaudeKey)
|
||||||
@@ -1056,7 +1074,7 @@ func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
|
return geminiAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func diffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []string {
|
func diffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []string {
|
||||||
@@ -1239,10 +1257,31 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
} else if !reflect.DeepEqual(trimStrings(oldCfg.APIKeys), trimStrings(newCfg.APIKeys)) {
|
} else if !reflect.DeepEqual(trimStrings(oldCfg.APIKeys), trimStrings(newCfg.APIKeys)) {
|
||||||
changes = append(changes, "api-keys: values updated (count unchanged, redacted)")
|
changes = append(changes, "api-keys: values updated (count unchanged, redacted)")
|
||||||
}
|
}
|
||||||
if len(oldCfg.GlAPIKey) != len(newCfg.GlAPIKey) {
|
if len(oldCfg.GeminiKey) != len(newCfg.GeminiKey) {
|
||||||
changes = append(changes, fmt.Sprintf("generative-language-api-key count: %d -> %d", len(oldCfg.GlAPIKey), len(newCfg.GlAPIKey)))
|
changes = append(changes, fmt.Sprintf("gemini-api-key count: %d -> %d", len(oldCfg.GeminiKey), len(newCfg.GeminiKey)))
|
||||||
} else if !reflect.DeepEqual(trimStrings(oldCfg.GlAPIKey), trimStrings(newCfg.GlAPIKey)) {
|
} else {
|
||||||
changes = append(changes, "generative-language-api-key: values updated (count unchanged, redacted)")
|
for i := range oldCfg.GeminiKey {
|
||||||
|
if i >= len(newCfg.GeminiKey) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
o := oldCfg.GeminiKey[i]
|
||||||
|
n := newCfg.GeminiKey[i]
|
||||||
|
if strings.TrimSpace(o.BaseURL) != strings.TrimSpace(n.BaseURL) {
|
||||||
|
changes = append(changes, fmt.Sprintf("gemini[%d].base-url: %s -> %s", i, strings.TrimSpace(o.BaseURL), strings.TrimSpace(n.BaseURL)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(o.ProxyURL) != strings.TrimSpace(n.ProxyURL) {
|
||||||
|
changes = append(changes, fmt.Sprintf("gemini[%d].proxy-url: %s -> %s", i, strings.TrimSpace(o.ProxyURL), strings.TrimSpace(n.ProxyURL)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) {
|
||||||
|
changes = append(changes, fmt.Sprintf("gemini[%d].api-key: updated", i))
|
||||||
|
}
|
||||||
|
if !equalStringMap(o.Headers, n.Headers) {
|
||||||
|
changes = append(changes, fmt.Sprintf("gemini[%d].headers: updated", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(trimStrings(oldCfg.GlAPIKey), trimStrings(newCfg.GlAPIKey)) {
|
||||||
|
changes = append(changes, "generative-language-api-key: values updated (legacy view, redacted)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude keys (do not print key material)
|
// Claude keys (do not print key material)
|
||||||
@@ -1325,3 +1364,15 @@ func trimStrings(in []string) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func equalStringMap(a, b map[string]string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range a {
|
||||||
|
if b[k] != v {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func NewAPIKeyClientProvider() APIKeyClientProvider {
|
|||||||
type apiKeyClientProvider struct{}
|
type apiKeyClientProvider struct{}
|
||||||
|
|
||||||
func (p *apiKeyClientProvider) Load(ctx context.Context, cfg *config.Config) (*APIKeyClientResult, error) {
|
func (p *apiKeyClientProvider) Load(ctx context.Context, cfg *config.Config) (*APIKeyClientResult, error) {
|
||||||
glCount, claudeCount, codexCount, openAICompat := watcher.BuildAPIKeyClients(cfg)
|
geminiCount, claudeCount, codexCount, openAICompat := watcher.BuildAPIKeyClients(cfg)
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -38,7 +38,7 @@ func (p *apiKeyClientProvider) Load(ctx context.Context, cfg *config.Config) (*A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &APIKeyClientResult{
|
return &APIKeyClientResult{
|
||||||
GeminiKeyCount: glCount,
|
GeminiKeyCount: geminiCount,
|
||||||
ClaudeKeyCount: claudeCount,
|
ClaudeKeyCount: claudeCount,
|
||||||
CodexKeyCount: codexCount,
|
CodexKeyCount: codexCount,
|
||||||
OpenAICompatCount: openAICompat,
|
OpenAICompatCount: openAICompat,
|
||||||
|
|||||||
Reference in New Issue
Block a user