mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-07-04 06:11:32 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57d593fa17 | ||
|
|
f38b5ae870 | ||
|
|
418b3ca13c | ||
|
|
09bfa85e69 | ||
|
|
62b132208b | ||
|
|
fc28f4f74e | ||
|
|
f79a52f839 | ||
|
|
94d1041961 | ||
|
|
ada32d526a | ||
|
|
ef1e38aba1 | ||
|
|
60b2d59e25 | ||
|
|
e18aa73456 | ||
|
|
24747a5f09 | ||
|
|
621dac22dc | ||
|
|
23d7004b60 | ||
|
|
c3b3d34127 | ||
|
|
18a166afb0 | ||
|
|
a41447a96d | ||
|
|
df8d543539 | ||
|
|
5ecce8e0fe | ||
|
|
00f423a622 | ||
|
|
05ce04de69 | ||
|
|
cd5549e1aa | ||
|
|
f573c0255a | ||
|
|
060d7fffe6 | ||
|
|
38dbcd1643 | ||
|
|
241d97027c | ||
|
|
d18689fe9f | ||
|
|
b72298fef4 |
@@ -33,6 +33,8 @@ TIME_OUT=300
|
||||
# 代理服务器配置 (支持 http 和 socks5)
|
||||
# 示例: PROXIES=["http://user:pass@host:port", "socks5://host:port"]
|
||||
PROXIES=[]
|
||||
# 对同一个API_KEY使用代理列表中固定的IP策略
|
||||
PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY=true
|
||||
#########################image_generate 相关配置###########################
|
||||
PAID_KEY=AIzaSyxxxxxxxxxxxxxxxxxxx
|
||||
CREATE_IMAGE_MODEL=imagen-3.0-generate-002
|
||||
@@ -72,3 +74,8 @@ FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS=5
|
||||
# 安全设置 (JSON 字符串格式)
|
||||
# 注意:这里的示例值可能需要根据实际模型支持情况调整
|
||||
SAFETY_SETTINGS=[{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}]
|
||||
URL_NORMALIZATION_ENABLED=false
|
||||
# tts配置
|
||||
TTS_MODEL=gemini-2.5-flash-preview-tts
|
||||
TTS_VOICE_NAME=Zephyr
|
||||
TTS_SPEED=normal
|
||||
@@ -14,6 +14,7 @@ ENV BASE_URL=https://generativelanguage.googleapis.com/v1beta
|
||||
ENV TOOLS_CODE_EXECUTION_ENABLED=false
|
||||
ENV IMAGE_MODELS='["gemini-2.0-flash-exp"]'
|
||||
ENV SEARCH_MODELS='["gemini-2.0-flash-exp","gemini-2.0-pro-exp"]'
|
||||
ENV URL_NORMALIZATION_ENABLED=false
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
133
README.md
133
README.md
@@ -11,7 +11,7 @@
|
||||
[](https://www.uvicorn.org/)
|
||||
[](https://t.me/+soaHax5lyI0wZDVl)
|
||||
|
||||
> Telegram Group: https://t.me/+soaHax5lyI0wZDVl
|
||||
> Telegram Group: <https://t.me/+soaHax5lyI0wZDVl>
|
||||
|
||||
## Project Introduction
|
||||
|
||||
@@ -40,39 +40,39 @@ app/
|
||||
|
||||
## ✨ Feature Highlights
|
||||
|
||||
* **Multi-Key Load Balancing**: Supports configuring multiple Gemini API Keys (`API_KEYS`) for automatic sequential polling, improving availability and concurrency.
|
||||
* **Visual Configuration Takes Effect Immediately**: Configurations modified through the admin backend take effect without restarting the service. Remember to click save for changes to apply.
|
||||
* **Multi-Key Load Balancing**: Supports configuring multiple Gemini API Keys (`API_KEYS`) for automatic sequential polling, improving availability and concurrency.
|
||||
* **Visual Configuration Takes Effect Immediately**: Configurations modified through the admin backend take effect without restarting the service. Remember to click save for changes to apply.
|
||||

|
||||
* **Dual Protocol API Compatibility**: Supports forwarding CHAT API requests in both Gemini and OpenAI formats.
|
||||
* **Dual Protocol API Compatibility**: Supports forwarding CHAT API requests in both Gemini and OpenAI formats.
|
||||
|
||||
```plaintext
|
||||
openai baseurl `http://localhost:8000(/hf)/v1`
|
||||
gemini baseurl `http://localhost:8000(/gemini)/v1beta`
|
||||
```
|
||||
|
||||
* **Supports Image-Text Chat and Image Modification**: `IMAGE_MODELS` configures which models can perform image-text chat and image editing. When actually calling, use the `configured_model-image` model name to use this feature.
|
||||
* **Supports Image-Text Chat and Image Modification**: `IMAGE_MODELS` configures which models can perform image-text chat and image editing. When actually calling, use the `configured_model-image` model name to use this feature.
|
||||

|
||||

|
||||
* **Supports Web Search**: Supports web search. `SEARCH_MODELS` configures which models can perform web searches. When actually calling, use the `configured_model-search` model name to use this feature.
|
||||
* **Supports Web Search**: Supports web search. `SEARCH_MODELS` configures which models can perform web searches. When actually calling, use the `configured_model-search` model name to use this feature.
|
||||

|
||||
* **Key Status Monitoring**: Provides a `/keys_status` page (requires authentication) to view the status and usage of each Key in real-time.
|
||||
* **Key Status Monitoring**: Provides a `/keys_status` page (requires authentication) to view the status and usage of each Key in real-time.
|
||||

|
||||
* **Detailed Logging**: Provides detailed error logs for easy troubleshooting.
|
||||
* **Detailed Logging**: Provides detailed error logs for easy troubleshooting.
|
||||

|
||||

|
||||

|
||||
* **Support for Custom Gemini Proxy**: Supports custom Gemini proxies, such as those built on Deno or Cloudflare.
|
||||
* **OpenAI Image Generation API Compatibility**: Adapts the `imagen-3.0-generate-002` model interface to be compatible with the OpenAI image generation API, supporting client calls.
|
||||
* **Flexible Key Addition**: Flexible way to add keys using regex matching for `gemini_key`, with key deduplication.
|
||||
* **Support for Custom Gemini Proxy**: Supports custom Gemini proxies, such as those built on Deno or Cloudflare.
|
||||
* **OpenAI Image Generation API Compatibility**: Adapts the `imagen-3.0-generate-002` model interface to be compatible with the OpenAI image generation API, supporting client calls.
|
||||
* **Flexible Key Addition**: Flexible way to add keys using regex matching for `gemini_key`, with key deduplication.
|
||||

|
||||
* **OpenAI Format Embeddings API Compatibility**: Perfectly adapts to the OpenAI format `embeddings` interface, usable for local document vectorization.
|
||||
* **Streamlined Response Optimization**: Optional stream output optimizer (`STREAM_OPTIMIZER_ENABLED`) to improve the experience of long-text stream responses.
|
||||
* **Failure Retry and Key Management**: Automatically handles API request failures, retries (`MAX_RETRIES`), automatically disables Keys after too many failures (`MAX_FAILURES`), and periodically checks for recovery (`CHECK_INTERVAL_HOURS`).
|
||||
* **Docker Support**: Supports AMD and ARM architecture Docker deployments. You can also build your own Docker image.
|
||||
* **OpenAI Format Embeddings API Compatibility**: Perfectly adapts to the OpenAI format `embeddings` interface, usable for local document vectorization.
|
||||
* **Streamlined Response Optimization**: Optional stream output optimizer (`STREAM_OPTIMIZER_ENABLED`) to improve the experience of long-text stream responses.
|
||||
* **Failure Retry and Key Management**: Automatically handles API request failures, retries (`MAX_RETRIES`), automatically disables Keys after too many failures (`MAX_FAILURES`), and periodically checks for recovery (`CHECK_INTERVAL_HOURS`).
|
||||
* **Docker Support**: Supports AMD and ARM architecture Docker deployments. You can also build your own Docker image.
|
||||
> Image address: docker pull ghcr.io/snailyp/gemini-balance:latest
|
||||
* **Automatic Model List Maintenance**: Supports fetching OpenAI and Gemini model lists, perfectly compatible with NewAPI's automatic model list fetching, no manual entry required.
|
||||
* **Support for Removing Unused Models**: Too many default models are provided, many of which are not used. You can filter them out using `FILTERED_MODELS`.
|
||||
* **Proxy Support**: Supports configuring HTTP/SOCKS5 proxy servers (`PROXIES`) for accessing the Gemini API, convenient for use in special network environments. Supports batch adding proxies.
|
||||
* **Automatic Model List Maintenance**: Supports fetching OpenAI and Gemini model lists, perfectly compatible with NewAPI's automatic model list fetching, no manual entry required.
|
||||
* **Support for Removing Unused Models**: Too many default models are provided, many of which are not used. You can filter them out using `FILTERED_MODELS`.
|
||||
* **Proxy Support**: Supports configuring HTTP/SOCKS5 proxy servers (`PROXIES`) for accessing the Gemini API, convenient for use in special network environments. Supports batch adding proxies.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@@ -80,79 +80,83 @@ app/
|
||||
|
||||
#### a) Build with Dockerfile
|
||||
|
||||
1. **Build Image**:
|
||||
1. **Build Image**:
|
||||
|
||||
```bash
|
||||
docker build -t gemini-balance .
|
||||
```
|
||||
|
||||
2. **Run Container**:
|
||||
2. **Run Container**:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8000:8000 --env-file .env gemini-balance
|
||||
```
|
||||
|
||||
* `-d`: Run in detached mode.
|
||||
* `-p 8000:8000`: Map port 8000 of the container to port 8000 of the host.
|
||||
* `--env-file .env`: Use the `.env` file to set environment variables.
|
||||
* `-d`: Run in detached mode.
|
||||
* `-p 8000:8000`: Map port 8000 of the container to port 8000 of the host.
|
||||
* `--env-file .env`: Use the `.env` file to set environment variables.
|
||||
|
||||
> Note: If using an SQLite database, you need to mount a data volume to persist
|
||||
> Note: If using an SQLite database, you need to mount a data volume to persist
|
||||
>
|
||||
> ```bash
|
||||
> docker run -d -p 8000:8000 --env-file .env -v /path/to/data:/app/data gemini-balance
|
||||
> ```
|
||||
>
|
||||
> Where `/path/to/data` is the data storage path on the host, and `/app/data` is the data directory inside the container.
|
||||
|
||||
#### b) Deploy with an Existing Docker Image
|
||||
|
||||
1. **Pull Image**:
|
||||
1. **Pull Image**:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/snailyp/gemini-balance:latest
|
||||
```
|
||||
|
||||
2. **Run Container**:
|
||||
2. **Run Container**:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8000:8000 --env-file .env ghcr.io/snailyp/gemini-balance:latest
|
||||
```
|
||||
|
||||
* `-d`: Run in detached mode.
|
||||
* `-p 8000:8000`: Map port 8000 of the container to port 8000 of the host (adjust as needed).
|
||||
* `--env-file .env`: Use the `.env` file to set environment variables (ensure the `.env` file exists in the directory where the command is executed).
|
||||
* `-d`: Run in detached mode.
|
||||
* `-p 8000:8000`: Map port 8000 of the container to port 8000 of the host (adjust as needed).
|
||||
* `--env-file .env`: Use the `.env` file to set environment variables (ensure the `.env` file exists in the directory where the command is executed).
|
||||
|
||||
> Note: If using an SQLite database, you need to mount a data volume to persist
|
||||
> Note: If using an SQLite database, you need to mount a data volume to persist
|
||||
>
|
||||
> ```bash
|
||||
> docker run -d -p 8000:8000 --env-file .env -v /path/to/data:/app/data ghcr.io/snailyp/gemini-balance:latest
|
||||
> ```
|
||||
>
|
||||
> Where `/path/to/data` is the data storage path on the host, and `/app/data` is the data directory inside the container.
|
||||
|
||||
### Run Locally (Suitable for Development and Testing)
|
||||
|
||||
If you want to run the source code directly locally for development or testing, follow these steps:
|
||||
|
||||
1. **Ensure Prerequisites are Met**:
|
||||
* Clone the repository locally.
|
||||
* Install Python 3.9 or higher.
|
||||
* Create and configure the `.env` file in the project root directory (refer to the "Configure Environment Variables" section above).
|
||||
* Install project dependencies:
|
||||
1. **Ensure Prerequisites are Met**:
|
||||
* Clone the repository locally.
|
||||
* Install Python 3.9 or higher.
|
||||
* Create and configure the `.env` file in the project root directory (refer to the "Configure Environment Variables" section above).
|
||||
* Install project dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Start Application**:
|
||||
2. **Start Application**:
|
||||
Run the following command in the project root directory:
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
* `app.main:app`: Specifies the location of the FastAPI application instance (the `app` object in the `main.py` file within the `app` module).
|
||||
* `--host 0.0.0.0`: Makes the application accessible from any IP address on the local network.
|
||||
* `--port 8000`: Specifies the port number the application listens on (you can change this as needed).
|
||||
* `--reload`: Enables automatic reloading. When you modify the code, the service will automatically restart, which is very suitable for development environments (remove this option in production environments).
|
||||
* `app.main:app`: Specifies the location of the FastAPI application instance (the `app` object in the `main.py` file within the `app` module).
|
||||
* `--host 0.0.0.0`: Makes the application accessible from any IP address on the local network.
|
||||
* `--port 8000`: Specifies the port number the application listens on (you can change this as needed).
|
||||
* `--reload`: Enables automatic reloading. When you modify the code, the service will automatically restart, which is very suitable for development environments (remove this option in production environments).
|
||||
|
||||
3. **Access Application**:
|
||||
3. **Access Application**:
|
||||
After the application starts, you can access `http://localhost:8000` (or the host and port you specified) through a browser or API tool.
|
||||
|
||||
### Complete Configuration List
|
||||
@@ -181,6 +185,7 @@ If you want to run the source code directly locally for development or testing,
|
||||
| `SHOW_THINKING_PROCESS` | Optional, whether to display the model's thinking process | `true` |
|
||||
| `THINKING_MODELS` | Optional, list of models that support thinking functions | `[]` |
|
||||
| `THINKING_BUDGET_MAP` | Optional, thinking function budget mapping (model_name:budget_value) | `{}` |
|
||||
| `URL_NORMALIZATION_ENABLED` | Optional, whether to enable intelligent URL routing mapping | `false` |
|
||||
| `BASE_URL` | Optional, Gemini API base URL, no modification needed by default | `https://generativelanguage.googleapis.com/v1beta` |
|
||||
| `MAX_FAILURES` | Optional, number of times a single key is allowed to fail | `3` |
|
||||
| `MAX_RETRIES` | Optional, maximum number of retries for failed API requests | `3` |
|
||||
@@ -194,6 +199,10 @@ If you want to run the source code directly locally for development or testing,
|
||||
| `AUTO_DELETE_REQUEST_LOGS_ENABLED`| Optional, whether to enable automatic deletion of request logs | `false` |
|
||||
| `AUTO_DELETE_REQUEST_LOGS_DAYS` | Optional, automatically delete request logs older than this many days (e.g., 1, 7, 30) | `30` |
|
||||
| `SAFETY_SETTINGS` | Optional, safety settings (JSON string format), used to configure content safety thresholds. Example values may need adjustment based on actual model support. | `[{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}]` |
|
||||
| **TTS Related** | | |
|
||||
| `TTS_MODEL` | Optional, TTS model name | `gemini-2.5-flash-preview-tts` |
|
||||
| `TTS_VOICE_NAME` | Optional, TTS voice name | `Zephyr` |
|
||||
| `TTS_SPEED` | Optional, TTS speed | `normal` |
|
||||
| **Image Generation Related** | | |
|
||||
| `PAID_KEY` | Optional, paid API Key for advanced features like image generation | `your-paid-api-key` |
|
||||
| `CREATE_IMAGE_MODEL` | Optional, image generation model | `imagen-3.0-generate-002` |
|
||||
@@ -219,20 +228,20 @@ The following are the main API endpoints provided by the service:
|
||||
|
||||
### Gemini API Related (`(/gemini)/v1beta`)
|
||||
|
||||
* `GET /models`: List available Gemini models.
|
||||
* `POST /models/{model_name}:generateContent`: Generate content using the specified Gemini model.
|
||||
* `POST /models/{model_name}:streamGenerateContent`: Stream content generation using the specified Gemini model.
|
||||
* `GET /models`: List available Gemini models.
|
||||
* `POST /models/{model_name}:generateContent`: Generate content using the specified Gemini model.
|
||||
* `POST /models/{model_name}:streamGenerateContent`: Stream content generation using the specified Gemini model.
|
||||
|
||||
### OpenAI API Related
|
||||
|
||||
* `GET (/hf)/v1/models`: List available models (uses Gemini format underneath).
|
||||
* `POST (/hf)/v1/chat/completions`: Perform chat completion (uses Gemini format underneath, supports streaming).
|
||||
* `POST (/hf)/v1/embeddings`: Create text embeddings (uses Gemini format underneath).
|
||||
* `POST (/hf)/v1/images/generations`: Generate images (uses Gemini format underneath).
|
||||
* `GET /openai/v1/models`: List available models (uses OpenAI format underneath).
|
||||
* `POST /openai/v1/chat/completions`: Perform chat completion (uses OpenAI format underneath, supports streaming, can prevent truncation, and is faster).
|
||||
* `POST /openai/v1/embeddings`: Create text embeddings (uses OpenAI format underneath).
|
||||
* `POST /openai/v1/images/generations`: Generate images (uses OpenAI format underneath).
|
||||
* `GET (/hf)/v1/models`: List available models (uses Gemini format underneath).
|
||||
* `POST (/hf)/v1/chat/completions`: Perform chat completion (uses Gemini format underneath, supports streaming).
|
||||
* `POST (/hf)/v1/embeddings`: Create text embeddings (uses Gemini format underneath).
|
||||
* `POST (/hf)/v1/images/generations`: Generate images (uses Gemini format underneath).
|
||||
* `GET /openai/v1/models`: List available models (uses OpenAI format underneath).
|
||||
* `POST /openai/v1/chat/completions`: Perform chat completion (uses OpenAI format underneath, supports streaming, can prevent truncation, and is faster).
|
||||
* `POST /openai/v1/embeddings`: Create text embeddings (uses OpenAI format underneath).
|
||||
* `POST /openai/v1/images/generations`: Generate images (uses OpenAI format underneath).
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -242,9 +251,9 @@ Pull Requests or Issues are welcome.
|
||||
|
||||
Special thanks to the following projects and platforms for providing image hosting services for this project:
|
||||
|
||||
* [PicGo](https://www.picgo.net/)
|
||||
* [SM.MS](https://smms.app/)
|
||||
* [CloudFlare-ImgBed](https://github.com/MarSeventh/CloudFlare-ImgBed) open source project
|
||||
* [PicGo](https://www.picgo.net/)
|
||||
* [SM.MS](https://smms.app/)
|
||||
* [CloudFlare-ImgBed](https://github.com/MarSeventh/CloudFlare-ImgBed) open source project
|
||||
|
||||
## 🙏 Thanks to Contributors
|
||||
|
||||
@@ -252,22 +261,26 @@ Thanks to all developers who contributed to this project!
|
||||
|
||||
[](https://github.com/snailyp/gemini-balance/graphs/contributors)
|
||||
|
||||
## Thanks to Our Supporters
|
||||
|
||||
A special shout-out to DigitalOcean for providing the rock-solid and dependable cloud infrastructure that keeps this project humming!
|
||||
[](https://m.do.co/c/b249dd7f3b4c)
|
||||
|
||||
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
|
||||
[](https://edgeone.ai/?from=github)
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#snailyp/gemini-balance&Date)
|
||||
|
||||
## 💖 Friendly Projects
|
||||
|
||||
* **[OneLine](https://github.com/chengtx809/OneLine)** by [chengtx809](https://github.com/chengtx809) - OneLine: AI-driven hot event timeline generation tool
|
||||
* **[OneLine](https://github.com/chengtx809/OneLine)** by [chengtx809](https://github.com/chengtx809) - OneLine: AI-driven hot event timeline generation tool
|
||||
|
||||
## 🎁 Project Support
|
||||
|
||||
If you find this project helpful, consider supporting me via [Afdian](https://afdian.com/a/snaily).
|
||||
|
||||
## VPS Recommend
|
||||
|
||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the CC BY-NC 4.0 (Attribution-NonCommercial) license. Any form of commercial resale service is prohibited. See the LICENSE file for details.
|
||||
|
||||
@@ -178,6 +178,7 @@ app/
|
||||
| `SHOW_THINKING_PROCESS` | 可选,是否显示模型思考过程 | `true` |
|
||||
| `THINKING_MODELS` | 可选,支持思考功能的模型列表 | `[]` |
|
||||
| `THINKING_BUDGET_MAP` | 可选,思考功能预算映射 (模型名:预算值) | `{}` |
|
||||
| `URL_NORMALIZATION_ENABLED` | 可选,是否启用智能路由映射功能 | `false` |
|
||||
| `BASE_URL` | 可选,Gemini API 基础 URL,默认无需修改 | `https://generativelanguage.googleapis.com/v1beta` |
|
||||
| `MAX_FAILURES` | 可选,允许单个key失败的次数 | `3` |
|
||||
| `MAX_RETRIES` | 可选,API 请求失败时的最大重试次数 | `3` |
|
||||
@@ -191,6 +192,10 @@ app/
|
||||
| `AUTO_DELETE_REQUEST_LOGS_ENABLED`| 可选,是否开启自动删除请求日志 | `false` |
|
||||
| `AUTO_DELETE_REQUEST_LOGS_DAYS` | 可选,自动删除多少天前的请求日志 (例如 1, 7, 30) | `30` |
|
||||
| `SAFETY_SETTINGS` | 可选,安全设置 (JSON 字符串格式),用于配置内容安全阈值。示例值可能需要根据实际模型支持情况调整。 | `[{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}]` |
|
||||
| **TTS 相关** | | |
|
||||
| `TTS_MODEL` | 可选,TTS 模型名称 | `gemini-2.5-flash-preview-tts` |
|
||||
| `TTS_VOICE_NAME` | 可选,TTS 语音名称 | `Zephyr` |
|
||||
| `TTS_SPEED` | 可选,TTS 语速 | `normal` |
|
||||
| **图像生成相关** | | |
|
||||
| `PAID_KEY` | 可选,付费版API Key,用于图片生成等高级功能 | `your-paid-api-key` |
|
||||
| `CREATE_IMAGE_MODEL` | 可选,图片生成模型 | `imagen-3.0-generate-002` |
|
||||
|
||||
@@ -60,9 +60,13 @@ class Settings(BaseSettings):
|
||||
TIME_OUT: int = DEFAULT_TIMEOUT
|
||||
MAX_RETRIES: int = MAX_RETRIES
|
||||
PROXIES: List[str] = []
|
||||
PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY: bool = True # 是否使用一致性哈希来选择代理
|
||||
VERTEX_API_KEYS: List[str] = []
|
||||
VERTEX_EXPRESS_BASE_URL: str = "https://aiplatform.googleapis.com/v1beta1/publishers/google"
|
||||
|
||||
|
||||
# 智能路由配置
|
||||
URL_NORMALIZATION_ENABLED: bool = False # 是否启用智能路由映射功能
|
||||
|
||||
# 模型相关配置
|
||||
SEARCH_MODELS: List[str] = ["gemini-2.0-flash-exp"]
|
||||
IMAGE_MODELS: List[str] = ["gemini-2.0-flash-exp"]
|
||||
@@ -73,6 +77,11 @@ class Settings(BaseSettings):
|
||||
THINKING_MODELS: List[str] = []
|
||||
THINKING_BUDGET_MAP: Dict[str, float] = {}
|
||||
|
||||
# TTS相关配置
|
||||
TTS_MODEL: str = "gemini-2.5-flash-preview-tts"
|
||||
TTS_VOICE_NAME: str = "Zephyr"
|
||||
TTS_SPEED: str = "normal"
|
||||
|
||||
# 图像生成相关配置
|
||||
PAID_KEY: str = ""
|
||||
CREATE_IMAGE_MODEL: str = DEFAULT_CREATE_IMAGE_MODEL
|
||||
@@ -110,6 +119,7 @@ class Settings(BaseSettings):
|
||||
AUTO_DELETE_REQUEST_LOGS_DAYS: int = 30
|
||||
SAFETY_SETTINGS: List[Dict[str, str]] = DEFAULT_SAFETY_SETTINGS
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# 设置默认AUTH_TOKEN(如果未提供)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
数据库连接池模块
|
||||
"""
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
from databases import Database
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
@@ -20,9 +21,9 @@ if settings.DATABASE_TYPE == "sqlite":
|
||||
DATABASE_URL = f"sqlite:///{db_path}"
|
||||
elif settings.DATABASE_TYPE == "mysql":
|
||||
if settings.MYSQL_SOCKET:
|
||||
DATABASE_URL = f"mysql+pymysql://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@/{settings.MYSQL_DATABASE}?unix_socket={settings.MYSQL_SOCKET}"
|
||||
DATABASE_URL = f"mysql+pymysql://{settings.MYSQL_USER}:{quote_plus(settings.MYSQL_PASSWORD)}@/{settings.MYSQL_DATABASE}?unix_socket={settings.MYSQL_SOCKET}"
|
||||
else:
|
||||
DATABASE_URL = f"mysql+pymysql://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}"
|
||||
DATABASE_URL = f"mysql+pymysql://{settings.MYSQL_USER}:{quote_plus(settings.MYSQL_PASSWORD)}@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}"
|
||||
else:
|
||||
raise ValueError("Unsupported database type. Please set DATABASE_TYPE to 'sqlite' or 'mysql'.")
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@ class GenerationConfig(BaseModel):
|
||||
|
||||
|
||||
class SystemInstruction(BaseModel):
|
||||
role: str = "system"
|
||||
parts: List[Dict[str, Any]] | Dict[str, Any]
|
||||
role: Optional[str] = "system"
|
||||
parts: Union[List[Dict[str, Any]], Dict[str, Any]]
|
||||
|
||||
|
||||
class GeminiContent(BaseModel):
|
||||
role: str
|
||||
role: Optional[str] = None
|
||||
parts: List[Dict[str, Any]]
|
||||
|
||||
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
from typing import Union
|
||||
|
||||
|
||||
class ImageMetadata:
|
||||
def __init__(self, width: int, height: int, filename: str, size: int, url: str, delete_url: str | None = None):
|
||||
def __init__(self, width: int, height: int, filename: str, size: int, url: str, delete_url: Union[str, None] = None):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.filename = filename
|
||||
self.size = size
|
||||
self.url = url
|
||||
self.delete_url = delete_url
|
||||
|
||||
|
||||
class UploadResponse:
|
||||
def __init__(self, success: bool, code: str, message: str, data: ImageMetadata):
|
||||
self.success = success
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.data = data
|
||||
|
||||
|
||||
class ImageUploader:
|
||||
def upload(self, file: bytes, filename: str) -> UploadResponse:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
@@ -33,3 +33,10 @@ class ImageGenerationRequest(BaseModel):
|
||||
quality: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
response_format: Optional[str] = "url"
|
||||
|
||||
|
||||
class TTSRequest(BaseModel):
|
||||
model: str = "gemini-2.5-flash-preview-tts"
|
||||
input: str
|
||||
voice: str = "Kore"
|
||||
response_format: Optional[str] = "wav"
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi.responses import RedirectResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
# from app.middleware.request_logging_middleware import RequestLoggingMiddleware
|
||||
from app.middleware.smart_routing_middleware import SmartRoutingMiddleware
|
||||
from app.core.constants import API_VERSION
|
||||
from app.core.security import verify_auth_token
|
||||
from app.log.logger import get_middleware_logger
|
||||
@@ -52,6 +53,9 @@ def setup_middlewares(app: FastAPI) -> None:
|
||||
Args:
|
||||
app: FastAPI应用程序实例
|
||||
"""
|
||||
# 添加智能路由中间件(必须在认证中间件之前)
|
||||
app.add_middleware(SmartRoutingMiddleware)
|
||||
|
||||
# 添加认证中间件
|
||||
app.add_middleware(AuthMiddleware)
|
||||
|
||||
|
||||
210
app/middleware/smart_routing_middleware.py
Normal file
210
app/middleware/smart_routing_middleware.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from app.config.config import settings
|
||||
from app.log.logger import get_main_logger
|
||||
import re
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
class SmartRoutingMiddleware(BaseHTTPMiddleware):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
# 简化的路由规则 - 直接根据检测结果路由
|
||||
pass
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
if not settings.URL_NORMALIZATION_ENABLED:
|
||||
return await call_next(request)
|
||||
logger.debug(f"request: {request}")
|
||||
original_path = str(request.url.path)
|
||||
method = request.method
|
||||
|
||||
# 尝试修复URL
|
||||
fixed_path, fix_info = self.fix_request_url(original_path, method, request)
|
||||
|
||||
if fixed_path != original_path:
|
||||
logger.info(f"URL fixed: {method} {original_path} → {fixed_path}")
|
||||
if fix_info:
|
||||
logger.debug(f"Fix details: {fix_info}")
|
||||
|
||||
# 重写请求路径
|
||||
request.scope["path"] = fixed_path
|
||||
request.scope["raw_path"] = fixed_path.encode()
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
def fix_request_url(self, path: str, method: str, request: Request) -> tuple:
|
||||
"""简化的URL修复逻辑"""
|
||||
|
||||
# 首先检查是否已经是正确的格式,如果是则不处理
|
||||
if self.is_already_correct_format(path):
|
||||
return path, None
|
||||
|
||||
# 1. 最高优先级:包含generateContent → Gemini格式
|
||||
if "generatecontent" in path.lower() or "v1beta/models" in path.lower():
|
||||
return self.fix_gemini_by_operation(path, method, request)
|
||||
|
||||
# 2. 第二优先级:包含/openai/ → OpenAI格式
|
||||
if "/openai/" in path.lower():
|
||||
return self.fix_openai_by_operation(path, method)
|
||||
|
||||
# 3. 第三优先级:包含/v1/ → v1格式
|
||||
if "/v1/" in path.lower():
|
||||
return self.fix_v1_by_operation(path, method)
|
||||
|
||||
# 4. 第四优先级:包含/chat/completions → chat功能
|
||||
if "/chat/completions" in path.lower():
|
||||
return "/v1/chat/completions", {"type": "v1_chat"}
|
||||
|
||||
# 5. 默认:原样传递
|
||||
return path, None
|
||||
|
||||
def is_already_correct_format(self, path: str) -> bool:
|
||||
"""检查是否已经是正确的API格式"""
|
||||
# 检查是否已经是正确的端点格式
|
||||
correct_patterns = [
|
||||
r"^/v1beta/models/[^/:]+:(generate|streamGenerate)Content$", # Gemini原生
|
||||
r"^/gemini/v1beta/models/[^/:]+:(generate|streamGenerate)Content$", # Gemini带前缀
|
||||
r"^/v1beta/models$", # Gemini模型列表
|
||||
r"^/gemini/v1beta/models$", # Gemini带前缀的模型列表
|
||||
r"^/v1/(chat/completions|models|embeddings|images/generations|audio/speech)$", # v1格式
|
||||
r"^/openai/v1/(chat/completions|models|embeddings|images/generations|audio/speech)$", # OpenAI格式
|
||||
r"^/hf/v1/(chat/completions|models|embeddings|images/generations|audio/speech)$", # HF格式
|
||||
r"^/vertex-express/v1beta/models/[^/:]+:(generate|streamGenerate)Content$", # Vertex Express Gemini格式
|
||||
r"^/vertex-express/v1beta/models$", # Vertex Express模型列表
|
||||
r"^/vertex-express/v1/(chat/completions|models|embeddings|images/generations)$", # Vertex Express OpenAI格式
|
||||
]
|
||||
|
||||
for pattern in correct_patterns:
|
||||
if re.match(pattern, path):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def fix_gemini_by_operation(
|
||||
self, path: str, method: str, request: Request
|
||||
) -> tuple:
|
||||
"""根据Gemini操作修复,考虑端点偏好"""
|
||||
if method == "GET":
|
||||
return "/v1beta/models", {
|
||||
"role": "gemini_models",
|
||||
}
|
||||
|
||||
# 提取模型名称
|
||||
try:
|
||||
model_name = self.extract_model_name(path, request)
|
||||
except ValueError:
|
||||
# 无法提取模型名称,返回原路径不做处理
|
||||
return path, None
|
||||
|
||||
# 检测是否为流式请求
|
||||
is_stream = self.detect_stream_request(path, request)
|
||||
|
||||
# 检查是否有vertex-express偏好
|
||||
if "/vertex-express/" in path.lower():
|
||||
if is_stream:
|
||||
target_url = (
|
||||
f"/vertex-express/v1beta/models/{model_name}:streamGenerateContent"
|
||||
)
|
||||
else:
|
||||
target_url = (
|
||||
f"/vertex-express/v1beta/models/{model_name}:generateContent"
|
||||
)
|
||||
|
||||
fix_info = {
|
||||
"rule": (
|
||||
"vertex_express_generate"
|
||||
if not is_stream
|
||||
else "vertex_express_stream"
|
||||
),
|
||||
"preference": "vertex_express_format",
|
||||
"is_stream": is_stream,
|
||||
"model": model_name,
|
||||
}
|
||||
else:
|
||||
# 标准Gemini端点
|
||||
if is_stream:
|
||||
target_url = f"/v1beta/models/{model_name}:streamGenerateContent"
|
||||
else:
|
||||
target_url = f"/v1beta/models/{model_name}:generateContent"
|
||||
|
||||
fix_info = {
|
||||
"rule": "gemini_generate" if not is_stream else "gemini_stream",
|
||||
"preference": "gemini_format",
|
||||
"is_stream": is_stream,
|
||||
"model": model_name,
|
||||
}
|
||||
|
||||
return target_url, fix_info
|
||||
|
||||
def fix_openai_by_operation(self, path: str, method: str) -> tuple:
|
||||
"""根据操作类型修复OpenAI格式"""
|
||||
if method == "POST":
|
||||
if "chat" in path.lower() or "completion" in path.lower():
|
||||
return "/openai/v1/chat/completions", {"type": "openai_chat"}
|
||||
elif "embedding" in path.lower():
|
||||
return "/openai/v1/embeddings", {"type": "openai_embeddings"}
|
||||
elif "image" in path.lower():
|
||||
return "/openai/v1/images/generations", {"type": "openai_images"}
|
||||
elif "audio" in path.lower():
|
||||
return "/openai/v1/audio/speech", {"type": "openai_audio"}
|
||||
elif method == "GET":
|
||||
if "model" in path.lower():
|
||||
return "/openai/v1/models", {"type": "openai_models"}
|
||||
|
||||
return path, None
|
||||
|
||||
def fix_v1_by_operation(self, path: str, method: str) -> tuple:
|
||||
"""根据操作类型修复v1格式"""
|
||||
if method == "POST":
|
||||
if "chat" in path.lower() or "completion" in path.lower():
|
||||
return "/v1/chat/completions", {"type": "v1_chat"}
|
||||
elif "embedding" in path.lower():
|
||||
return "/v1/embeddings", {"type": "v1_embeddings"}
|
||||
elif "image" in path.lower():
|
||||
return "/v1/images/generations", {"type": "v1_images"}
|
||||
elif "audio" in path.lower():
|
||||
return "/v1/audio/speech", {"type": "v1_audio"}
|
||||
elif method == "GET":
|
||||
if "model" in path.lower():
|
||||
return "/v1/models", {"type": "v1_models"}
|
||||
|
||||
return path, None
|
||||
|
||||
def detect_stream_request(self, path: str, request: Request) -> bool:
|
||||
"""检测是否为流式请求"""
|
||||
# 1. 路径中包含stream关键词
|
||||
if "stream" in path.lower():
|
||||
return True
|
||||
|
||||
# 2. 查询参数
|
||||
if request.query_params.get("stream") == "true":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def extract_model_name(self, path: str, request: Request) -> str:
|
||||
"""从请求中提取模型名称,用于构建Gemini API URL"""
|
||||
# 1. 从请求体中提取
|
||||
try:
|
||||
if hasattr(request, "_body") and request._body:
|
||||
import json
|
||||
|
||||
body = json.loads(request._body.decode())
|
||||
if "model" in body and body["model"]:
|
||||
return body["model"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. 从查询参数中提取
|
||||
model_param = request.query_params.get("model")
|
||||
if model_param:
|
||||
return model_param
|
||||
|
||||
# 3. 从路径中提取(用于已包含模型名称的路径)
|
||||
match = re.search(r"/models/([^/:]+)", path, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
# 4. 如果无法提取模型名称,抛出异常
|
||||
raise ValueError("Unable to extract model name from request")
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.config.config import settings
|
||||
@@ -7,6 +7,7 @@ from app.domain.openai_models import (
|
||||
ChatRequest,
|
||||
EmbeddingRequest,
|
||||
ImageGenerationRequest,
|
||||
TTSRequest,
|
||||
)
|
||||
from app.handler.retry_handler import RetryHandler
|
||||
from app.handler.error_handler import handle_route_errors
|
||||
@@ -14,6 +15,7 @@ from app.log.logger import get_openai_logger
|
||||
from app.service.chat.openai_chat_service import OpenAIChatService
|
||||
from app.service.embedding.embedding_service import EmbeddingService
|
||||
from app.service.image.image_create_service import ImageCreateService
|
||||
from app.service.tts.tts_service import TTSService
|
||||
from app.service.key.key_manager import KeyManager, get_key_manager_instance
|
||||
from app.service.model.model_service import ModelService
|
||||
|
||||
@@ -24,6 +26,7 @@ security_service = SecurityService()
|
||||
model_service = ModelService()
|
||||
embedding_service = EmbeddingService()
|
||||
image_create_service = ImageCreateService()
|
||||
tts_service = TTSService()
|
||||
|
||||
|
||||
async def get_key_manager():
|
||||
@@ -41,6 +44,11 @@ async def get_openai_chat_service(key_manager: KeyManager = Depends(get_key_mana
|
||||
return OpenAIChatService(settings.BASE_URL, key_manager)
|
||||
|
||||
|
||||
async def get_tts_service():
|
||||
"""获取TTS服务实例"""
|
||||
return tts_service
|
||||
|
||||
|
||||
@router.get("/v1/models")
|
||||
@router.get("/hf/v1/models")
|
||||
async def list_models(
|
||||
@@ -147,3 +155,21 @@ async def get_keys_list(
|
||||
},
|
||||
"total": len(keys_status["valid_keys"]) + len(keys_status["invalid_keys"]),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/v1/audio/speech")
|
||||
@router.post("/hf/v1/audio/speech")
|
||||
async def text_to_speech(
|
||||
request: TTSRequest,
|
||||
_=Depends(security_service.verify_authorization),
|
||||
api_key: str = Depends(get_next_working_key_wrapper),
|
||||
tts_service: TTSService = Depends(get_tts_service),
|
||||
):
|
||||
"""处理 OpenAI TTS 请求。"""
|
||||
operation_name = "text_to_speech"
|
||||
async with handle_route_errors(logger, operation_name):
|
||||
logger.info(f"Handling TTS request for model: {request.model}")
|
||||
logger.debug(f"Request: \n{request.model_dump_json(indent=2)}")
|
||||
logger.info(f"Using API key: {api_key}")
|
||||
audio_data = await tts_service.create_tts(request, api_key)
|
||||
return Response(content=audio_data, media_type="audio/wav")
|
||||
|
||||
@@ -46,11 +46,14 @@ class GeminiApiClient(ApiClient):
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/models?key={api_key}"
|
||||
url = f"{self.base_url}/models?key={api_key}&pageSize=1000"
|
||||
try:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
@@ -69,8 +72,11 @@ class GeminiApiClient(ApiClient):
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy: {proxy_to_use}")
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/models/{model}:generateContent?key={api_key}"
|
||||
@@ -86,8 +92,11 @@ class GeminiApiClient(ApiClient):
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy: {proxy_to_use}")
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/models/{model}:streamGenerateContent?alt=sse&key={api_key}"
|
||||
@@ -109,7 +118,16 @@ class OpenaiApiClient(ApiClient):
|
||||
|
||||
async def get_models(self, api_key: str) -> Dict[str, Any]:
|
||||
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/openai/models"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
response = await client.get(url, headers=headers)
|
||||
@@ -120,11 +138,14 @@ class OpenaiApiClient(ApiClient):
|
||||
|
||||
async def generate_content(self, payload: Dict[str, Any], api_key: str) -> Dict[str, Any]:
|
||||
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
||||
|
||||
logger.info(f"settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY: {settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY}")
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy: {proxy_to_use}")
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/openai/chat/completions"
|
||||
@@ -137,11 +158,13 @@ class OpenaiApiClient(ApiClient):
|
||||
|
||||
async def stream_generate_content(self, payload: Dict[str, Any], api_key: str) -> AsyncGenerator[str, None]:
|
||||
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy: {proxy_to_use}")
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/openai/chat/completions"
|
||||
@@ -159,8 +182,11 @@ class OpenaiApiClient(ApiClient):
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy: {proxy_to_use}")
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/openai/embeddings"
|
||||
@@ -177,11 +203,14 @@ class OpenaiApiClient(ApiClient):
|
||||
|
||||
async def generate_images(self, payload: Dict[str, Any], api_key: str) -> Dict[str, Any]:
|
||||
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
||||
|
||||
|
||||
proxy_to_use = None
|
||||
if settings.PROXIES:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy: {proxy_to_use}")
|
||||
if settings.PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY:
|
||||
proxy_to_use = settings.PROXIES[hash(api_key) % len(settings.PROXIES)]
|
||||
else:
|
||||
proxy_to_use = random.choice(settings.PROXIES)
|
||||
logger.info(f"Using proxy for getting models: {proxy_to_use}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client:
|
||||
url = f"{self.base_url}/openai/images/generations"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from itertools import cycle
|
||||
from typing import Dict
|
||||
from typing import Dict, Union
|
||||
|
||||
from app.config.config import settings
|
||||
from app.log.logger import get_key_manager_logger
|
||||
@@ -178,19 +178,20 @@ class KeyManager:
|
||||
if self.api_keys:
|
||||
return self.api_keys[0]
|
||||
if not self.api_keys:
|
||||
logger.warning("API key list is empty, cannot get first valid key.")
|
||||
logger.warning(
|
||||
"API key list is empty, cannot get first valid key.")
|
||||
return ""
|
||||
return self.api_keys[0]
|
||||
|
||||
|
||||
_singleton_instance = None
|
||||
_singleton_lock = asyncio.Lock()
|
||||
_preserved_failure_counts: Dict[str, int] | None = None
|
||||
_preserved_vertex_failure_counts: Dict[str, int] | None = None
|
||||
_preserved_old_api_keys_for_reset: list | None = None
|
||||
_preserved_vertex_old_api_keys_for_reset: list | None = None
|
||||
_preserved_next_key_in_cycle: str | None = None
|
||||
_preserved_vertex_next_key_in_cycle: str | None = None
|
||||
_preserved_failure_counts: Union[Dict[str, int], None] = None
|
||||
_preserved_vertex_failure_counts: Union[Dict[str, int], None] = None
|
||||
_preserved_old_api_keys_for_reset: Union[list, None] = None
|
||||
_preserved_vertex_old_api_keys_for_reset: Union[list, None] = None
|
||||
_preserved_next_key_in_cycle: Union[str, None] = None
|
||||
_preserved_vertex_next_key_in_cycle: Union[str, None] = None
|
||||
|
||||
|
||||
async def get_key_manager_instance(
|
||||
@@ -252,7 +253,8 @@ async def get_key_manager_instance(
|
||||
_singleton_instance.vertex_key_failure_counts = (
|
||||
current_vertex_failure_counts
|
||||
)
|
||||
logger.info("Inherited failure counts for applicable Vertex keys.")
|
||||
logger.info(
|
||||
"Inherited failure counts for applicable Vertex keys.")
|
||||
_preserved_vertex_failure_counts = None
|
||||
|
||||
# 2. 调整 key_cycle 的起始点
|
||||
@@ -357,7 +359,7 @@ async def get_key_manager_instance(
|
||||
f"Error determining start key for new Vertex key cycle from preserved state: {e}. "
|
||||
"New cycle will start from the beginning."
|
||||
)
|
||||
|
||||
|
||||
if start_key_for_new_vertex_cycle and _singleton_instance.vertex_api_keys:
|
||||
try:
|
||||
target_idx = _singleton_instance.vertex_api_keys.index(
|
||||
@@ -380,7 +382,7 @@ async def get_key_manager_instance(
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error advancing new Vertex key cycle: {e}. Cycle will start from beginning."
|
||||
)
|
||||
)
|
||||
else:
|
||||
if _singleton_instance.vertex_api_keys:
|
||||
logger.info(
|
||||
@@ -418,7 +420,7 @@ async def reset_key_manager_instance():
|
||||
# 3. 保存 key_cycle 的下一个 key 提示
|
||||
try:
|
||||
if _singleton_instance.api_keys:
|
||||
_preserved_next_key_in_cycle = (
|
||||
_preserved_next_key_in_cycle = (
|
||||
await _singleton_instance.get_next_key()
|
||||
)
|
||||
else:
|
||||
@@ -427,9 +429,10 @@ async def reset_key_manager_instance():
|
||||
logger.warning(
|
||||
"Could not preserve next key hint: key cycle was empty or exhausted in old instance."
|
||||
)
|
||||
_preserved_next_key_in_cycle = None
|
||||
_preserved_next_key_in_cycle = None
|
||||
except Exception as e:
|
||||
logger.error(f"Error preserving next key hint during reset: {e}")
|
||||
logger.error(
|
||||
f"Error preserving next key hint during reset: {e}")
|
||||
_preserved_next_key_in_cycle = None
|
||||
|
||||
# 4. 保存 vertex_key_cycle 的下一个 key 提示
|
||||
@@ -443,12 +446,12 @@ async def reset_key_manager_instance():
|
||||
except StopIteration:
|
||||
logger.warning(
|
||||
"Could not preserve next key hint: Vertex key cycle was empty or exhausted in old instance."
|
||||
)
|
||||
)
|
||||
_preserved_vertex_next_key_in_cycle = None
|
||||
except Exception as e:
|
||||
logger.error(f"Error preserving next key hint during reset: {e}")
|
||||
logger.error(
|
||||
f"Error preserving next key hint during reset: {e}")
|
||||
_preserved_vertex_next_key_in_cycle = None
|
||||
|
||||
|
||||
_singleton_instance = None
|
||||
logger.info(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# app/service/stats_service.py
|
||||
|
||||
import datetime
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import and_, case, func, or_, select
|
||||
|
||||
@@ -195,10 +196,11 @@ class StatsService:
|
||||
return details
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get API call details for period '{period}': {e}")
|
||||
logger.error(
|
||||
f"Failed to get API call details for period '{period}': {e}")
|
||||
raise
|
||||
|
||||
async def get_key_usage_details_last_24h(self, key: str) -> dict | None:
|
||||
async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]:
|
||||
"""
|
||||
获取指定 API 密钥在过去 24 小时内按模型统计的调用次数。
|
||||
|
||||
@@ -218,7 +220,8 @@ class StatsService:
|
||||
try:
|
||||
query = (
|
||||
select(
|
||||
RequestLog.model_name, func.count(RequestLog.id).label("call_count")
|
||||
RequestLog.model_name, func.count(
|
||||
RequestLog.id).label("call_count")
|
||||
)
|
||||
.where(
|
||||
RequestLog.api_key == key,
|
||||
@@ -237,7 +240,8 @@ class StatsService:
|
||||
)
|
||||
return {}
|
||||
|
||||
usage_details = {row["model_name"]: row["call_count"] for row in results}
|
||||
usage_details = {row["model_name"]: row["call_count"]
|
||||
for row in results}
|
||||
logger.info(
|
||||
f"Successfully fetched usage details for key ending in ...{key[-4:]}: {usage_details}"
|
||||
)
|
||||
|
||||
94
app/service/tts/tts_service.py
Normal file
94
app/service/tts/tts_service.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import datetime
|
||||
import io
|
||||
import re
|
||||
import time
|
||||
import wave
|
||||
from typing import Optional
|
||||
|
||||
from google import genai
|
||||
|
||||
from app.config.config import settings
|
||||
from app.database.services import add_error_log, add_request_log
|
||||
from app.domain.openai_models import TTSRequest
|
||||
from app.log.logger import get_openai_logger
|
||||
|
||||
logger = get_openai_logger()
|
||||
|
||||
|
||||
def _create_wav_file(audio_data: bytes) -> bytes:
|
||||
"""Creates a WAV file in memory from raw audio data."""
|
||||
with io.BytesIO() as wav_file:
|
||||
with wave.open(wav_file, "wb") as wf:
|
||||
wf.setnchannels(1) # Mono
|
||||
wf.setsampwidth(2) # 16-bit
|
||||
wf.setframerate(24000) # 24kHz sample rate
|
||||
wf.writeframes(audio_data)
|
||||
return wav_file.getvalue()
|
||||
|
||||
|
||||
class TTSService:
|
||||
async def create_tts(self, request: TTSRequest, api_key: str) -> Optional[bytes]:
|
||||
"""
|
||||
使用 Google Gemini SDK 创建音频。
|
||||
"""
|
||||
start_time = time.perf_counter()
|
||||
request_datetime = datetime.datetime.now()
|
||||
is_success = False
|
||||
status_code = None
|
||||
response = None
|
||||
error_log_msg = ""
|
||||
try:
|
||||
client = genai.Client(api_key=api_key)
|
||||
response =await client.aio.models.generate_content(
|
||||
model=settings.TTS_MODEL,
|
||||
contents=f"Speak in a {settings.TTS_SPEED} speed voice: {request.input}",
|
||||
config={
|
||||
"response_modalities": ["Audio"],
|
||||
"speech_config": {
|
||||
"voice_config": {
|
||||
"prebuilt_voice_config": {
|
||||
"voice_name": settings.TTS_VOICE_NAME
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
if (
|
||||
response.candidates
|
||||
and response.candidates[0].content.parts
|
||||
and response.candidates[0].content.parts[0].inline_data
|
||||
):
|
||||
raw_audio_data = response.candidates[0].content.parts[0].inline_data.data
|
||||
is_success = True
|
||||
status_code = 200
|
||||
return _create_wav_file(raw_audio_data)
|
||||
except Exception as e:
|
||||
is_success = False
|
||||
error_log_msg = f"Generic error: {e}"
|
||||
logger.error(f"An error occurred in TTSService: {error_log_msg}")
|
||||
match = re.search(r"status code (\d+)", str(e))
|
||||
if match:
|
||||
status_code = int(match.group(1))
|
||||
else:
|
||||
status_code = 500
|
||||
raise
|
||||
finally:
|
||||
end_time = time.perf_counter()
|
||||
latency_ms = int((end_time - start_time) * 1000)
|
||||
if not is_success:
|
||||
await add_error_log(
|
||||
gemini_key=api_key,
|
||||
model_name=settings.TTS_MODEL,
|
||||
error_type="google-tts",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=request.input
|
||||
)
|
||||
await add_request_log(
|
||||
model_name=settings.TTS_MODEL,
|
||||
api_key=api_key,
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
)
|
||||
@@ -12,7 +12,7 @@ const PROXY_REGEX =
|
||||
/(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g;
|
||||
const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_]{50}/g; // 新增 Vertex API Key 正则
|
||||
const MASKED_VALUE = "••••••••";
|
||||
|
||||
|
||||
// DOM Elements - Global Scope for frequently accessed elements
|
||||
const safetySettingsContainer = document.getElementById(
|
||||
"SAFETY_SETTINGS_container"
|
||||
@@ -31,7 +31,7 @@ const bulkDeleteProxyModal = document.getElementById("bulkDeleteProxyModal");
|
||||
const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput");
|
||||
const resetConfirmModal = document.getElementById("resetConfirmModal");
|
||||
const configForm = document.getElementById("configForm"); // Added for frequent use
|
||||
|
||||
|
||||
// Vertex API Key Modal Elements
|
||||
const vertexApiKeyModal = document.getElementById("vertexApiKeyModal");
|
||||
const vertexApiKeyBulkInput = document.getElementById("vertexApiKeyBulkInput");
|
||||
@@ -41,7 +41,7 @@ const bulkDeleteVertexApiKeyModal = document.getElementById(
|
||||
const bulkDeleteVertexApiKeyInput = document.getElementById(
|
||||
"bulkDeleteVertexApiKeyInput"
|
||||
);
|
||||
|
||||
|
||||
// Model Helper Modal Elements
|
||||
const modelHelperModal = document.getElementById("modelHelperModal");
|
||||
const modelHelperTitleElement = document.getElementById("modelHelperTitle");
|
||||
@@ -384,7 +384,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
|
||||
initializeSensitiveFields(); // Initialize sensitive field handling
|
||||
|
||||
|
||||
// Vertex API Key Modal Elements and Events
|
||||
const addVertexApiKeyBtn = document.getElementById("addVertexApiKeyBtn");
|
||||
const closeVertexApiKeyModalBtn = document.getElementById(
|
||||
@@ -408,7 +408,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const confirmBulkDeleteVertexApiKeyBtn = document.getElementById(
|
||||
"confirmBulkDeleteVertexApiKeyBtn"
|
||||
);
|
||||
|
||||
|
||||
if (addVertexApiKeyBtn) {
|
||||
addVertexApiKeyBtn.addEventListener("click", () => {
|
||||
openModal(vertexApiKeyModal);
|
||||
@@ -428,7 +428,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"click",
|
||||
handleBulkAddVertexApiKeys
|
||||
);
|
||||
|
||||
|
||||
if (bulkDeleteVertexApiKeyBtn) {
|
||||
bulkDeleteVertexApiKeyBtn.addEventListener("click", () => {
|
||||
openModal(bulkDeleteVertexApiKeyModal);
|
||||
@@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"click",
|
||||
handleBulkDeleteVertexApiKeys
|
||||
);
|
||||
|
||||
|
||||
// Model Helper Modal Event Listeners
|
||||
if (closeModelHelperModalBtn) {
|
||||
closeModelHelperModalBtn.addEventListener("click", () =>
|
||||
@@ -765,7 +765,7 @@ async function initConfig() {
|
||||
FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS: 5,
|
||||
// --- 结束:处理假流式配置的默认值 ---
|
||||
};
|
||||
|
||||
|
||||
populateForm(defaultConfig);
|
||||
if (configForm) {
|
||||
// Ensure form exists
|
||||
@@ -1177,7 +1177,7 @@ function handleBulkDeleteProxies() {
|
||||
}
|
||||
bulkDeleteProxyInput.value = "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the bulk addition of Vertex API keys from the modal input.
|
||||
*/
|
||||
@@ -1192,10 +1192,10 @@ function handleBulkAddVertexApiKeys() {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const bulkText = vertexApiKeyBulkInput.value;
|
||||
const extractedKeys = bulkText.match(VERTEX_API_KEY_REGEX) || [];
|
||||
|
||||
|
||||
const currentKeyInputs = vertexApiKeyContainer.querySelectorAll(
|
||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
||||
);
|
||||
@@ -1206,16 +1206,16 @@ function handleBulkAddVertexApiKeys() {
|
||||
: input.value;
|
||||
})
|
||||
.filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE);
|
||||
|
||||
|
||||
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
|
||||
const uniqueKeys = Array.from(combinedKeys);
|
||||
|
||||
|
||||
vertexApiKeyContainer.innerHTML = ""; // Clear existing items
|
||||
|
||||
|
||||
uniqueKeys.forEach((key) => {
|
||||
addArrayItemWithValue("VERTEX_API_KEYS", key); // VERTEX_API_KEYS are sensitive
|
||||
});
|
||||
|
||||
|
||||
// Ensure new sensitive inputs are masked
|
||||
const newKeyInputs = vertexApiKeyContainer.querySelectorAll(
|
||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
||||
@@ -1229,7 +1229,7 @@ function handleBulkAddVertexApiKeys() {
|
||||
input.dispatchEvent(focusoutEvent);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
closeModal(vertexApiKeyModal);
|
||||
showNotification(
|
||||
`添加/更新了 ${uniqueKeys.length} 个唯一 Vertex 密钥`,
|
||||
@@ -1237,7 +1237,7 @@ function handleBulkAddVertexApiKeys() {
|
||||
);
|
||||
vertexApiKeyBulkInput.value = "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the bulk deletion of Vertex API keys based on input from the modal.
|
||||
*/
|
||||
@@ -1252,15 +1252,15 @@ function handleBulkDeleteVertexApiKeys() {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const bulkText = bulkDeleteVertexApiKeyInput.value;
|
||||
if (!bulkText.trim()) {
|
||||
showNotification("请粘贴需要删除的 Vertex API 密钥", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const keysToDelete = new Set(bulkText.match(VERTEX_API_KEY_REGEX) || []);
|
||||
|
||||
|
||||
if (keysToDelete.size === 0) {
|
||||
showNotification(
|
||||
"未在输入内容中提取到有效的 Vertex API 密钥格式",
|
||||
@@ -1268,10 +1268,10 @@ function handleBulkDeleteVertexApiKeys() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const keyItems = vertexApiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`);
|
||||
let deleteCount = 0;
|
||||
|
||||
|
||||
keyItems.forEach((item) => {
|
||||
const input = item.querySelector(
|
||||
`.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}`
|
||||
@@ -1286,9 +1286,9 @@ function handleBulkDeleteVertexApiKeys() {
|
||||
deleteCount++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
closeModal(bulkDeleteVertexApiKeyModal);
|
||||
|
||||
|
||||
if (deleteCount > 0) {
|
||||
showNotification(`成功删除了 ${deleteCount} 个匹配的 Vertex 密钥`, "success");
|
||||
} else {
|
||||
@@ -1296,7 +1296,7 @@ function handleBulkDeleteVertexApiKeys() {
|
||||
}
|
||||
bulkDeleteVertexApiKeyInput.value = "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Switches the active configuration tab.
|
||||
* @param {string} tabId - The ID of the tab to switch to.
|
||||
@@ -1442,7 +1442,7 @@ function addArrayItemWithValue(key, value) {
|
||||
const isSensitive =
|
||||
key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断
|
||||
const modelId = isThinkingModel ? generateUUID() : null;
|
||||
|
||||
|
||||
const arrayItem = document.createElement("div");
|
||||
arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`;
|
||||
if (isThinkingModel) {
|
||||
@@ -1535,14 +1535,14 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
|
||||
valueInput.value = isNaN(intValue) ? 0 : intValue;
|
||||
valueInput.placeholder = "预算 (整数)";
|
||||
valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`;
|
||||
valueInput.min = 0;
|
||||
valueInput.max = 24576;
|
||||
valueInput.min = -1;
|
||||
valueInput.max = 32767;
|
||||
valueInput.addEventListener("input", function () {
|
||||
let val = this.value.replace(/[^0-9]/g, "");
|
||||
let val = this.value.replace(/[^0-9-]/g, "");
|
||||
if (val !== "") {
|
||||
val = parseInt(val, 10);
|
||||
if (val < 0) val = 0;
|
||||
if (val > 24576) val = 24576;
|
||||
if (val < -1) val = -1;
|
||||
if (val > 32767) val = 32767;
|
||||
}
|
||||
this.value = val; // Corrected variable name
|
||||
});
|
||||
|
||||
@@ -745,6 +745,13 @@ endblock %} {% block head_extra_styles %}
|
||||
>
|
||||
模型配置
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn px-5 py-2 rounded-full font-medium text-sm transition-all duration-200"
|
||||
data-tab="tts"
|
||||
style="background-color: #f8fafc !important; color: #64748b !important; border: 2px solid #e2e8f0 !important; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1) !important; font-weight: 500 !important;"
|
||||
>
|
||||
TTS 配置
|
||||
</button>
|
||||
<button
|
||||
class="tab-btn px-5 py-2 rounded-full font-medium text-sm transition-all duration-200"
|
||||
data-tab="image"
|
||||
@@ -935,7 +942,33 @@ endblock %} {% block head_extra_styles %}
|
||||
/>
|
||||
<small class="text-gray-500 mt-1 block">Vertex Express API的基础URL</small>
|
||||
</div>
|
||||
|
||||
<!-- 智能路由配置 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<label
|
||||
for="URL_NORMALIZATION_ENABLED"
|
||||
class="font-semibold text-gray-700"
|
||||
>启用智能路由映射</label
|
||||
>
|
||||
<div
|
||||
class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="URL_NORMALIZATION_ENABLED"
|
||||
id="URL_NORMALIZATION_ENABLED"
|
||||
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
|
||||
/>
|
||||
<label
|
||||
for="URL_NORMALIZATION_ENABLED"
|
||||
class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-gray-500 mt-1 block">
|
||||
自动客户端请求的url拼接为正确格式(仅保证正常聊天,出现问题请关闭)
|
||||
</small>
|
||||
</div>
|
||||
<!-- 最大失败次数 -->
|
||||
<div class="mb-6">
|
||||
<label
|
||||
@@ -1021,6 +1054,33 @@ endblock %} {% block head_extra_styles %}
|
||||
socks5://host:port。点击按钮可批量添加或删除。</small
|
||||
>
|
||||
</div>
|
||||
<!-- 代理使用策略 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<label
|
||||
for="PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY"
|
||||
class="font-semibold text-gray-700"
|
||||
>是否开启固定代理策略</label
|
||||
>
|
||||
<div
|
||||
class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY"
|
||||
id="PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY"
|
||||
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
|
||||
/>
|
||||
<label
|
||||
for="PROXIES_USE_CONSISTENCY_HASH_BY_API_KEY"
|
||||
class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-gray-500 mt-1 block"
|
||||
>开启后,对于每一个API_KEY将根据算法从代理列表中选取同一个代理IP,防止一个API_KEY同时被多个IP访问,也同时防止了一个IP访问了过多的API_KEY。</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型相关配置 -->
|
||||
@@ -1272,7 +1332,7 @@ endblock %} {% block head_extra_styles %}
|
||||
</div> -->
|
||||
<small class="text-gray-500 mt-1 block"
|
||||
>为每个思考模型设置预算(整数,最大值
|
||||
24576),此项与上方模型列表自动关联。</small
|
||||
32767),此项与上方模型列表自动关联。</small
|
||||
>
|
||||
</div>
|
||||
<!-- 安全设置 -->
|
||||
@@ -1317,11 +1377,97 @@ endblock %} {% block head_extra_styles %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像生成相关配置 -->
|
||||
<div class="config-section" id="image-section">
|
||||
|
||||
<!-- TTS配置 -->
|
||||
<div class="config-section" id="tts-section">
|
||||
<h2
|
||||
class="text-xl font-bold mb-6 pb-3 border-b flex items-center gap-2 text-gray-800 border-violet-300 border-opacity-30"
|
||||
>
|
||||
<i class="fas fa-volume-up text-violet-400"></i> TTS 相关配置
|
||||
</h2>
|
||||
|
||||
<!-- TTS 模型 -->
|
||||
<div class="mb-6">
|
||||
<label for="TTS_MODEL" class="block font-semibold mb-2 text-gray-700"
|
||||
>TTS 模型</label
|
||||
>
|
||||
<select
|
||||
id="TTS_MODEL"
|
||||
name="TTS_MODEL"
|
||||
class="w-full px-4 py-3 rounded-lg form-select-themed"
|
||||
>
|
||||
<option value="gemini-2.5-flash-preview-tts">gemini-2.5-flash-preview-tts</option>
|
||||
<option value="gemini-2.5-pro-preview-tts">gemini-2.5-pro-preview-tts</option>
|
||||
</select>
|
||||
<small class="text-gray-500 mt-1 block">用于TTS的模型</small>
|
||||
</div>
|
||||
|
||||
<!-- TTS 语音名称 -->
|
||||
<div class="mb-6">
|
||||
<label for="TTS_VOICE_NAME" class="block font-semibold mb-2 text-gray-700"
|
||||
>TTS 语音名称</label
|
||||
>
|
||||
<select
|
||||
id="TTS_VOICE_NAME"
|
||||
name="TTS_VOICE_NAME"
|
||||
class="w-full px-4 py-3 rounded-lg form-select-themed"
|
||||
>
|
||||
<option value="Zephyr">Zephyr (明亮)</option>
|
||||
<option value="Puck">Puck (欢快)</option>
|
||||
<option value="Charon">Charon (信息丰富)</option>
|
||||
<option value="Kore">Kore (坚定)</option>
|
||||
<option value="Fenrir">Fenrir (易激动)</option>
|
||||
<option value="Leda">Leda (年轻)</option>
|
||||
<option value="Orus">Orus (坚定)</option>
|
||||
<option value="Aoede">Aoede (轻松)</option>
|
||||
<option value="Callirhoe">Callirhoe (随和)</option>
|
||||
<option value="Autonoe">Autonoe (明亮)</option>
|
||||
<option value="Enceladus">Enceladus (呼吸感)</option>
|
||||
<option value="Iapetus">Iapetus (清晰)</option>
|
||||
<option value="Umbriel">Umbriel (随和)</option>
|
||||
<option value="Algieba">Algieba (平滑)</option>
|
||||
<option value="Despina">Despina (平滑)</option>
|
||||
<option value="Erinome">Erinome (清晰)</option>
|
||||
<option value="Algenib">Algenib (沙哑)</option>
|
||||
<option value="Rasalgethi">Rasalgethi (信息丰富)</option>
|
||||
<option value="Laomedeia">Laomedeia (欢快)</option>
|
||||
<option value="Achernar">Achernar (轻柔)</option>
|
||||
<option value="Alnilam">Alnilam (坚定)</option>
|
||||
<option value="Schedar">Schedar (平稳)</option>
|
||||
<option value="Gacrux">Gacrux (成熟)</option>
|
||||
<option value="Pulcherrima">Pulcherrima (向前)</option>
|
||||
<option value="Achird">Achird (友好)</option>
|
||||
<option value="Zubenelgenubi">Zubenelgenubi (休闲)</option>
|
||||
<option value="Vindemiatrix">Vindemiatrix (温柔)</option>
|
||||
<option value="Sadachbia">Sadachbia (活泼)</option>
|
||||
<option value="Sadaltager">Sadaltager (博学)</option>
|
||||
<option value="Sulafat">Sulafat (温暖)</option>
|
||||
</select>
|
||||
<small class="text-gray-500 mt-1 block">TTS 的语音名称,控制风格、语调、口音和节奏</small>
|
||||
</div>
|
||||
|
||||
<!-- TTS 语速 -->
|
||||
<div class="mb-6">
|
||||
<label for="TTS_SPEED" class="block font-semibold mb-2 text-gray-700"
|
||||
>TTS 语速</label
|
||||
>
|
||||
<select
|
||||
id="TTS_SPEED"
|
||||
name="TTS_SPEED"
|
||||
class="w-full px-4 py-3 rounded-lg form-select-themed"
|
||||
>
|
||||
<option value="slow">慢</option>
|
||||
<option value="normal">正常</option>
|
||||
<option value="fast">快</option>
|
||||
</select>
|
||||
<small class="text-gray-500 mt-1 block">选择 TTS 的语速</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像生成相关配置 -->
|
||||
<div class="config-section" id="image-section">
|
||||
<h2
|
||||
class="text-xl font-bold mb-6 pb-3 border-b flex items-center gap-2 text-gray-800 border-violet-300 border-opacity-30"
|
||||
>
|
||||
<i class="fas fa-image text-violet-400"></i> 图像生成配置
|
||||
</h2>
|
||||
@@ -1458,12 +1604,12 @@ endblock %} {% block head_extra_styles %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流式输出优化器配置 -->
|
||||
<!-- 流式输出优化配置 -->
|
||||
<div class="config-section" id="stream-section">
|
||||
<h2
|
||||
class="text-xl font-bold mb-6 pb-3 border-b flex items-center gap-2 text-gray-800 border-violet-300 border-opacity-30"
|
||||
>
|
||||
<i class="fas fa-stream text-violet-400"></i> 流式输出优化器
|
||||
<i class="fas fa-stream text-violet-400"></i> 流式输出相关配置
|
||||
</h2>
|
||||
|
||||
<!-- 启用流式输出优化 -->
|
||||
@@ -2328,7 +2474,7 @@ endblock %} {% block head_extra_styles %}
|
||||
let val = this.value.replace(/[^0-9]/g, "");
|
||||
if (val !== "") {
|
||||
val = parseInt(val, 10);
|
||||
if (val > 24576) val = 24576;
|
||||
if (val > 32767) val = 32767;
|
||||
}
|
||||
this.value = val;
|
||||
|
||||
|
||||
71
files/dataocean.svg
Normal file
71
files/dataocean.svg
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 603 103" style="enable-background:new 0 0 603 103;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#0080FF;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#0080FF;}
|
||||
</style>
|
||||
<g id="XMLID_2369_">
|
||||
<g id="XMLID_2638_">
|
||||
<g id="XMLID_2639_">
|
||||
<g>
|
||||
<g id="XMLID_44_">
|
||||
<g id="XMLID_48_">
|
||||
<path id="XMLID_49_" class="st0" d="M52.1,102.1l0-19.6c20.8,0,36.8-20.6,28.9-42.4C78,32,71.6,25.5,63.5,22.6
|
||||
c-21.8-7.9-42.4,8.1-42.4,28.9c0,0,0,0,0,0l-19.6,0c0-33.1,32-58.9,66.7-48.1c15.2,4.7,27.2,16.8,31.9,31.9
|
||||
C110.9,70.1,85.2,102.1,52.1,102.1z"/>
|
||||
</g>
|
||||
<polygon id="XMLID_47_" class="st1" points="52.1,82.5 32.6,82.5 32.6,63 32.6,63 52.1,63 52.1,63 "/>
|
||||
<polygon id="XMLID_46_" class="st1" points="32.6,97.5 17.6,97.5 17.6,97.5 17.6,82.5 32.6,82.5 32.6,97.5 "/>
|
||||
<polygon id="XMLID_45_" class="st1" points="17.6,82.5 5,82.5 5,82.5 5,70 5,70 17.6,70 17.6,70 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="XMLID_2370_">
|
||||
<path id="XMLID_2635_" class="st0" d="M181.5,30.2c-5.8-4-13-6.1-21.4-6.1h-18.3v58.1h18.3c8.4,0,15.6-2.1,21.4-6.4
|
||||
c3.2-2.2,5.7-5.4,7.4-9.3c1.7-3.9,2.6-8.5,2.6-13.7c0-5.1-0.9-9.7-2.6-13.6C187.2,35.4,184.7,32.3,181.5,30.2z M152.5,34h5.8
|
||||
c6.4,0,11.7,1.3,15.7,3.7c4.4,2.7,6.7,7.8,6.7,15.1c0,7.6-2.3,12.9-6.7,15.8h0c-3.8,2.5-9.1,3.8-15.6,3.8h-5.8V34z"/>
|
||||
<path id="XMLID_2634_" class="st0" d="M204.3,23.4c-1.8,0-3.3,0.6-4.5,1.8c-1.2,1.2-1.9,2.7-1.9,4.4c0,1.8,0.6,3.3,1.9,4.5
|
||||
c1.2,1.2,2.7,1.9,4.5,1.9c1.8,0,3.3-0.6,4.5-1.9c1.2-1.2,1.9-2.8,1.9-4.5c0-1.8-0.6-3.3-1.9-4.4C207.6,24,206,23.4,204.3,23.4z"/>
|
||||
<rect id="XMLID_2564_" x="199" y="41.3" class="st0" width="10.3" height="41"/>
|
||||
<path id="XMLID_2561_" class="st0" d="M246.8,44.7c-3.1-2.8-6.6-4.4-10.3-4.4c-5.7,0-10.4,2-14.1,5.8c-3.7,3.8-5.5,8.8-5.5,14.7
|
||||
c0,5.8,1.8,10.7,5.5,14.7c3.7,3.8,8.4,5.8,14.1,5.8c4,0,7.4-1.1,10.2-3.3V79c0,3.4-0.9,6-2.7,7.9c-1.8,1.8-4.3,2.7-7.4,2.7
|
||||
c-4.8,0-7.7-1.9-11.4-6.8l-7,6.7l0.2,0.3c1.5,2.1,3.8,4.2,6.9,6.2c3.1,2,6.9,3,11.5,3c6.1,0,11.1-1.9,14.7-5.6
|
||||
c3.7-3.7,5.5-8.7,5.5-14.9V41.3h-10.1V44.7z M244.1,68.9c-1.8,2-4.1,3-7.1,3c-3,0-5.3-1-7-3c-1.8-2-2.7-4.7-2.7-8
|
||||
c0-3.3,0.9-6.1,2.7-8.1c1.8-2,4.1-3.1,7-3.1c3,0,5.3,1,7.1,3.1c1.8,2,2.7,4.8,2.7,8.1C246.8,64.2,245.8,66.9,244.1,68.9z"/>
|
||||
<rect id="XMLID_2560_" x="265.7" y="41.3" class="st0" width="10.3" height="41"/>
|
||||
<path id="XMLID_2552_" class="st0" d="M271,23.4c-1.8,0-3.3,0.6-4.5,1.8c-1.2,1.2-1.9,2.7-1.9,4.4c0,1.8,0.6,3.3,1.9,4.5
|
||||
c1.2,1.2,2.7,1.9,4.5,1.9c1.8,0,3.3-0.6,4.5-1.9c1.2-1.2,1.9-2.8,1.9-4.5c0-1.8-0.6-3.3-1.9-4.4C274.3,24,272.7,23.4,271,23.4z"/>
|
||||
<path id="XMLID_2509_" class="st0" d="M298.6,30.3h-10.1v11.1h-5.9v9.4h5.9v17c0,5.3,1.1,9.1,3.2,11.3c2.1,2.2,5.8,3.3,11.1,3.3
|
||||
c1.7,0,3.4-0.1,5-0.2l0.5,0v-9.4l-3.5,0.2c-2.5,0-4.1-0.4-4.9-1.3c-0.8-0.9-1.2-2.7-1.2-5.4V50.7h9.6v-9.4h-9.6V30.3z"/>
|
||||
<rect id="XMLID_2508_" x="356.5" y="24.1" class="st0" width="10.3" height="58.1"/>
|
||||
<path id="XMLID_2470_" class="st0" d="M470.9,67.6c-1.8,2.1-3.7,3.9-5.2,4.8v0c-1.4,0.9-3.2,1.4-5.3,1.4c-3,0-5.5-1.1-7.5-3.4
|
||||
c-2-2.3-3-5.2-3-8.7s1-6.4,2.9-8.6c2-2.3,4.4-3.4,7.4-3.4c3.3,0,6.8,2.1,9.8,5.6l6.8-6.5l0,0c-4.4-5.8-10.1-8.5-16.9-8.5
|
||||
c-5.7,0-10.6,2.1-14.6,6.1c-4,4-6,9.2-6,15.3s2,11.2,6,15.3c4,4.1,8.9,6.1,14.6,6.1c7.5,0,13.5-3.2,17.5-9.1L470.9,67.6z"/>
|
||||
<path id="XMLID_2460_" class="st0" d="M513.2,47c-1.5-2-3.5-3.7-5.9-4.9c-2.5-1.2-5.3-1.8-8.5-1.8c-5.8,0-10.5,2.1-14,6.3
|
||||
c-3.4,4.2-5.2,9.3-5.2,15.4c0,6.2,1.9,11.3,5.7,15.3c3.7,3.9,8.8,5.9,14.9,5.9c6.9,0,12.7-2.8,16.9-8.4l0.2-0.3l-6.7-6.5l0,0
|
||||
c-0.6,0.8-1.5,1.6-2.3,2.4c-1,1-2,1.7-3,2.2c-1.5,0.8-3.3,1.1-5.2,1.1c-2.9,0-5.2-0.8-7-2.5c-1.7-1.5-2.7-3.6-2.9-6.2h27.3
|
||||
l0.1-3.8c0-2.7-0.4-5.2-1.1-7.6C515.8,51.3,514.7,49.1,513.2,47z M490.7,56.7c0.5-2,1.4-3.6,2.7-4.9c1.4-1.4,3.2-2.1,5.4-2.1
|
||||
c2.5,0,4.4,0.7,5.7,2.1c1.2,1.3,1.9,2.9,2.1,4.8H490.7z"/>
|
||||
<path id="XMLID_2456_" class="st0" d="M552.8,44.4L552.8,44.4c-3.1-2.7-7.4-4-12.8-4c-3.4,0-6.6,0.8-9.5,2.2
|
||||
c-2.7,1.4-5.3,3.6-7,6.6l0.1,0.1l6.6,6.3c2.7-4.3,5.7-5.8,9.7-5.8c2.2,0,3.9,0.6,5.3,1.7c1.4,1.1,2,2.6,2,4.4v2
|
||||
c-2.6-0.8-5.1-1.2-7.6-1.2c-5.1,0-9.3,1.2-12.4,3.6c-3.1,2.4-4.7,5.9-4.7,10.2c0,3.8,1.3,7,4,9.3c2.7,2.2,6,3.4,9.9,3.4
|
||||
c3.9,0,7.6-1.6,10.9-4.3v3.4h10.1V55.9C557.6,51,556,47.1,552.8,44.4z M534.5,66.6c1.2-0.8,2.8-1.2,4.9-1.2c2.5,0,5.1,0.5,7.8,1.5
|
||||
v4C545,73,542,74,538.3,74c-1.8,0-3.2-0.4-4.1-1.2c-0.9-0.8-1.4-1.7-1.4-3C532.8,68.5,533.4,67.4,534.5,66.6z"/>
|
||||
<path id="XMLID_2454_" class="st0" d="M597.2,45.2c-2.9-3.2-6.9-4.8-12-4.8c-4.1,0-7.4,1.2-9.9,3.5v-2.5h-10.1v41h10.3V59.7
|
||||
c0-3.1,0.7-5.6,2.2-7.3c1.5-1.8,3.4-2.6,6.1-2.6c2.3,0,4.1,0.8,5.4,2.3c1.3,1.6,2,3.7,2,6.4v23.7h10.3V58.5
|
||||
C601.5,52.9,600.1,48.4,597.2,45.2z"/>
|
||||
<path id="XMLID_2450_" class="st0" d="M343.6,44.4L343.6,44.4c-3.1-2.7-7.4-4-12.8-4c-3.4,0-6.6,0.8-9.5,2.2
|
||||
c-2.7,1.4-5.3,3.6-7,6.6l0.1,0.1l6.6,6.3c2.7-4.3,5.7-5.8,9.7-5.8c2.2,0,3.9,0.6,5.3,1.7c1.4,1.1,2,2.6,2,4.4v2
|
||||
c-2.6-0.8-5.1-1.2-7.6-1.2c-5.1,0-9.3,1.2-12.4,3.6c-3.1,2.4-4.7,5.9-4.7,10.2c0,3.8,1.3,7,4,9.3c2.7,2.2,6,3.4,9.9,3.4
|
||||
c3.9,0,7.6-1.6,10.9-4.3v3.4h10.1V55.9C348.3,51,346.7,47.1,343.6,44.4z M325.3,66.6c1.2-0.8,2.8-1.2,4.9-1.2
|
||||
c2.5,0,5.1,0.5,7.8,1.5v4c-2.2,2.1-5.2,3.1-8.9,3.1c-1.8,0-3.2-0.4-4.1-1.2c-0.9-0.8-1.4-1.7-1.4-3
|
||||
C323.6,68.5,324.1,67.4,325.3,66.6z"/>
|
||||
<path id="XMLID_2371_" class="st0" d="M404.2,83.1c-16.5,0-30-13.4-30-30s13.4-30,30-30c16.5,0,30,13.4,30,30
|
||||
S420.7,83.1,404.2,83.1z M404.2,33.8c-10.7,0-19.4,8.7-19.4,19.4s8.7,19.4,19.4,19.4c10.7,0,19.4-8.7,19.4-19.4
|
||||
S414.9,33.8,404.2,33.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
Reference in New Issue
Block a user