25:05:10 03:17:05 v0.7.1
Browse files- README.en.md +115 -128
- VERSION +1 -1
- constant/azure.go +5 -0
- constant/env.go +1 -1
- controller/channel-billing.go +25 -0
- controller/option.go +9 -0
- controller/user.go +25 -1
- BT.md → docs/installation/BT.md +3 -3
- Midjourney.md → docs/models/Midjourney.md +0 -0
- Rerank.md → docs/models/Rerank.md +0 -0
- Suno.md → docs/models/Suno.md +0 -0
- dto/dalle.go +10 -11
- dto/openai_response.go +30 -25
- middleware/distributor.go +1 -0
- middleware/model-rate-limit.go +35 -19
- model/option.go +6 -0
- model/user.go +1 -0
- relay/channel/api_request.go +67 -2
- relay/channel/gemini/relay-gemini.go +1 -0
- relay/channel/openai/adaptor.go +7 -8
- relay/channel/openai/helper.go +7 -0
- relay/channel/openai/relay-openai.go +26 -112
- relay/channel/openai/relay_responses.go +119 -0
- relay/channel/vertex/adaptor.go +1 -1
- relay/channel/xai/adaptor.go +20 -12
- relay/channel/xai/dto.go +13 -0
- relay/common/relay_info.go +49 -8
- relay/helper/common.go +15 -7
- relay/helper/model_mapped.go +33 -4
- relay/helper/price.go +1 -1
- relay/helper/stream_scanner.go +2 -1
- relay/relay-image.go +2 -2
- relay/relay-responses.go +5 -5
- relay/relay-text.go +47 -0
- service/cf_worker.go +1 -1
- setting/operation_setting/tools.go +57 -0
- setting/rate_limit.go +58 -0
- setting/system_setting.go +1 -0
- web/src/components/LogsTable.js +13 -1
- web/src/components/PersonalSetting.js +46 -9
- web/src/components/RateLimitSetting.js +9 -4
- web/src/components/SystemSetting.js +23 -4
- web/src/helpers/render.js +142 -45
- web/src/i18n/locales/en.json +3 -2
- web/src/pages/Channel/EditChannel.js +56 -43
- web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js +44 -0
README.en.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<div align="center">
|
| 2 |
|
| 3 |

|
| 4 |
|
| 5 |
# New API
|
| 6 |
|
| 7 |
-
🍥 Next
|
| 8 |
|
| 9 |
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
| 10 |
|
|
@@ -33,171 +36,155 @@
|
|
| 33 |
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
|
| 34 |
|
| 35 |
> [!IMPORTANT]
|
| 36 |
-
> -
|
| 37 |
-
> -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
## ✨ Key Features
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
6.
|
| 49 |
-
7.
|
| 50 |
-
8.
|
| 51 |
-
9.
|
| 52 |
-
10.
|
| 53 |
-
11.
|
| 54 |
-
12.
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
1. Set `Prompt Cache Ratio` in `System Settings -> Operation Settings`
|
| 70 |
-
2. Set `Prompt Cache Ratio` in channel settings, range 0-1 (e.g., 0.5 means 50% charge on cache hits)
|
| 71 |
3. Supported channels:
|
| 72 |
- [x] OpenAI
|
| 73 |
-
- [x] Azure
|
| 74 |
- [x] DeepSeek
|
| 75 |
-
- [
|
| 76 |
|
| 77 |
## Model Support
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
- `
|
| 94 |
-
- `
|
| 95 |
-
- `
|
| 96 |
-
- `
|
| 97 |
-
- `
|
| 98 |
-
- `
|
| 99 |
-
- `
|
| 100 |
-
- `
|
| 101 |
-
- `
|
| 102 |
-
- `
|
| 103 |
-
- `
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
## Deployment
|
| 106 |
|
|
|
|
|
|
|
| 107 |
> [!TIP]
|
| 108 |
-
> Latest Docker image: `calciumion/new-api:latest`
|
| 109 |
-
> Default account: root, password: 123456
|
| 110 |
|
| 111 |
-
### Multi-
|
| 112 |
-
-
|
| 113 |
-
- If
|
| 114 |
|
| 115 |
-
### Requirements
|
| 116 |
-
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
|
| 117 |
-
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6
|
| 118 |
|
| 119 |
-
### Deployment
|
| 120 |
-
Install BT Panel (**version 9.2.0** or above) from [BT Panel Official Website](https://www.bt.cn/new/download.html), choose the stable version script to download and install.
|
| 121 |
-
After installation, log in to BT Panel and click Docker in the menu bar. First-time access will prompt to install Docker service. Click Install Now and follow the prompts to complete installation.
|
| 122 |
-
After installation, find **New-API** in the app store, click install, configure basic options to complete installation.
|
| 123 |
-
[Pictorial Guide](BT.md)
|
| 124 |
|
| 125 |
-
### Docker
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
### Using Docker Compose (Recommended)
|
| 128 |
```shell
|
| 129 |
-
#
|
| 130 |
git clone https://github.com/Calcium-Ion/new-api.git
|
| 131 |
cd new-api
|
| 132 |
# Edit docker-compose.yml as needed
|
| 133 |
-
# nano docker-compose.yml
|
| 134 |
-
# vim docker-compose.yml
|
| 135 |
# Start
|
| 136 |
docker-compose up -d
|
| 137 |
```
|
| 138 |
|
| 139 |
-
####
|
| 140 |
```shell
|
| 141 |
-
|
| 142 |
-
docker-compose up -d
|
| 143 |
-
```
|
| 144 |
-
|
| 145 |
-
### Direct Docker Image Usage
|
| 146 |
-
```shell
|
| 147 |
-
# SQLite deployment:
|
| 148 |
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
| 149 |
|
| 150 |
-
#
|
| 151 |
-
# Example:
|
| 152 |
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
| 153 |
```
|
| 154 |
|
| 155 |
-
##
|
| 156 |
-
``
|
| 157 |
-
# Pull the latest image
|
| 158 |
-
docker pull calciumion/new-api:latest
|
| 159 |
-
# Stop and remove the old container
|
| 160 |
-
docker stop new-api
|
| 161 |
-
docker rm new-api
|
| 162 |
-
# Run the new container with the same parameters as before
|
| 163 |
-
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
| 164 |
-
```
|
| 165 |
|
| 166 |
-
|
| 167 |
-
``
|
| 168 |
-
|
| 169 |
-
```
|
| 170 |
|
| 171 |
-
##
|
| 172 |
-
Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**.
|
| 173 |
-
If retry is enabled, the system will automatically use the next priority channel for the same request after a failed request.
|
| 174 |
-
|
| 175 |
-
### Cache Configuration
|
| 176 |
-
1. `REDIS_CONN_STRING`: Use Redis as cache
|
| 177 |
-
+ Example: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
| 178 |
-
2. `MEMORY_CACHE_ENABLED`: Enable memory cache, default `false`
|
| 179 |
-
+ Example: `MEMORY_CACHE_ENABLED=true`
|
| 180 |
-
|
| 181 |
-
### Why Some Errors Don't Retry
|
| 182 |
-
Error codes 400, 504, 524 won't retry
|
| 183 |
-
### To Enable Retry for 400
|
| 184 |
-
In `Channel->Edit`, set `Status Code Override` to:
|
| 185 |
-
```json
|
| 186 |
-
{
|
| 187 |
-
"400": "500"
|
| 188 |
-
}
|
| 189 |
-
```
|
| 190 |
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
## Related Projects
|
| 196 |
- [One API](https://github.com/songquanpeng/one-api): Original project
|
| 197 |
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
|
| 198 |
-
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-
|
| 199 |
-
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
## 🌟 Star History
|
| 202 |
|
| 203 |
-
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
|
|
|
| 1 |
+
<p align="right">
|
| 2 |
+
<a href="./README.md">中文</a> | <strong>English</strong>
|
| 3 |
+
</p>
|
| 4 |
<div align="center">
|
| 5 |
|
| 6 |

|
| 7 |
|
| 8 |
# New API
|
| 9 |
|
| 10 |
+
🍥 Next-Generation Large Model Gateway and AI Asset Management System
|
| 11 |
|
| 12 |
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
| 13 |
|
|
|
|
| 36 |
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
|
| 37 |
|
| 38 |
> [!IMPORTANT]
|
| 39 |
+
> - This project is for personal learning purposes only, with no guarantee of stability or technical support.
|
| 40 |
+
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**, and must not use it for illegal purposes.
|
| 41 |
+
> - According to the [《Interim Measures for the Management of Generative Artificial Intelligence Services》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), please do not provide any unregistered generative AI services to the public in China.
|
| 42 |
+
|
| 43 |
+
## 📚 Documentation
|
| 44 |
+
|
| 45 |
+
For detailed documentation, please visit our official Wiki: [https://docs.newapi.pro/](https://docs.newapi.pro/)
|
| 46 |
|
| 47 |
## ✨ Key Features
|
| 48 |
|
| 49 |
+
New API offers a wide range of features, please refer to [Features Introduction](https://docs.newapi.pro/wiki/features-introduction) for details:
|
| 50 |
+
|
| 51 |
+
1. 🎨 Brand new UI interface
|
| 52 |
+
2. 🌍 Multi-language support
|
| 53 |
+
3. 💰 Online recharge functionality (YiPay)
|
| 54 |
+
4. 🔍 Support for querying usage quotas with keys (works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
| 55 |
+
5. 🔄 Compatible with the original One API database
|
| 56 |
+
6. 💵 Support for pay-per-use model pricing
|
| 57 |
+
7. ⚖️ Support for weighted random channel selection
|
| 58 |
+
8. 📈 Data dashboard (console)
|
| 59 |
+
9. 🔒 Token grouping and model restrictions
|
| 60 |
+
10. 🤖 Support for more authorization login methods (LinuxDO, Telegram, OIDC)
|
| 61 |
+
11. 🔄 Support for Rerank models (Cohere and Jina), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
|
| 62 |
+
12. ⚡ Support for OpenAI Realtime API (including Azure channels), [API Documentation](https://docs.newapi.pro/api/openai-realtime)
|
| 63 |
+
13. ⚡ Support for Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
| 64 |
+
14. Support for entering chat interface via /chat2link route
|
| 65 |
+
15. 🧠 Support for setting reasoning effort through model name suffixes:
|
| 66 |
+
1. OpenAI o-series models
|
| 67 |
+
- Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`)
|
| 68 |
+
- Add `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`)
|
| 69 |
+
- Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`)
|
| 70 |
+
2. Claude thinking models
|
| 71 |
+
- Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
|
| 72 |
+
16. 🔄 Thinking-to-content functionality
|
| 73 |
+
17. 🔄 Model rate limiting for users
|
| 74 |
+
18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
|
| 75 |
+
1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings`
|
| 76 |
+
2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit
|
|
|
|
|
|
|
| 77 |
3. Supported channels:
|
| 78 |
- [x] OpenAI
|
| 79 |
+
- [x] Azure
|
| 80 |
- [x] DeepSeek
|
| 81 |
+
- [x] Claude
|
| 82 |
|
| 83 |
## Model Support
|
| 84 |
+
|
| 85 |
+
This version supports multiple models, please refer to [API Documentation-Relay Interface](https://docs.newapi.pro/api) for details:
|
| 86 |
+
|
| 87 |
+
1. Third-party models **gpts** (gpt-4-gizmo-*)
|
| 88 |
+
2. Third-party channel [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [API Documentation](https://docs.newapi.pro/api/midjourney-proxy-image)
|
| 89 |
+
3. Third-party channel [Suno API](https://github.com/Suno-API/Suno-API) interface, [API Documentation](https://docs.newapi.pro/api/suno-music)
|
| 90 |
+
4. Custom channels, supporting full call address input
|
| 91 |
+
5. Rerank models ([Cohere](https://cohere.ai/) and [Jina](https://jina.ai/)), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
|
| 92 |
+
6. Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
| 93 |
+
7. Dify, currently only supports chatflow
|
| 94 |
+
|
| 95 |
+
## Environment Variable Configuration
|
| 96 |
+
|
| 97 |
+
For detailed configuration instructions, please refer to [Installation Guide-Environment Variables Configuration](https://docs.newapi.pro/installation/environment-variables):
|
| 98 |
+
|
| 99 |
+
- `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
|
| 100 |
+
- `STREAMING_TIMEOUT`: Streaming response timeout, default is 60 seconds
|
| 101 |
+
- `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true`
|
| 102 |
+
- `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true`
|
| 103 |
+
- `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true`
|
| 104 |
+
- `GET_MEDIA_TOKEN_NOT_STREAM`: Whether to count image tokens in non-streaming cases, default is `true`
|
| 105 |
+
- `UPDATE_TASK`: Whether to update asynchronous tasks (Midjourney, Suno), default is `true`
|
| 106 |
+
- `COHERE_SAFETY_SETTING`: Cohere model safety settings, options are `NONE`, `CONTEXTUAL`, `STRICT`, default is `NONE`
|
| 107 |
+
- `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16`
|
| 108 |
+
- `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20`
|
| 109 |
+
- `CRYPTO_SECRET`: Encryption key used for encrypting database content
|
| 110 |
+
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview`
|
| 111 |
+
- `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes
|
| 112 |
+
- `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2`
|
| 113 |
|
| 114 |
## Deployment
|
| 115 |
|
| 116 |
+
For detailed deployment guides, please refer to [Installation Guide-Deployment Methods](https://docs.newapi.pro/installation):
|
| 117 |
+
|
| 118 |
> [!TIP]
|
| 119 |
+
> Latest Docker image: `calciumion/new-api:latest`
|
|
|
|
| 120 |
|
| 121 |
+
### Multi-machine Deployment Considerations
|
| 122 |
+
- Environment variable `SESSION_SECRET` must be set, otherwise login status will be inconsistent across multiple machines
|
| 123 |
+
- If sharing Redis, `CRYPTO_SECRET` must be set, otherwise Redis content cannot be accessed across multiple machines
|
| 124 |
|
| 125 |
+
### Deployment Requirements
|
| 126 |
+
- Local database (default): SQLite (Docker deployment must mount the `/data` directory)
|
| 127 |
+
- Remote database: MySQL version >= 5.7.8, PgSQL version >= 9.6
|
| 128 |
|
| 129 |
+
### Deployment Methods
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
+
#### Using BaoTa Panel Docker Feature
|
| 132 |
+
Install BaoTa Panel (version **9.2.0** or above), find **New-API** in the application store and install it.
|
| 133 |
+
[Tutorial with images](./docs/BT.md)
|
| 134 |
|
| 135 |
+
#### Using Docker Compose (Recommended)
|
| 136 |
```shell
|
| 137 |
+
# Download the project
|
| 138 |
git clone https://github.com/Calcium-Ion/new-api.git
|
| 139 |
cd new-api
|
| 140 |
# Edit docker-compose.yml as needed
|
|
|
|
|
|
|
| 141 |
# Start
|
| 142 |
docker-compose up -d
|
| 143 |
```
|
| 144 |
|
| 145 |
+
#### Using Docker Image Directly
|
| 146 |
```shell
|
| 147 |
+
# Using SQLite
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
| 149 |
|
| 150 |
+
# Using MySQL
|
|
|
|
| 151 |
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
| 152 |
```
|
| 153 |
|
| 154 |
+
## Channel Retry and Cache
|
| 155 |
+
Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings`. It is **recommended to enable caching**.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
### Cache Configuration Method
|
| 158 |
+
1. `REDIS_CONN_STRING`: Set Redis as cache
|
| 159 |
+
2. `MEMORY_CACHE_ENABLED`: Enable memory cache (no need to set manually if Redis is set)
|
|
|
|
| 160 |
|
| 161 |
+
## API Documentation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
+
For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api):
|
| 164 |
+
|
| 165 |
+
- [Chat API](https://docs.newapi.pro/api/openai-chat)
|
| 166 |
+
- [Image API](https://docs.newapi.pro/api/openai-image)
|
| 167 |
+
- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank)
|
| 168 |
+
- [Realtime API](https://docs.newapi.pro/api/openai-realtime)
|
| 169 |
+
- [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat)
|
| 170 |
|
| 171 |
## Related Projects
|
| 172 |
- [One API](https://github.com/songquanpeng/one-api): Original project
|
| 173 |
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
|
| 174 |
+
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-generation AI one-stop B/C-end solution
|
| 175 |
+
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key
|
| 176 |
+
|
| 177 |
+
Other projects based on New API:
|
| 178 |
+
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of New API
|
| 179 |
+
- [VoAPI](https://github.com/VoAPI/VoAPI): Frontend beautified version based on New API
|
| 180 |
+
|
| 181 |
+
## Help and Support
|
| 182 |
+
|
| 183 |
+
If you have any questions, please refer to [Help and Support](https://docs.newapi.pro/support):
|
| 184 |
+
- [Community Interaction](https://docs.newapi.pro/support/community-interaction)
|
| 185 |
+
- [Issue Feedback](https://docs.newapi.pro/support/feedback-issues)
|
| 186 |
+
- [FAQ](https://docs.newapi.pro/support/faq)
|
| 187 |
|
| 188 |
## 🌟 Star History
|
| 189 |
|
| 190 |
+
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
v0.7.
|
|
|
|
| 1 |
+
v0.7.1
|
constant/azure.go
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package constant
|
| 2 |
+
|
| 3 |
+
import "time"
|
| 4 |
+
|
| 5 |
+
var AzureNoRemoveDotTime = time.Date(2025, time.May, 10, 0, 0, 0, 0, time.UTC).Unix()
|
constant/env.go
CHANGED
|
@@ -31,7 +31,7 @@ func InitEnv() {
|
|
| 31 |
GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
|
| 32 |
GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
|
| 33 |
UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
| 34 |
-
AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "
|
| 35 |
GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
| 36 |
NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
|
| 37 |
NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
|
|
|
|
| 31 |
GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
|
| 32 |
GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
|
| 33 |
UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
| 34 |
+
AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2025-04-01-preview")
|
| 35 |
GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
| 36 |
NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
|
| 37 |
NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
|
controller/channel-billing.go
CHANGED
|
@@ -108,6 +108,13 @@ type DeepSeekUsageResponse struct {
|
|
| 108 |
} `json:"balance_infos"`
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
// GetAuthHeader get auth header
|
| 112 |
func GetAuthHeader(token string) http.Header {
|
| 113 |
h := http.Header{}
|
|
@@ -281,6 +288,22 @@ func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) {
|
|
| 281 |
return response.TotalAvailable, nil
|
| 282 |
}
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
| 285 |
baseURL := common.ChannelBaseURLs[channel.Type]
|
| 286 |
if channel.GetBaseURL() == "" {
|
|
@@ -307,6 +330,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
|
|
| 307 |
return updateChannelSiliconFlowBalance(channel)
|
| 308 |
case common.ChannelTypeDeepSeek:
|
| 309 |
return updateChannelDeepSeekBalance(channel)
|
|
|
|
|
|
|
| 310 |
default:
|
| 311 |
return 0, errors.New("尚未实现")
|
| 312 |
}
|
|
|
|
| 108 |
} `json:"balance_infos"`
|
| 109 |
}
|
| 110 |
|
| 111 |
+
type OpenRouterCreditResponse struct {
|
| 112 |
+
Data struct {
|
| 113 |
+
TotalCredits float64 `json:"total_credits"`
|
| 114 |
+
TotalUsage float64 `json:"total_usage"`
|
| 115 |
+
} `json:"data"`
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
// GetAuthHeader get auth header
|
| 119 |
func GetAuthHeader(token string) http.Header {
|
| 120 |
h := http.Header{}
|
|
|
|
| 288 |
return response.TotalAvailable, nil
|
| 289 |
}
|
| 290 |
|
| 291 |
+
func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
|
| 292 |
+
url := "https://openrouter.ai/api/v1/credits"
|
| 293 |
+
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 294 |
+
if err != nil {
|
| 295 |
+
return 0, err
|
| 296 |
+
}
|
| 297 |
+
response := OpenRouterCreditResponse{}
|
| 298 |
+
err = json.Unmarshal(body, &response)
|
| 299 |
+
if err != nil {
|
| 300 |
+
return 0, err
|
| 301 |
+
}
|
| 302 |
+
balance := response.Data.TotalCredits - response.Data.TotalUsage
|
| 303 |
+
channel.UpdateBalance(balance)
|
| 304 |
+
return balance, nil
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
| 308 |
baseURL := common.ChannelBaseURLs[channel.Type]
|
| 309 |
if channel.GetBaseURL() == "" {
|
|
|
|
| 330 |
return updateChannelSiliconFlowBalance(channel)
|
| 331 |
case common.ChannelTypeDeepSeek:
|
| 332 |
return updateChannelDeepSeekBalance(channel)
|
| 333 |
+
case common.ChannelTypeOpenRouter:
|
| 334 |
+
return updateChannelOpenRouterBalance(channel)
|
| 335 |
default:
|
| 336 |
return 0, errors.New("尚未实现")
|
| 337 |
}
|
controller/option.go
CHANGED
|
@@ -110,6 +110,15 @@ func UpdateOption(c *gin.Context) {
|
|
| 110 |
})
|
| 111 |
return
|
| 112 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
}
|
| 115 |
err = model.UpdateOption(option.Key, option.Value)
|
|
|
|
| 110 |
})
|
| 111 |
return
|
| 112 |
}
|
| 113 |
+
case "ModelRequestRateLimitGroup":
|
| 114 |
+
err = setting.CheckModelRequestRateLimitGroup(option.Value)
|
| 115 |
+
if err != nil {
|
| 116 |
+
c.JSON(http.StatusOK, gin.H{
|
| 117 |
+
"success": false,
|
| 118 |
+
"message": err.Error(),
|
| 119 |
+
})
|
| 120 |
+
return
|
| 121 |
+
}
|
| 122 |
|
| 123 |
}
|
| 124 |
err = model.UpdateOption(option.Key, option.Value)
|
controller/user.go
CHANGED
|
@@ -592,7 +592,14 @@ func UpdateSelf(c *gin.Context) {
|
|
| 592 |
user.Password = "" // rollback to what it should be
|
| 593 |
cleanUser.Password = ""
|
| 594 |
}
|
| 595 |
-
updatePassword := user.Password
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
if err := cleanUser.Update(updatePassword); err != nil {
|
| 597 |
c.JSON(http.StatusOK, gin.H{
|
| 598 |
"success": false,
|
|
@@ -608,6 +615,23 @@ func UpdateSelf(c *gin.Context) {
|
|
| 608 |
return
|
| 609 |
}
|
| 610 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
func DeleteUser(c *gin.Context) {
|
| 612 |
id, err := strconv.Atoi(c.Param("id"))
|
| 613 |
if err != nil {
|
|
|
|
| 592 |
user.Password = "" // rollback to what it should be
|
| 593 |
cleanUser.Password = ""
|
| 594 |
}
|
| 595 |
+
updatePassword, err := checkUpdatePassword(user.OriginalPassword, user.Password, cleanUser.Id)
|
| 596 |
+
if err != nil {
|
| 597 |
+
c.JSON(http.StatusOK, gin.H{
|
| 598 |
+
"success": false,
|
| 599 |
+
"message": err.Error(),
|
| 600 |
+
})
|
| 601 |
+
return
|
| 602 |
+
}
|
| 603 |
if err := cleanUser.Update(updatePassword); err != nil {
|
| 604 |
c.JSON(http.StatusOK, gin.H{
|
| 605 |
"success": false,
|
|
|
|
| 615 |
return
|
| 616 |
}
|
| 617 |
|
| 618 |
+
func checkUpdatePassword(originalPassword string, newPassword string, userId int) (updatePassword bool, err error) {
|
| 619 |
+
var currentUser *model.User
|
| 620 |
+
currentUser, err = model.GetUserById(userId, true)
|
| 621 |
+
if err != nil {
|
| 622 |
+
return
|
| 623 |
+
}
|
| 624 |
+
if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) {
|
| 625 |
+
err = fmt.Errorf("原密码错误")
|
| 626 |
+
return
|
| 627 |
+
}
|
| 628 |
+
if newPassword == "" {
|
| 629 |
+
return
|
| 630 |
+
}
|
| 631 |
+
updatePassword = true
|
| 632 |
+
return
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
func DeleteUser(c *gin.Context) {
|
| 636 |
id, err := strconv.Atoi(c.Param("id"))
|
| 637 |
if err != nil {
|
BT.md → docs/installation/BT.md
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
密钥为环境变量SESSION_SECRET
|
| 2 |
-
|
| 3 |
-

|
|
|
|
| 1 |
+
密钥为环境变量SESSION_SECRET
|
| 2 |
+
|
| 3 |
+

|
Midjourney.md → docs/models/Midjourney.md
RENAMED
|
File without changes
|
Rerank.md → docs/models/Rerank.md
RENAMED
|
File without changes
|
Suno.md → docs/models/Suno.md
RENAMED
|
File without changes
|
dto/dalle.go
CHANGED
|
@@ -1,17 +1,16 @@
|
|
| 1 |
package dto
|
| 2 |
|
| 3 |
-
import "encoding/json"
|
| 4 |
-
|
| 5 |
type ImageRequest struct {
|
| 6 |
-
Model string
|
| 7 |
-
Prompt string
|
| 8 |
-
N int
|
| 9 |
-
Size string
|
| 10 |
-
Quality string
|
| 11 |
-
ResponseFormat string
|
| 12 |
-
Style string
|
| 13 |
-
User string
|
| 14 |
-
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
type ImageResponse struct {
|
|
|
|
| 1 |
package dto
|
| 2 |
|
|
|
|
|
|
|
| 3 |
type ImageRequest struct {
|
| 4 |
+
Model string `json:"model"`
|
| 5 |
+
Prompt string `json:"prompt" binding:"required"`
|
| 6 |
+
N int `json:"n,omitempty"`
|
| 7 |
+
Size string `json:"size,omitempty"`
|
| 8 |
+
Quality string `json:"quality,omitempty"`
|
| 9 |
+
ResponseFormat string `json:"response_format,omitempty"`
|
| 10 |
+
Style string `json:"style,omitempty"`
|
| 11 |
+
User string `json:"user,omitempty"`
|
| 12 |
+
Moderation string `json:"moderation,omitempty"`
|
| 13 |
+
Background string `json:"background,omitempty"`
|
| 14 |
}
|
| 15 |
|
| 16 |
type ImageResponse struct {
|
dto/openai_response.go
CHANGED
|
@@ -195,28 +195,28 @@ type OutputTokenDetails struct {
|
|
| 195 |
}
|
| 196 |
|
| 197 |
type OpenAIResponsesResponse struct {
|
| 198 |
-
ID string
|
| 199 |
-
Object string
|
| 200 |
-
CreatedAt int
|
| 201 |
-
Status string
|
| 202 |
-
Error *OpenAIError
|
| 203 |
-
IncompleteDetails *IncompleteDetails
|
| 204 |
-
Instructions string
|
| 205 |
-
MaxOutputTokens int
|
| 206 |
-
Model string
|
| 207 |
-
Output []ResponsesOutput
|
| 208 |
-
ParallelToolCalls bool
|
| 209 |
-
PreviousResponseID string
|
| 210 |
-
Reasoning *Reasoning
|
| 211 |
-
Store bool
|
| 212 |
-
Temperature float64
|
| 213 |
-
ToolChoice string
|
| 214 |
-
Tools []
|
| 215 |
-
TopP float64
|
| 216 |
-
Truncation string
|
| 217 |
-
Usage *Usage
|
| 218 |
-
User json.RawMessage
|
| 219 |
-
Metadata json.RawMessage
|
| 220 |
}
|
| 221 |
|
| 222 |
type IncompleteDetails struct {
|
|
@@ -238,8 +238,12 @@ type ResponsesOutputContent struct {
|
|
| 238 |
}
|
| 239 |
|
| 240 |
const (
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
)
|
| 244 |
|
| 245 |
const (
|
|
@@ -250,6 +254,7 @@ const (
|
|
| 250 |
// ResponsesStreamResponse 用于处理 /v1/responses 流式响应
|
| 251 |
type ResponsesStreamResponse struct {
|
| 252 |
Type string `json:"type"`
|
| 253 |
-
Response *OpenAIResponsesResponse `json:"response"`
|
| 254 |
Delta string `json:"delta,omitempty"`
|
|
|
|
| 255 |
}
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
type OpenAIResponsesResponse struct {
|
| 198 |
+
ID string `json:"id"`
|
| 199 |
+
Object string `json:"object"`
|
| 200 |
+
CreatedAt int `json:"created_at"`
|
| 201 |
+
Status string `json:"status"`
|
| 202 |
+
Error *OpenAIError `json:"error,omitempty"`
|
| 203 |
+
IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"`
|
| 204 |
+
Instructions string `json:"instructions"`
|
| 205 |
+
MaxOutputTokens int `json:"max_output_tokens"`
|
| 206 |
+
Model string `json:"model"`
|
| 207 |
+
Output []ResponsesOutput `json:"output"`
|
| 208 |
+
ParallelToolCalls bool `json:"parallel_tool_calls"`
|
| 209 |
+
PreviousResponseID string `json:"previous_response_id"`
|
| 210 |
+
Reasoning *Reasoning `json:"reasoning"`
|
| 211 |
+
Store bool `json:"store"`
|
| 212 |
+
Temperature float64 `json:"temperature"`
|
| 213 |
+
ToolChoice string `json:"tool_choice"`
|
| 214 |
+
Tools []ResponsesToolsCall `json:"tools"`
|
| 215 |
+
TopP float64 `json:"top_p"`
|
| 216 |
+
Truncation string `json:"truncation"`
|
| 217 |
+
Usage *Usage `json:"usage"`
|
| 218 |
+
User json.RawMessage `json:"user"`
|
| 219 |
+
Metadata json.RawMessage `json:"metadata"`
|
| 220 |
}
|
| 221 |
|
| 222 |
type IncompleteDetails struct {
|
|
|
|
| 238 |
}
|
| 239 |
|
| 240 |
const (
|
| 241 |
+
BuildInToolWebSearchPreview = "web_search_preview"
|
| 242 |
+
BuildInToolFileSearch = "file_search"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
const (
|
| 246 |
+
BuildInCallWebSearchCall = "web_search_call"
|
| 247 |
)
|
| 248 |
|
| 249 |
const (
|
|
|
|
| 254 |
// ResponsesStreamResponse 用于处理 /v1/responses 流式响应
|
| 255 |
type ResponsesStreamResponse struct {
|
| 256 |
Type string `json:"type"`
|
| 257 |
+
Response *OpenAIResponsesResponse `json:"response,omitempty"`
|
| 258 |
Delta string `json:"delta,omitempty"`
|
| 259 |
+
Item *ResponsesOutput `json:"item,omitempty"`
|
| 260 |
}
|
middleware/distributor.go
CHANGED
|
@@ -213,6 +213,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
|
| 213 |
c.Set("channel_id", channel.Id)
|
| 214 |
c.Set("channel_name", channel.Name)
|
| 215 |
c.Set("channel_type", channel.Type)
|
|
|
|
| 216 |
c.Set("channel_setting", channel.GetSetting())
|
| 217 |
c.Set("param_override", channel.GetParamOverride())
|
| 218 |
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
|
|
|
| 213 |
c.Set("channel_id", channel.Id)
|
| 214 |
c.Set("channel_name", channel.Name)
|
| 215 |
c.Set("channel_type", channel.Type)
|
| 216 |
+
c.Set("channel_create_time", channel.CreatedTime)
|
| 217 |
c.Set("channel_setting", channel.GetSetting())
|
| 218 |
c.Set("param_override", channel.GetParamOverride())
|
| 219 |
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
middleware/model-rate-limit.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
| 6 |
"net/http"
|
| 7 |
"one-api/common"
|
| 8 |
"one-api/common/limiter"
|
|
|
|
| 9 |
"one-api/setting"
|
| 10 |
"strconv"
|
| 11 |
"time"
|
|
@@ -93,25 +94,27 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
|
|
| 93 |
}
|
| 94 |
|
| 95 |
//2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
// 4. 处理请求
|
|
@@ -173,6 +176,19 @@ func ModelRequestRateLimit() func(c *gin.Context) {
|
|
| 173 |
totalMaxCount := setting.ModelRequestRateLimitCount
|
| 174 |
successMaxCount := setting.ModelRequestRateLimitSuccessCount
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
// 根据存储类型选择并执行限流处理器
|
| 177 |
if common.RedisEnabled {
|
| 178 |
redisRateLimitHandler(duration, totalMaxCount, successMaxCount)(c)
|
|
|
|
| 6 |
"net/http"
|
| 7 |
"one-api/common"
|
| 8 |
"one-api/common/limiter"
|
| 9 |
+
"one-api/constant"
|
| 10 |
"one-api/setting"
|
| 11 |
"strconv"
|
| 12 |
"time"
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
//2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器
|
| 97 |
+
if totalMaxCount > 0 {
|
| 98 |
+
totalKey := fmt.Sprintf("rateLimit:%s", userId)
|
| 99 |
+
// 初始化
|
| 100 |
+
tb := limiter.New(ctx, rdb)
|
| 101 |
+
allowed, err = tb.Allow(
|
| 102 |
+
ctx,
|
| 103 |
+
totalKey,
|
| 104 |
+
limiter.WithCapacity(int64(totalMaxCount)*duration),
|
| 105 |
+
limiter.WithRate(int64(totalMaxCount)),
|
| 106 |
+
limiter.WithRequested(duration),
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
if err != nil {
|
| 110 |
+
fmt.Println("检查总请求数限制失败:", err.Error())
|
| 111 |
+
abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed")
|
| 112 |
+
return
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
if !allowed {
|
| 116 |
+
abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount))
|
| 117 |
+
}
|
| 118 |
}
|
| 119 |
|
| 120 |
// 4. 处理请求
|
|
|
|
| 176 |
totalMaxCount := setting.ModelRequestRateLimitCount
|
| 177 |
successMaxCount := setting.ModelRequestRateLimitSuccessCount
|
| 178 |
|
| 179 |
+
// 获取分组
|
| 180 |
+
group := c.GetString("token_group")
|
| 181 |
+
if group == "" {
|
| 182 |
+
group = c.GetString(constant.ContextKeyUserGroup)
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
//获取分组的限流配置
|
| 186 |
+
groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(group)
|
| 187 |
+
if found {
|
| 188 |
+
totalMaxCount = groupTotalCount
|
| 189 |
+
successMaxCount = groupSuccessCount
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
// 根据存储类型选择并执行限流处理器
|
| 193 |
if common.RedisEnabled {
|
| 194 |
redisRateLimitHandler(duration, totalMaxCount, successMaxCount)(c)
|
model/option.go
CHANGED
|
@@ -67,6 +67,7 @@ func InitOptionMap() {
|
|
| 67 |
common.OptionMap["ServerAddress"] = ""
|
| 68 |
common.OptionMap["WorkerUrl"] = setting.WorkerUrl
|
| 69 |
common.OptionMap["WorkerValidKey"] = setting.WorkerValidKey
|
|
|
|
| 70 |
common.OptionMap["PayAddress"] = ""
|
| 71 |
common.OptionMap["CustomCallbackAddress"] = ""
|
| 72 |
common.OptionMap["EpayId"] = ""
|
|
@@ -92,6 +93,7 @@ func InitOptionMap() {
|
|
| 92 |
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
| 93 |
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
| 94 |
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
|
|
|
| 95 |
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
| 96 |
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
| 97 |
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
|
@@ -256,6 +258,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|
| 256 |
setting.StopOnSensitiveEnabled = boolValue
|
| 257 |
case "SMTPSSLEnabled":
|
| 258 |
common.SMTPSSLEnabled = boolValue
|
|
|
|
|
|
|
| 259 |
}
|
| 260 |
}
|
| 261 |
switch key {
|
|
@@ -338,6 +342,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|
| 338 |
setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
|
| 339 |
case "ModelRequestRateLimitSuccessCount":
|
| 340 |
setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
|
|
|
|
|
|
|
| 341 |
case "RetryTimes":
|
| 342 |
common.RetryTimes, _ = strconv.Atoi(value)
|
| 343 |
case "DataExportInterval":
|
|
|
|
| 67 |
common.OptionMap["ServerAddress"] = ""
|
| 68 |
common.OptionMap["WorkerUrl"] = setting.WorkerUrl
|
| 69 |
common.OptionMap["WorkerValidKey"] = setting.WorkerValidKey
|
| 70 |
+
common.OptionMap["WorkerAllowHttpImageRequestEnabled"] = strconv.FormatBool(setting.WorkerAllowHttpImageRequestEnabled)
|
| 71 |
common.OptionMap["PayAddress"] = ""
|
| 72 |
common.OptionMap["CustomCallbackAddress"] = ""
|
| 73 |
common.OptionMap["EpayId"] = ""
|
|
|
|
| 93 |
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
| 94 |
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
| 95 |
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
| 96 |
+
common.OptionMap["ModelRequestRateLimitGroup"] = setting.ModelRequestRateLimitGroup2JSONString()
|
| 97 |
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
| 98 |
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
| 99 |
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
|
|
|
| 258 |
setting.StopOnSensitiveEnabled = boolValue
|
| 259 |
case "SMTPSSLEnabled":
|
| 260 |
common.SMTPSSLEnabled = boolValue
|
| 261 |
+
case "WorkerAllowHttpImageRequestEnabled":
|
| 262 |
+
setting.WorkerAllowHttpImageRequestEnabled = boolValue
|
| 263 |
}
|
| 264 |
}
|
| 265 |
switch key {
|
|
|
|
| 342 |
setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
|
| 343 |
case "ModelRequestRateLimitSuccessCount":
|
| 344 |
setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
|
| 345 |
+
case "ModelRequestRateLimitGroup":
|
| 346 |
+
err = setting.UpdateModelRequestRateLimitGroupByJSONString(value)
|
| 347 |
case "RetryTimes":
|
| 348 |
common.RetryTimes, _ = strconv.Atoi(value)
|
| 349 |
case "DataExportInterval":
|
model/user.go
CHANGED
|
@@ -18,6 +18,7 @@ type User struct {
|
|
| 18 |
Id int `json:"id"`
|
| 19 |
Username string `json:"username" gorm:"unique;index" validate:"max=12"`
|
| 20 |
Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"`
|
|
|
|
| 21 |
DisplayName string `json:"display_name" gorm:"index" validate:"max=20"`
|
| 22 |
Role int `json:"role" gorm:"type:int;default:1"` // admin, common
|
| 23 |
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
|
|
|
| 18 |
Id int `json:"id"`
|
| 19 |
Username string `json:"username" gorm:"unique;index" validate:"max=12"`
|
| 20 |
Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"`
|
| 21 |
+
OriginalPassword string `json:"original_password" gorm:"-:all"` // this field is only for Password change verification, don't save it to database!
|
| 22 |
DisplayName string `json:"display_name" gorm:"index" validate:"max=20"`
|
| 23 |
Role int `json:"role" gorm:"type:int;default:1"` // admin, common
|
| 24 |
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
relay/channel/api_request.go
CHANGED
|
@@ -1,16 +1,23 @@
|
|
| 1 |
package channel
|
| 2 |
|
| 3 |
import (
|
|
|
|
| 4 |
"errors"
|
| 5 |
"fmt"
|
| 6 |
-
"github.com/gin-gonic/gin"
|
| 7 |
-
"github.com/gorilla/websocket"
|
| 8 |
"io"
|
| 9 |
"net/http"
|
| 10 |
common2 "one-api/common"
|
| 11 |
"one-api/relay/common"
|
| 12 |
"one-api/relay/constant"
|
|
|
|
| 13 |
"one-api/service"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
)
|
| 15 |
|
| 16 |
func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Header) {
|
|
@@ -55,6 +62,9 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
|
|
| 55 |
if err != nil {
|
| 56 |
return nil, fmt.Errorf("get request url failed: %w", err)
|
| 57 |
}
|
|
|
|
|
|
|
|
|
|
| 58 |
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
| 59 |
if err != nil {
|
| 60 |
return nil, fmt.Errorf("new request failed: %w", err)
|
|
@@ -105,7 +115,62 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
|
|
| 105 |
} else {
|
| 106 |
client = service.GetHttpClient()
|
| 107 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
resp, err := client.Do(req)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
if err != nil {
|
| 110 |
return nil, err
|
| 111 |
}
|
|
|
|
| 1 |
package channel
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"context"
|
| 5 |
"errors"
|
| 6 |
"fmt"
|
|
|
|
|
|
|
| 7 |
"io"
|
| 8 |
"net/http"
|
| 9 |
common2 "one-api/common"
|
| 10 |
"one-api/relay/common"
|
| 11 |
"one-api/relay/constant"
|
| 12 |
+
"one-api/relay/helper"
|
| 13 |
"one-api/service"
|
| 14 |
+
"one-api/setting/operation_setting"
|
| 15 |
+
"sync"
|
| 16 |
+
"time"
|
| 17 |
+
|
| 18 |
+
"github.com/bytedance/gopkg/util/gopool"
|
| 19 |
+
"github.com/gin-gonic/gin"
|
| 20 |
+
"github.com/gorilla/websocket"
|
| 21 |
)
|
| 22 |
|
| 23 |
func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Header) {
|
|
|
|
| 62 |
if err != nil {
|
| 63 |
return nil, fmt.Errorf("get request url failed: %w", err)
|
| 64 |
}
|
| 65 |
+
if common2.DebugEnabled {
|
| 66 |
+
println("fullRequestURL:", fullRequestURL)
|
| 67 |
+
}
|
| 68 |
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
| 69 |
if err != nil {
|
| 70 |
return nil, fmt.Errorf("new request failed: %w", err)
|
|
|
|
| 115 |
} else {
|
| 116 |
client = service.GetHttpClient()
|
| 117 |
}
|
| 118 |
+
// 流式请求 ping 保活
|
| 119 |
+
var stopPinger func()
|
| 120 |
+
generalSettings := operation_setting.GetGeneralSetting()
|
| 121 |
+
pingEnabled := generalSettings.PingIntervalEnabled
|
| 122 |
+
var pingerWg sync.WaitGroup
|
| 123 |
+
if info.IsStream {
|
| 124 |
+
helper.SetEventStreamHeaders(c)
|
| 125 |
+
pingInterval := time.Duration(generalSettings.PingIntervalSeconds) * time.Second
|
| 126 |
+
var pingerCtx context.Context
|
| 127 |
+
pingerCtx, stopPinger = context.WithCancel(c.Request.Context())
|
| 128 |
+
|
| 129 |
+
if pingEnabled {
|
| 130 |
+
pingerWg.Add(1)
|
| 131 |
+
gopool.Go(func() {
|
| 132 |
+
defer pingerWg.Done()
|
| 133 |
+
if pingInterval <= 0 {
|
| 134 |
+
pingInterval = helper.DefaultPingInterval
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
ticker := time.NewTicker(pingInterval)
|
| 138 |
+
defer ticker.Stop()
|
| 139 |
+
var pingMutex sync.Mutex
|
| 140 |
+
if common2.DebugEnabled {
|
| 141 |
+
println("SSE ping goroutine started")
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
for {
|
| 145 |
+
select {
|
| 146 |
+
case <-ticker.C:
|
| 147 |
+
pingMutex.Lock()
|
| 148 |
+
err2 := helper.PingData(c)
|
| 149 |
+
pingMutex.Unlock()
|
| 150 |
+
if err2 != nil {
|
| 151 |
+
common2.LogError(c, "SSE ping error: "+err.Error())
|
| 152 |
+
return
|
| 153 |
+
}
|
| 154 |
+
if common2.DebugEnabled {
|
| 155 |
+
println("SSE ping data sent.")
|
| 156 |
+
}
|
| 157 |
+
case <-pingerCtx.Done():
|
| 158 |
+
if common2.DebugEnabled {
|
| 159 |
+
println("SSE ping goroutine stopped.")
|
| 160 |
+
}
|
| 161 |
+
return
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
})
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
resp, err := client.Do(req)
|
| 169 |
+
// request结束后停止ping
|
| 170 |
+
if info.IsStream && pingEnabled {
|
| 171 |
+
stopPinger()
|
| 172 |
+
pingerWg.Wait()
|
| 173 |
+
}
|
| 174 |
if err != nil {
|
| 175 |
return nil, err
|
| 176 |
}
|
relay/channel/gemini/relay-gemini.go
CHANGED
|
@@ -391,6 +391,7 @@ func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interfac
|
|
| 391 |
}
|
| 392 |
// 删除所有的title字段
|
| 393 |
delete(v, "title")
|
|
|
|
| 394 |
// 如果type不为object和array,则直接返回
|
| 395 |
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
|
| 396 |
return schema
|
|
|
|
| 391 |
}
|
| 392 |
// 删除所有的title字段
|
| 393 |
delete(v, "title")
|
| 394 |
+
delete(v, "$schema")
|
| 395 |
// 如果type不为object和array,则直接返回
|
| 396 |
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
|
| 397 |
return schema
|
relay/channel/openai/adaptor.go
CHANGED
|
@@ -8,6 +8,7 @@ import (
|
|
| 8 |
"io"
|
| 9 |
"mime/multipart"
|
| 10 |
"net/http"
|
|
|
|
| 11 |
"one-api/common"
|
| 12 |
constant2 "one-api/constant"
|
| 13 |
"one-api/dto"
|
|
@@ -25,8 +26,6 @@ import (
|
|
| 25 |
"path/filepath"
|
| 26 |
"strings"
|
| 27 |
|
| 28 |
-
"net/textproto"
|
| 29 |
-
|
| 30 |
"github.com/gin-gonic/gin"
|
| 31 |
)
|
| 32 |
|
|
@@ -68,9 +67,6 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
|
| 68 |
if info.RelayFormat == relaycommon.RelayFormatClaude {
|
| 69 |
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
|
| 70 |
}
|
| 71 |
-
if info.RelayMode == constant.RelayModeResponses {
|
| 72 |
-
return fmt.Sprintf("%s/v1/responses", info.BaseUrl), nil
|
| 73 |
-
}
|
| 74 |
if info.RelayMode == constant.RelayModeRealtime {
|
| 75 |
if strings.HasPrefix(info.BaseUrl, "https://") {
|
| 76 |
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
|
@@ -93,7 +89,10 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
|
| 93 |
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
|
| 94 |
task := strings.TrimPrefix(requestURL, "/v1/")
|
| 95 |
model_ := info.UpstreamModelName
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
| 97 |
// https://github.com/songquanpeng/one-api/issues/67
|
| 98 |
requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task)
|
| 99 |
if info.RelayMode == constant.RelayModeRealtime {
|
|
@@ -173,7 +172,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
|
| 173 |
info.UpstreamModelName = request.Model
|
| 174 |
|
| 175 |
// o系列模型developer适配(o1-mini除外)
|
| 176 |
-
if !strings.HasPrefix(request.Model, "o1-mini") {
|
| 177 |
//修改第一个Message的内容,将system改为developer
|
| 178 |
if len(request.Messages) > 0 && request.Messages[0].Role == "system" {
|
| 179 |
request.Messages[0].Role = "developer"
|
|
@@ -429,7 +428,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
|
| 429 |
if info.IsStream {
|
| 430 |
err, usage = OaiResponsesStreamHandler(c, resp, info)
|
| 431 |
} else {
|
| 432 |
-
err, usage =
|
| 433 |
}
|
| 434 |
default:
|
| 435 |
if info.IsStream {
|
|
|
|
| 8 |
"io"
|
| 9 |
"mime/multipart"
|
| 10 |
"net/http"
|
| 11 |
+
"net/textproto"
|
| 12 |
"one-api/common"
|
| 13 |
constant2 "one-api/constant"
|
| 14 |
"one-api/dto"
|
|
|
|
| 26 |
"path/filepath"
|
| 27 |
"strings"
|
| 28 |
|
|
|
|
|
|
|
| 29 |
"github.com/gin-gonic/gin"
|
| 30 |
)
|
| 31 |
|
|
|
|
| 67 |
if info.RelayFormat == relaycommon.RelayFormatClaude {
|
| 68 |
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
| 70 |
if info.RelayMode == constant.RelayModeRealtime {
|
| 71 |
if strings.HasPrefix(info.BaseUrl, "https://") {
|
| 72 |
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
|
|
|
| 89 |
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
|
| 90 |
task := strings.TrimPrefix(requestURL, "/v1/")
|
| 91 |
model_ := info.UpstreamModelName
|
| 92 |
+
// 2025年5月10日后创建的渠道不移除.
|
| 93 |
+
if info.ChannelCreateTime < constant2.AzureNoRemoveDotTime {
|
| 94 |
+
model_ = strings.Replace(model_, ".", "", -1)
|
| 95 |
+
}
|
| 96 |
// https://github.com/songquanpeng/one-api/issues/67
|
| 97 |
requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task)
|
| 98 |
if info.RelayMode == constant.RelayModeRealtime {
|
|
|
|
| 172 |
info.UpstreamModelName = request.Model
|
| 173 |
|
| 174 |
// o系列模型developer适配(o1-mini除外)
|
| 175 |
+
if !strings.HasPrefix(request.Model, "o1-mini") && !strings.HasPrefix(request.Model, "o1-preview") {
|
| 176 |
//修改第一个Message的内容,将system改为developer
|
| 177 |
if len(request.Messages) > 0 && request.Messages[0].Role == "system" {
|
| 178 |
request.Messages[0].Role = "developer"
|
|
|
|
| 428 |
if info.IsStream {
|
| 429 |
err, usage = OaiResponsesStreamHandler(c, resp, info)
|
| 430 |
} else {
|
| 431 |
+
err, usage = OaiResponsesHandler(c, resp, info)
|
| 432 |
}
|
| 433 |
default:
|
| 434 |
if info.IsStream {
|
relay/channel/openai/helper.go
CHANGED
|
@@ -187,3 +187,10 @@ func handleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream
|
|
| 187 |
}
|
| 188 |
}
|
| 189 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
}
|
| 189 |
}
|
| 190 |
+
|
| 191 |
+
func sendResponsesStreamData(c *gin.Context, streamResponse dto.ResponsesStreamResponse, data string) {
|
| 192 |
+
if data == "" {
|
| 193 |
+
return
|
| 194 |
+
}
|
| 195 |
+
helper.ResponseChunkData(c, streamResponse, data)
|
| 196 |
+
}
|
relay/channel/openai/relay-openai.go
CHANGED
|
@@ -215,10 +215,35 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
|
|
| 215 |
StatusCode: resp.StatusCode,
|
| 216 |
}, nil
|
| 217 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
switch info.RelayFormat {
|
| 220 |
case relaycommon.RelayFormatOpenAI:
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
case relaycommon.RelayFormatClaude:
|
| 223 |
claudeResp := service.ResponseOpenAI2Claude(&simpleResponse, info)
|
| 224 |
claudeRespStr, err := json.Marshal(claudeResp)
|
|
@@ -244,18 +269,6 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
|
|
| 244 |
common.SysError("error copying response body: " + err.Error())
|
| 245 |
}
|
| 246 |
resp.Body.Close()
|
| 247 |
-
if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) {
|
| 248 |
-
completionTokens := 0
|
| 249 |
-
for _, choice := range simpleResponse.Choices {
|
| 250 |
-
ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, info.UpstreamModelName)
|
| 251 |
-
completionTokens += ctkm
|
| 252 |
-
}
|
| 253 |
-
simpleResponse.Usage = dto.Usage{
|
| 254 |
-
PromptTokens: info.PromptTokens,
|
| 255 |
-
CompletionTokens: completionTokens,
|
| 256 |
-
TotalTokens: info.PromptTokens + completionTokens,
|
| 257 |
-
}
|
| 258 |
-
}
|
| 259 |
return nil, &simpleResponse.Usage
|
| 260 |
}
|
| 261 |
|
|
@@ -644,102 +657,3 @@ func OpenaiHandlerWithUsage(c *gin.Context, resp *http.Response, info *relaycomm
|
|
| 644 |
}
|
| 645 |
return nil, &usageResp.Usage
|
| 646 |
}
|
| 647 |
-
|
| 648 |
-
func OpenaiResponsesHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
| 649 |
-
// read response body
|
| 650 |
-
var responsesResponse dto.OpenAIResponsesResponse
|
| 651 |
-
responseBody, err := io.ReadAll(resp.Body)
|
| 652 |
-
if err != nil {
|
| 653 |
-
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 654 |
-
}
|
| 655 |
-
err = resp.Body.Close()
|
| 656 |
-
if err != nil {
|
| 657 |
-
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 658 |
-
}
|
| 659 |
-
err = common.DecodeJson(responseBody, &responsesResponse)
|
| 660 |
-
if err != nil {
|
| 661 |
-
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 662 |
-
}
|
| 663 |
-
if responsesResponse.Error != nil {
|
| 664 |
-
return &dto.OpenAIErrorWithStatusCode{
|
| 665 |
-
Error: dto.OpenAIError{
|
| 666 |
-
Message: responsesResponse.Error.Message,
|
| 667 |
-
Type: "openai_error",
|
| 668 |
-
Code: responsesResponse.Error.Code,
|
| 669 |
-
},
|
| 670 |
-
StatusCode: resp.StatusCode,
|
| 671 |
-
}, nil
|
| 672 |
-
}
|
| 673 |
-
|
| 674 |
-
// reset response body
|
| 675 |
-
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
| 676 |
-
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
| 677 |
-
// And then we will have to send an error response, but in this case, the header has already been set.
|
| 678 |
-
// So the httpClient will be confused by the response.
|
| 679 |
-
// For example, Postman will report error, and we cannot check the response at all.
|
| 680 |
-
for k, v := range resp.Header {
|
| 681 |
-
c.Writer.Header().Set(k, v[0])
|
| 682 |
-
}
|
| 683 |
-
c.Writer.WriteHeader(resp.StatusCode)
|
| 684 |
-
// copy response body
|
| 685 |
-
_, err = io.Copy(c.Writer, resp.Body)
|
| 686 |
-
if err != nil {
|
| 687 |
-
common.SysError("error copying response body: " + err.Error())
|
| 688 |
-
}
|
| 689 |
-
resp.Body.Close()
|
| 690 |
-
// compute usage
|
| 691 |
-
usage := dto.Usage{}
|
| 692 |
-
usage.PromptTokens = responsesResponse.Usage.InputTokens
|
| 693 |
-
usage.CompletionTokens = responsesResponse.Usage.OutputTokens
|
| 694 |
-
usage.TotalTokens = responsesResponse.Usage.TotalTokens
|
| 695 |
-
return nil, &usage
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
func OaiResponsesStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
| 699 |
-
if resp == nil || resp.Body == nil {
|
| 700 |
-
common.LogError(c, "invalid response or response body")
|
| 701 |
-
return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
|
| 702 |
-
}
|
| 703 |
-
|
| 704 |
-
var usage = &dto.Usage{}
|
| 705 |
-
var responseTextBuilder strings.Builder
|
| 706 |
-
|
| 707 |
-
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
| 708 |
-
|
| 709 |
-
// 检查当前数据是否包含 completed 状态和 usage 信息
|
| 710 |
-
var streamResponse dto.ResponsesStreamResponse
|
| 711 |
-
if err := common.DecodeJsonStr(data, &streamResponse); err == nil {
|
| 712 |
-
sendResponsesStreamData(c, streamResponse, data)
|
| 713 |
-
switch streamResponse.Type {
|
| 714 |
-
case "response.completed":
|
| 715 |
-
usage.PromptTokens = streamResponse.Response.Usage.InputTokens
|
| 716 |
-
usage.CompletionTokens = streamResponse.Response.Usage.OutputTokens
|
| 717 |
-
usage.TotalTokens = streamResponse.Response.Usage.TotalTokens
|
| 718 |
-
case "response.output_text.delta":
|
| 719 |
-
// 处理输出文本
|
| 720 |
-
responseTextBuilder.WriteString(streamResponse.Delta)
|
| 721 |
-
|
| 722 |
-
}
|
| 723 |
-
}
|
| 724 |
-
return true
|
| 725 |
-
})
|
| 726 |
-
|
| 727 |
-
if usage.CompletionTokens == 0 {
|
| 728 |
-
// 计算输出文本的 token 数量
|
| 729 |
-
tempStr := responseTextBuilder.String()
|
| 730 |
-
if len(tempStr) > 0 {
|
| 731 |
-
// 非正常结束,使用输出文本的 token 数量
|
| 732 |
-
completionTokens, _ := service.CountTextToken(tempStr, info.UpstreamModelName)
|
| 733 |
-
usage.CompletionTokens = completionTokens
|
| 734 |
-
}
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
-
return nil, usage
|
| 738 |
-
}
|
| 739 |
-
|
| 740 |
-
func sendResponsesStreamData(c *gin.Context, streamResponse dto.ResponsesStreamResponse, data string) {
|
| 741 |
-
if data == "" {
|
| 742 |
-
return
|
| 743 |
-
}
|
| 744 |
-
helper.ResponseChunkData(c, streamResponse, data)
|
| 745 |
-
}
|
|
|
|
| 215 |
StatusCode: resp.StatusCode,
|
| 216 |
}, nil
|
| 217 |
}
|
| 218 |
+
|
| 219 |
+
forceFormat := false
|
| 220 |
+
if forceFmt, ok := info.ChannelSetting[constant.ForceFormat].(bool); ok {
|
| 221 |
+
forceFormat = forceFmt
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) {
|
| 225 |
+
completionTokens := 0
|
| 226 |
+
for _, choice := range simpleResponse.Choices {
|
| 227 |
+
ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, info.UpstreamModelName)
|
| 228 |
+
completionTokens += ctkm
|
| 229 |
+
}
|
| 230 |
+
simpleResponse.Usage = dto.Usage{
|
| 231 |
+
PromptTokens: info.PromptTokens,
|
| 232 |
+
CompletionTokens: completionTokens,
|
| 233 |
+
TotalTokens: info.PromptTokens + completionTokens,
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
|
| 237 |
switch info.RelayFormat {
|
| 238 |
case relaycommon.RelayFormatOpenAI:
|
| 239 |
+
if forceFormat {
|
| 240 |
+
responseBody, err = json.Marshal(simpleResponse)
|
| 241 |
+
if err != nil {
|
| 242 |
+
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 243 |
+
}
|
| 244 |
+
} else {
|
| 245 |
+
break
|
| 246 |
+
}
|
| 247 |
case relaycommon.RelayFormatClaude:
|
| 248 |
claudeResp := service.ResponseOpenAI2Claude(&simpleResponse, info)
|
| 249 |
claudeRespStr, err := json.Marshal(claudeResp)
|
|
|
|
| 269 |
common.SysError("error copying response body: " + err.Error())
|
| 270 |
}
|
| 271 |
resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
return nil, &simpleResponse.Usage
|
| 273 |
}
|
| 274 |
|
|
|
|
| 657 |
}
|
| 658 |
return nil, &usageResp.Usage
|
| 659 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
relay/channel/openai/relay_responses.go
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package openai
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"fmt"
|
| 6 |
+
"io"
|
| 7 |
+
"net/http"
|
| 8 |
+
"one-api/common"
|
| 9 |
+
"one-api/dto"
|
| 10 |
+
relaycommon "one-api/relay/common"
|
| 11 |
+
"one-api/relay/helper"
|
| 12 |
+
"one-api/service"
|
| 13 |
+
"strings"
|
| 14 |
+
|
| 15 |
+
"github.com/gin-gonic/gin"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
func OaiResponsesHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
| 19 |
+
// read response body
|
| 20 |
+
var responsesResponse dto.OpenAIResponsesResponse
|
| 21 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 22 |
+
if err != nil {
|
| 23 |
+
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 24 |
+
}
|
| 25 |
+
err = resp.Body.Close()
|
| 26 |
+
if err != nil {
|
| 27 |
+
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 28 |
+
}
|
| 29 |
+
err = common.DecodeJson(responseBody, &responsesResponse)
|
| 30 |
+
if err != nil {
|
| 31 |
+
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 32 |
+
}
|
| 33 |
+
if responsesResponse.Error != nil {
|
| 34 |
+
return &dto.OpenAIErrorWithStatusCode{
|
| 35 |
+
Error: dto.OpenAIError{
|
| 36 |
+
Message: responsesResponse.Error.Message,
|
| 37 |
+
Type: "openai_error",
|
| 38 |
+
Code: responsesResponse.Error.Code,
|
| 39 |
+
},
|
| 40 |
+
StatusCode: resp.StatusCode,
|
| 41 |
+
}, nil
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// reset response body
|
| 45 |
+
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
| 46 |
+
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
| 47 |
+
// And then we will have to send an error response, but in this case, the header has already been set.
|
| 48 |
+
// So the httpClient will be confused by the response.
|
| 49 |
+
// For example, Postman will report error, and we cannot check the response at all.
|
| 50 |
+
for k, v := range resp.Header {
|
| 51 |
+
c.Writer.Header().Set(k, v[0])
|
| 52 |
+
}
|
| 53 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 54 |
+
// copy response body
|
| 55 |
+
_, err = io.Copy(c.Writer, resp.Body)
|
| 56 |
+
if err != nil {
|
| 57 |
+
common.SysError("error copying response body: " + err.Error())
|
| 58 |
+
}
|
| 59 |
+
resp.Body.Close()
|
| 60 |
+
// compute usage
|
| 61 |
+
usage := dto.Usage{}
|
| 62 |
+
usage.PromptTokens = responsesResponse.Usage.InputTokens
|
| 63 |
+
usage.CompletionTokens = responsesResponse.Usage.OutputTokens
|
| 64 |
+
usage.TotalTokens = responsesResponse.Usage.TotalTokens
|
| 65 |
+
// 解析 Tools 用量
|
| 66 |
+
for _, tool := range responsesResponse.Tools {
|
| 67 |
+
info.ResponsesUsageInfo.BuiltInTools[tool.Type].CallCount++
|
| 68 |
+
}
|
| 69 |
+
return nil, &usage
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func OaiResponsesStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
| 73 |
+
if resp == nil || resp.Body == nil {
|
| 74 |
+
common.LogError(c, "invalid response or response body")
|
| 75 |
+
return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
var usage = &dto.Usage{}
|
| 79 |
+
var responseTextBuilder strings.Builder
|
| 80 |
+
|
| 81 |
+
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
| 82 |
+
|
| 83 |
+
// 检查当前数据是否包含 completed 状态和 usage 信息
|
| 84 |
+
var streamResponse dto.ResponsesStreamResponse
|
| 85 |
+
if err := common.DecodeJsonStr(data, &streamResponse); err == nil {
|
| 86 |
+
sendResponsesStreamData(c, streamResponse, data)
|
| 87 |
+
switch streamResponse.Type {
|
| 88 |
+
case "response.completed":
|
| 89 |
+
usage.PromptTokens = streamResponse.Response.Usage.InputTokens
|
| 90 |
+
usage.CompletionTokens = streamResponse.Response.Usage.OutputTokens
|
| 91 |
+
usage.TotalTokens = streamResponse.Response.Usage.TotalTokens
|
| 92 |
+
case "response.output_text.delta":
|
| 93 |
+
// 处理输出文本
|
| 94 |
+
responseTextBuilder.WriteString(streamResponse.Delta)
|
| 95 |
+
case dto.ResponsesOutputTypeItemDone:
|
| 96 |
+
// 函数调用处理
|
| 97 |
+
if streamResponse.Item != nil {
|
| 98 |
+
switch streamResponse.Item.Type {
|
| 99 |
+
case dto.BuildInCallWebSearchCall:
|
| 100 |
+
info.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview].CallCount++
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
return true
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
if usage.CompletionTokens == 0 {
|
| 109 |
+
// 计算输出文本的 token 数量
|
| 110 |
+
tempStr := responseTextBuilder.String()
|
| 111 |
+
if len(tempStr) > 0 {
|
| 112 |
+
// 非正常结束,使用输出文本的 token 数量
|
| 113 |
+
completionTokens, _ := service.CountTextToken(tempStr, info.UpstreamModelName)
|
| 114 |
+
usage.CompletionTokens = completionTokens
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
return nil, usage
|
| 119 |
+
}
|
relay/channel/vertex/adaptor.go
CHANGED
|
@@ -11,8 +11,8 @@ import (
|
|
| 11 |
"one-api/relay/channel/claude"
|
| 12 |
"one-api/relay/channel/gemini"
|
| 13 |
"one-api/relay/channel/openai"
|
| 14 |
-
"one-api/setting/model_setting"
|
| 15 |
relaycommon "one-api/relay/common"
|
|
|
|
| 16 |
"strings"
|
| 17 |
|
| 18 |
"github.com/gin-gonic/gin"
|
|
|
|
| 11 |
"one-api/relay/channel/claude"
|
| 12 |
"one-api/relay/channel/gemini"
|
| 13 |
"one-api/relay/channel/openai"
|
|
|
|
| 14 |
relaycommon "one-api/relay/common"
|
| 15 |
+
"one-api/setting/model_setting"
|
| 16 |
"strings"
|
| 17 |
|
| 18 |
"github.com/gin-gonic/gin"
|
relay/channel/xai/adaptor.go
CHANGED
|
@@ -2,14 +2,16 @@ package xai
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"errors"
|
| 5 |
-
"fmt"
|
| 6 |
"io"
|
| 7 |
"net/http"
|
| 8 |
"one-api/dto"
|
| 9 |
"one-api/relay/channel"
|
|
|
|
| 10 |
relaycommon "one-api/relay/common"
|
| 11 |
"strings"
|
| 12 |
|
|
|
|
|
|
|
| 13 |
"github.com/gin-gonic/gin"
|
| 14 |
)
|
| 15 |
|
|
@@ -28,15 +30,20 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
|
| 28 |
}
|
| 29 |
|
| 30 |
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
| 36 |
}
|
| 37 |
|
| 38 |
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
| 39 |
-
return
|
| 40 |
}
|
| 41 |
|
| 42 |
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
|
@@ -89,15 +96,16 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
|
|
| 89 |
}
|
| 90 |
|
| 91 |
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
-
//if _, ok := usage.(*dto.Usage); ok && usage != nil {
|
| 98 |
-
// usage.(*dto.Usage).CompletionTokens = usage.(*dto.Usage).TotalTokens - usage.(*dto.Usage).PromptTokens
|
| 99 |
-
//}
|
| 100 |
-
|
| 101 |
return
|
| 102 |
}
|
| 103 |
|
|
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"errors"
|
|
|
|
| 5 |
"io"
|
| 6 |
"net/http"
|
| 7 |
"one-api/dto"
|
| 8 |
"one-api/relay/channel"
|
| 9 |
+
"one-api/relay/channel/openai"
|
| 10 |
relaycommon "one-api/relay/common"
|
| 11 |
"strings"
|
| 12 |
|
| 13 |
+
"one-api/relay/constant"
|
| 14 |
+
|
| 15 |
"github.com/gin-gonic/gin"
|
| 16 |
)
|
| 17 |
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
| 33 |
+
xaiRequest := ImageRequest{
|
| 34 |
+
Model: request.Model,
|
| 35 |
+
Prompt: request.Prompt,
|
| 36 |
+
N: request.N,
|
| 37 |
+
ResponseFormat: request.ResponseFormat,
|
| 38 |
+
}
|
| 39 |
+
return xaiRequest, nil
|
| 40 |
}
|
| 41 |
|
| 42 |
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
| 43 |
}
|
| 44 |
|
| 45 |
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
| 46 |
+
return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
|
| 47 |
}
|
| 48 |
|
| 49 |
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
|
| 99 |
+
switch info.RelayMode {
|
| 100 |
+
case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
|
| 101 |
+
err, usage = openai.OpenaiHandlerWithUsage(c, resp, info)
|
| 102 |
+
default:
|
| 103 |
+
if info.IsStream {
|
| 104 |
+
err, usage = xAIStreamHandler(c, resp, info)
|
| 105 |
+
} else {
|
| 106 |
+
err, usage = xAIHandler(c, resp, info)
|
| 107 |
+
}
|
| 108 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
return
|
| 110 |
}
|
| 111 |
|
relay/channel/xai/dto.go
CHANGED
|
@@ -12,3 +12,16 @@ type ChatCompletionResponse struct {
|
|
| 12 |
Usage *dto.Usage `json:"usage"`
|
| 13 |
SystemFingerprint string `json:"system_fingerprint"`
|
| 14 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
Usage *dto.Usage `json:"usage"`
|
| 13 |
SystemFingerprint string `json:"system_fingerprint"`
|
| 14 |
}
|
| 15 |
+
|
| 16 |
+
// quality, size or style are not supported by xAI API at the moment.
|
| 17 |
+
type ImageRequest struct {
|
| 18 |
+
Model string `json:"model"`
|
| 19 |
+
Prompt string `json:"prompt" binding:"required"`
|
| 20 |
+
N int `json:"n,omitempty"`
|
| 21 |
+
// Size string `json:"size,omitempty"`
|
| 22 |
+
// Quality string `json:"quality,omitempty"`
|
| 23 |
+
ResponseFormat string `json:"response_format,omitempty"`
|
| 24 |
+
// Style string `json:"style,omitempty"`
|
| 25 |
+
// User string `json:"user,omitempty"`
|
| 26 |
+
// ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
|
| 27 |
+
}
|
relay/common/relay_info.go
CHANGED
|
@@ -36,6 +36,7 @@ type ClaudeConvertInfo struct {
|
|
| 36 |
const (
|
| 37 |
RelayFormatOpenAI = "openai"
|
| 38 |
RelayFormatClaude = "claude"
|
|
|
|
| 39 |
)
|
| 40 |
|
| 41 |
type RerankerInfo struct {
|
|
@@ -43,6 +44,16 @@ type RerankerInfo struct {
|
|
| 43 |
ReturnDocuments bool
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
type RelayInfo struct {
|
| 47 |
ChannelType int
|
| 48 |
ChannelId int
|
|
@@ -87,9 +98,11 @@ type RelayInfo struct {
|
|
| 87 |
UserQuota int
|
| 88 |
RelayFormat string
|
| 89 |
SendResponseCount int
|
|
|
|
| 90 |
ThinkingContentInfo
|
| 91 |
*ClaudeConvertInfo
|
| 92 |
*RerankerInfo
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
// 定义支持流式选项的通道类型
|
|
@@ -103,6 +116,8 @@ var streamSupportedChannels = map[int]bool{
|
|
| 103 |
common.ChannelTypeVolcEngine: true,
|
| 104 |
common.ChannelTypeOllama: true,
|
| 105 |
common.ChannelTypeXai: true,
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
|
@@ -134,6 +149,31 @@ func GenRelayInfoRerank(c *gin.Context, req *dto.RerankRequest) *RelayInfo {
|
|
| 134 |
return info
|
| 135 |
}
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
func GenRelayInfo(c *gin.Context) *RelayInfo {
|
| 138 |
channelType := c.GetInt("channel_type")
|
| 139 |
channelId := c.GetInt("channel_id")
|
|
@@ -170,14 +210,15 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|
| 170 |
OriginModelName: c.GetString("original_model"),
|
| 171 |
UpstreamModelName: c.GetString("original_model"),
|
| 172 |
//RecodeModelName: c.GetString("original_model"),
|
| 173 |
-
IsModelMapped:
|
| 174 |
-
ApiType:
|
| 175 |
-
ApiVersion:
|
| 176 |
-
ApiKey:
|
| 177 |
-
Organization:
|
| 178 |
-
ChannelSetting:
|
| 179 |
-
|
| 180 |
-
|
|
|
|
| 181 |
ThinkingContentInfo: ThinkingContentInfo{
|
| 182 |
IsFirstThinkingContent: true,
|
| 183 |
SendLastThinkingContent: false,
|
|
|
|
| 36 |
const (
|
| 37 |
RelayFormatOpenAI = "openai"
|
| 38 |
RelayFormatClaude = "claude"
|
| 39 |
+
RelayFormatGemini = "gemini"
|
| 40 |
)
|
| 41 |
|
| 42 |
type RerankerInfo struct {
|
|
|
|
| 44 |
ReturnDocuments bool
|
| 45 |
}
|
| 46 |
|
| 47 |
+
type BuildInToolInfo struct {
|
| 48 |
+
ToolName string
|
| 49 |
+
CallCount int
|
| 50 |
+
SearchContextSize string
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
type ResponsesUsageInfo struct {
|
| 54 |
+
BuiltInTools map[string]*BuildInToolInfo
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
type RelayInfo struct {
|
| 58 |
ChannelType int
|
| 59 |
ChannelId int
|
|
|
|
| 98 |
UserQuota int
|
| 99 |
RelayFormat string
|
| 100 |
SendResponseCount int
|
| 101 |
+
ChannelCreateTime int64
|
| 102 |
ThinkingContentInfo
|
| 103 |
*ClaudeConvertInfo
|
| 104 |
*RerankerInfo
|
| 105 |
+
*ResponsesUsageInfo
|
| 106 |
}
|
| 107 |
|
| 108 |
// 定义支持流式选项的通道类型
|
|
|
|
| 116 |
common.ChannelTypeVolcEngine: true,
|
| 117 |
common.ChannelTypeOllama: true,
|
| 118 |
common.ChannelTypeXai: true,
|
| 119 |
+
common.ChannelTypeDeepSeek: true,
|
| 120 |
+
common.ChannelTypeBaiduV2: true,
|
| 121 |
}
|
| 122 |
|
| 123 |
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
|
|
|
| 149 |
return info
|
| 150 |
}
|
| 151 |
|
| 152 |
+
func GenRelayInfoResponses(c *gin.Context, req *dto.OpenAIResponsesRequest) *RelayInfo {
|
| 153 |
+
info := GenRelayInfo(c)
|
| 154 |
+
info.RelayMode = relayconstant.RelayModeResponses
|
| 155 |
+
info.ResponsesUsageInfo = &ResponsesUsageInfo{
|
| 156 |
+
BuiltInTools: make(map[string]*BuildInToolInfo),
|
| 157 |
+
}
|
| 158 |
+
if len(req.Tools) > 0 {
|
| 159 |
+
for _, tool := range req.Tools {
|
| 160 |
+
info.ResponsesUsageInfo.BuiltInTools[tool.Type] = &BuildInToolInfo{
|
| 161 |
+
ToolName: tool.Type,
|
| 162 |
+
CallCount: 0,
|
| 163 |
+
}
|
| 164 |
+
switch tool.Type {
|
| 165 |
+
case dto.BuildInToolWebSearchPreview:
|
| 166 |
+
if tool.SearchContextSize == "" {
|
| 167 |
+
tool.SearchContextSize = "medium"
|
| 168 |
+
}
|
| 169 |
+
info.ResponsesUsageInfo.BuiltInTools[tool.Type].SearchContextSize = tool.SearchContextSize
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
info.IsStream = req.Stream
|
| 174 |
+
return info
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
func GenRelayInfo(c *gin.Context) *RelayInfo {
|
| 178 |
channelType := c.GetInt("channel_type")
|
| 179 |
channelId := c.GetInt("channel_id")
|
|
|
|
| 210 |
OriginModelName: c.GetString("original_model"),
|
| 211 |
UpstreamModelName: c.GetString("original_model"),
|
| 212 |
//RecodeModelName: c.GetString("original_model"),
|
| 213 |
+
IsModelMapped: false,
|
| 214 |
+
ApiType: apiType,
|
| 215 |
+
ApiVersion: c.GetString("api_version"),
|
| 216 |
+
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
| 217 |
+
Organization: c.GetString("channel_organization"),
|
| 218 |
+
ChannelSetting: channelSetting,
|
| 219 |
+
ChannelCreateTime: c.GetInt64("channel_create_time"),
|
| 220 |
+
ParamOverride: paramOverride,
|
| 221 |
+
RelayFormat: RelayFormatOpenAI,
|
| 222 |
ThinkingContentInfo: ThinkingContentInfo{
|
| 223 |
IsFirstThinkingContent: true,
|
| 224 |
SendLastThinkingContent: false,
|
relay/helper/common.go
CHANGED
|
@@ -12,11 +12,19 @@ import (
|
|
| 12 |
)
|
| 13 |
|
| 14 |
func SetEventStreamHeaders(c *gin.Context) {
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error {
|
|
@@ -37,7 +45,7 @@ func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error {
|
|
| 37 |
|
| 38 |
func ClaudeChunkData(c *gin.Context, resp dto.ClaudeResponse, data string) {
|
| 39 |
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)})
|
| 40 |
-
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s", data)})
|
| 41 |
if flusher, ok := c.Writer.(http.Flusher); ok {
|
| 42 |
flusher.Flush()
|
| 43 |
}
|
|
@@ -45,7 +53,7 @@ func ClaudeChunkData(c *gin.Context, resp dto.ClaudeResponse, data string) {
|
|
| 45 |
|
| 46 |
func ResponseChunkData(c *gin.Context, resp dto.ResponsesStreamResponse, data string) {
|
| 47 |
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)})
|
| 48 |
-
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s
|
| 49 |
if flusher, ok := c.Writer.(http.Flusher); ok {
|
| 50 |
flusher.Flush()
|
| 51 |
}
|
|
|
|
| 12 |
)
|
| 13 |
|
| 14 |
func SetEventStreamHeaders(c *gin.Context) {
|
| 15 |
+
// 检查是否已经设置过头部
|
| 16 |
+
if _, exists := c.Get("event_stream_headers_set"); exists {
|
| 17 |
+
return
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
| 21 |
+
c.Writer.Header().Set("Cache-Control", "no-cache")
|
| 22 |
+
c.Writer.Header().Set("Connection", "keep-alive")
|
| 23 |
+
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
| 24 |
+
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
| 25 |
+
|
| 26 |
+
// 设置标志,表示头部已经设置过
|
| 27 |
+
c.Set("event_stream_headers_set", true)
|
| 28 |
}
|
| 29 |
|
| 30 |
func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error {
|
|
|
|
| 45 |
|
| 46 |
func ClaudeChunkData(c *gin.Context, resp dto.ClaudeResponse, data string) {
|
| 47 |
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)})
|
| 48 |
+
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s\n", data)})
|
| 49 |
if flusher, ok := c.Writer.(http.Flusher); ok {
|
| 50 |
flusher.Flush()
|
| 51 |
}
|
|
|
|
| 53 |
|
| 54 |
func ResponseChunkData(c *gin.Context, resp dto.ResponsesStreamResponse, data string) {
|
| 55 |
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)})
|
| 56 |
+
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s", data)})
|
| 57 |
if flusher, ok := c.Writer.(http.Flusher); ok {
|
| 58 |
flusher.Flush()
|
| 59 |
}
|
relay/helper/model_mapped.go
CHANGED
|
@@ -2,9 +2,11 @@ package helper
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"encoding/json"
|
|
|
|
| 5 |
"fmt"
|
| 6 |
-
"github.com/gin-gonic/gin"
|
| 7 |
"one-api/relay/common"
|
|
|
|
|
|
|
| 8 |
)
|
| 9 |
|
| 10 |
func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
|
|
@@ -16,9 +18,36 @@ func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
|
|
| 16 |
if err != nil {
|
| 17 |
return fmt.Errorf("unmarshal_model_mapping_failed")
|
| 18 |
}
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
}
|
| 24 |
return nil
|
|
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"encoding/json"
|
| 5 |
+
"errors"
|
| 6 |
"fmt"
|
|
|
|
| 7 |
"one-api/relay/common"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
)
|
| 11 |
|
| 12 |
func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
|
|
|
|
| 18 |
if err != nil {
|
| 19 |
return fmt.Errorf("unmarshal_model_mapping_failed")
|
| 20 |
}
|
| 21 |
+
|
| 22 |
+
// 支持链式模型重定向,最终使用链尾的模型
|
| 23 |
+
currentModel := info.OriginModelName
|
| 24 |
+
visitedModels := map[string]bool{
|
| 25 |
+
currentModel: true,
|
| 26 |
+
}
|
| 27 |
+
for {
|
| 28 |
+
if mappedModel, exists := modelMap[currentModel]; exists && mappedModel != "" {
|
| 29 |
+
// 模型重定向循环检测,避免无限循环
|
| 30 |
+
if visitedModels[mappedModel] {
|
| 31 |
+
if mappedModel == currentModel {
|
| 32 |
+
if currentModel == info.OriginModelName {
|
| 33 |
+
info.IsModelMapped = false
|
| 34 |
+
return nil
|
| 35 |
+
} else {
|
| 36 |
+
info.IsModelMapped = true
|
| 37 |
+
break
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
return errors.New("model_mapping_contains_cycle")
|
| 41 |
+
}
|
| 42 |
+
visitedModels[mappedModel] = true
|
| 43 |
+
currentModel = mappedModel
|
| 44 |
+
info.IsModelMapped = true
|
| 45 |
+
} else {
|
| 46 |
+
break
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
if info.IsModelMapped {
|
| 50 |
+
info.UpstreamModelName = currentModel
|
| 51 |
}
|
| 52 |
}
|
| 53 |
return nil
|
relay/helper/price.go
CHANGED
|
@@ -23,7 +23,7 @@ type PriceData struct {
|
|
| 23 |
}
|
| 24 |
|
| 25 |
func (p PriceData) ToSetting() string {
|
| 26 |
-
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %
|
| 27 |
}
|
| 28 |
|
| 29 |
func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
func (p PriceData) ToSetting() string {
|
| 26 |
+
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio)
|
| 27 |
}
|
| 28 |
|
| 29 |
func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
|
relay/helper/stream_scanner.go
CHANGED
|
@@ -3,7 +3,6 @@ package helper
|
|
| 3 |
import (
|
| 4 |
"bufio"
|
| 5 |
"context"
|
| 6 |
-
"github.com/bytedance/gopkg/util/gopool"
|
| 7 |
"io"
|
| 8 |
"net/http"
|
| 9 |
"one-api/common"
|
|
@@ -14,6 +13,8 @@ import (
|
|
| 14 |
"sync"
|
| 15 |
"time"
|
| 16 |
|
|
|
|
|
|
|
| 17 |
"github.com/gin-gonic/gin"
|
| 18 |
)
|
| 19 |
|
|
|
|
| 3 |
import (
|
| 4 |
"bufio"
|
| 5 |
"context"
|
|
|
|
| 6 |
"io"
|
| 7 |
"net/http"
|
| 8 |
"one-api/common"
|
|
|
|
| 13 |
"sync"
|
| 14 |
"time"
|
| 15 |
|
| 16 |
+
"github.com/bytedance/gopkg/util/gopool"
|
| 17 |
+
|
| 18 |
"github.com/gin-gonic/gin"
|
| 19 |
)
|
| 20 |
|
relay/relay-image.go
CHANGED
|
@@ -49,11 +49,11 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
|
|
| 49 |
// Not "256x256", "512x512", or "1024x1024"
|
| 50 |
if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" {
|
| 51 |
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
|
| 52 |
-
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024
|
| 53 |
}
|
| 54 |
} else if imageRequest.Model == "dall-e-3" {
|
| 55 |
if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" {
|
| 56 |
-
return nil, errors.New("size must be one of
|
| 57 |
}
|
| 58 |
if imageRequest.Quality == "" {
|
| 59 |
imageRequest.Quality = "standard"
|
|
|
|
| 49 |
// Not "256x256", "512x512", or "1024x1024"
|
| 50 |
if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" {
|
| 51 |
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
|
| 52 |
+
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024 for dall-e-2 or dall-e")
|
| 53 |
}
|
| 54 |
} else if imageRequest.Model == "dall-e-3" {
|
| 55 |
if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" {
|
| 56 |
+
return nil, errors.New("size must be one of 1024x1024, 1024x1792 or 1792x1024 for dall-e-3")
|
| 57 |
}
|
| 58 |
if imageRequest.Quality == "" {
|
| 59 |
imageRequest.Quality = "standard"
|
relay/relay-responses.go
CHANGED
|
@@ -19,7 +19,7 @@ import (
|
|
| 19 |
"github.com/gin-gonic/gin"
|
| 20 |
)
|
| 21 |
|
| 22 |
-
func getAndValidateResponsesRequest(c *gin.Context
|
| 23 |
request := &dto.OpenAIResponsesRequest{}
|
| 24 |
err := common.UnmarshalBodyReusable(c, request)
|
| 25 |
if err != nil {
|
|
@@ -31,13 +31,11 @@ func getAndValidateResponsesRequest(c *gin.Context, relayInfo *relaycommon.Relay
|
|
| 31 |
if len(request.Input) == 0 {
|
| 32 |
return nil, errors.New("input is required")
|
| 33 |
}
|
| 34 |
-
relayInfo.IsStream = request.Stream
|
| 35 |
return request, nil
|
| 36 |
|
| 37 |
}
|
| 38 |
|
| 39 |
func checkInputSensitive(textRequest *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo) ([]string, error) {
|
| 40 |
-
|
| 41 |
sensitiveWords, err := service.CheckSensitiveInput(textRequest.Input)
|
| 42 |
return sensitiveWords, err
|
| 43 |
}
|
|
@@ -49,12 +47,14 @@ func getInputTokens(req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo
|
|
| 49 |
}
|
| 50 |
|
| 51 |
func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
| 52 |
-
|
| 53 |
-
req, err := getAndValidateResponsesRequest(c, relayInfo)
|
| 54 |
if err != nil {
|
| 55 |
common.LogError(c, fmt.Sprintf("getAndValidateResponsesRequest error: %s", err.Error()))
|
| 56 |
return service.OpenAIErrorWrapperLocal(err, "invalid_responses_request", http.StatusBadRequest)
|
| 57 |
}
|
|
|
|
|
|
|
|
|
|
| 58 |
if setting.ShouldCheckPromptSensitive() {
|
| 59 |
sensitiveWords, err := checkInputSensitive(req, relayInfo)
|
| 60 |
if err != nil {
|
|
|
|
| 19 |
"github.com/gin-gonic/gin"
|
| 20 |
)
|
| 21 |
|
| 22 |
+
func getAndValidateResponsesRequest(c *gin.Context) (*dto.OpenAIResponsesRequest, error) {
|
| 23 |
request := &dto.OpenAIResponsesRequest{}
|
| 24 |
err := common.UnmarshalBodyReusable(c, request)
|
| 25 |
if err != nil {
|
|
|
|
| 31 |
if len(request.Input) == 0 {
|
| 32 |
return nil, errors.New("input is required")
|
| 33 |
}
|
|
|
|
| 34 |
return request, nil
|
| 35 |
|
| 36 |
}
|
| 37 |
|
| 38 |
func checkInputSensitive(textRequest *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo) ([]string, error) {
|
|
|
|
| 39 |
sensitiveWords, err := service.CheckSensitiveInput(textRequest.Input)
|
| 40 |
return sensitiveWords, err
|
| 41 |
}
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
| 50 |
+
req, err := getAndValidateResponsesRequest(c)
|
|
|
|
| 51 |
if err != nil {
|
| 52 |
common.LogError(c, fmt.Sprintf("getAndValidateResponsesRequest error: %s", err.Error()))
|
| 53 |
return service.OpenAIErrorWrapperLocal(err, "invalid_responses_request", http.StatusBadRequest)
|
| 54 |
}
|
| 55 |
+
|
| 56 |
+
relayInfo := relaycommon.GenRelayInfoResponses(c, req)
|
| 57 |
+
|
| 58 |
if setting.ShouldCheckPromptSensitive() {
|
| 59 |
sensitiveWords, err := checkInputSensitive(req, relayInfo)
|
| 60 |
if err != nil {
|
relay/relay-text.go
CHANGED
|
@@ -18,6 +18,7 @@ import (
|
|
| 18 |
"one-api/service"
|
| 19 |
"one-api/setting"
|
| 20 |
"one-api/setting/model_setting"
|
|
|
|
| 21 |
"strings"
|
| 22 |
"time"
|
| 23 |
|
|
@@ -193,6 +194,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
|
| 193 |
|
| 194 |
var httpResp *http.Response
|
| 195 |
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
|
|
|
| 196 |
if err != nil {
|
| 197 |
return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
| 198 |
}
|
|
@@ -358,6 +360,34 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|
| 358 |
|
| 359 |
ratio := dModelRatio.Mul(dGroupRatio)
|
| 360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
var quotaCalculateDecimal decimal.Decimal
|
| 362 |
if !priceData.UsePrice {
|
| 363 |
nonCachedTokens := dPromptTokens.Sub(dCacheTokens)
|
|
@@ -380,6 +410,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|
| 380 |
} else {
|
| 381 |
quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
|
| 382 |
}
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
| 385 |
totalTokens := promptTokens + completionTokens
|
|
@@ -430,6 +463,20 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|
| 430 |
other["image_ratio"] = imageRatio
|
| 431 |
other["image_output"] = imageTokens
|
| 432 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,
|
| 434 |
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
| 435 |
}
|
|
|
|
| 18 |
"one-api/service"
|
| 19 |
"one-api/setting"
|
| 20 |
"one-api/setting/model_setting"
|
| 21 |
+
"one-api/setting/operation_setting"
|
| 22 |
"strings"
|
| 23 |
"time"
|
| 24 |
|
|
|
|
| 194 |
|
| 195 |
var httpResp *http.Response
|
| 196 |
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
| 197 |
+
|
| 198 |
if err != nil {
|
| 199 |
return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
| 200 |
}
|
|
|
|
| 360 |
|
| 361 |
ratio := dModelRatio.Mul(dGroupRatio)
|
| 362 |
|
| 363 |
+
// openai web search 工具计费
|
| 364 |
+
var dWebSearchQuota decimal.Decimal
|
| 365 |
+
var webSearchPrice float64
|
| 366 |
+
if relayInfo.ResponsesUsageInfo != nil {
|
| 367 |
+
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 {
|
| 368 |
+
// 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000 * 分组倍率)
|
| 369 |
+
webSearchPrice = operation_setting.GetWebSearchPricePerThousand(modelName, webSearchTool.SearchContextSize)
|
| 370 |
+
dWebSearchQuota = decimal.NewFromFloat(webSearchPrice).
|
| 371 |
+
Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))).
|
| 372 |
+
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
| 373 |
+
extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s,调用花费 $%s",
|
| 374 |
+
webSearchTool.CallCount, webSearchTool.SearchContextSize, dWebSearchQuota.String())
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
// file search tool 计费
|
| 378 |
+
var dFileSearchQuota decimal.Decimal
|
| 379 |
+
var fileSearchPrice float64
|
| 380 |
+
if relayInfo.ResponsesUsageInfo != nil {
|
| 381 |
+
if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists && fileSearchTool.CallCount > 0 {
|
| 382 |
+
fileSearchPrice = operation_setting.GetFileSearchPricePerThousand()
|
| 383 |
+
dFileSearchQuota = decimal.NewFromFloat(fileSearchPrice).
|
| 384 |
+
Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))).
|
| 385 |
+
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
| 386 |
+
extraContent += fmt.Sprintf("File Search 调用 %d 次,调用花费 $%s",
|
| 387 |
+
fileSearchTool.CallCount, dFileSearchQuota.String())
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
var quotaCalculateDecimal decimal.Decimal
|
| 392 |
if !priceData.UsePrice {
|
| 393 |
nonCachedTokens := dPromptTokens.Sub(dCacheTokens)
|
|
|
|
| 410 |
} else {
|
| 411 |
quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
|
| 412 |
}
|
| 413 |
+
// 添加 responses tools call 调用的配额
|
| 414 |
+
quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
|
| 415 |
+
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
| 416 |
|
| 417 |
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
| 418 |
totalTokens := promptTokens + completionTokens
|
|
|
|
| 463 |
other["image_ratio"] = imageRatio
|
| 464 |
other["image_output"] = imageTokens
|
| 465 |
}
|
| 466 |
+
if !dWebSearchQuota.IsZero() && relayInfo.ResponsesUsageInfo != nil {
|
| 467 |
+
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists {
|
| 468 |
+
other["web_search"] = true
|
| 469 |
+
other["web_search_call_count"] = webSearchTool.CallCount
|
| 470 |
+
other["web_search_price"] = webSearchPrice
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
if !dFileSearchQuota.IsZero() && relayInfo.ResponsesUsageInfo != nil {
|
| 474 |
+
if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists {
|
| 475 |
+
other["file_search"] = true
|
| 476 |
+
other["file_search_call_count"] = fileSearchTool.CallCount
|
| 477 |
+
other["file_search_price"] = fileSearchPrice
|
| 478 |
+
}
|
| 479 |
+
}
|
| 480 |
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,
|
| 481 |
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
| 482 |
}
|
service/cf_worker.go
CHANGED
|
@@ -24,7 +24,7 @@ func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) {
|
|
| 24 |
if !setting.EnableWorker() {
|
| 25 |
return nil, fmt.Errorf("worker not enabled")
|
| 26 |
}
|
| 27 |
-
if !strings.HasPrefix(req.URL, "https") {
|
| 28 |
return nil, fmt.Errorf("only support https url")
|
| 29 |
}
|
| 30 |
|
|
|
|
| 24 |
if !setting.EnableWorker() {
|
| 25 |
return nil, fmt.Errorf("worker not enabled")
|
| 26 |
}
|
| 27 |
+
if !setting.WorkerAllowHttpImageRequestEnabled && !strings.HasPrefix(req.URL, "https") {
|
| 28 |
return nil, fmt.Errorf("only support https url")
|
| 29 |
}
|
| 30 |
|
setting/operation_setting/tools.go
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package operation_setting
|
| 2 |
+
|
| 3 |
+
import "strings"
|
| 4 |
+
|
| 5 |
+
const (
|
| 6 |
+
// Web search
|
| 7 |
+
WebSearchHighTierModelPriceLow = 30.00
|
| 8 |
+
WebSearchHighTierModelPriceMedium = 35.00
|
| 9 |
+
WebSearchHighTierModelPriceHigh = 50.00
|
| 10 |
+
WebSearchPriceLow = 25.00
|
| 11 |
+
WebSearchPriceMedium = 27.50
|
| 12 |
+
WebSearchPriceHigh = 30.00
|
| 13 |
+
// File search
|
| 14 |
+
FileSearchPrice = 2.5
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
func GetWebSearchPricePerThousand(modelName string, contextSize string) float64 {
|
| 18 |
+
// 确定模型类型
|
| 19 |
+
// https://platform.openai.com/docs/pricing Web search 价格按模型类型和 search context size 收费
|
| 20 |
+
// gpt-4.1, gpt-4o, or gpt-4o-search-preview 更贵,gpt-4.1-mini, gpt-4o-mini, gpt-4o-mini-search-preview 更便宜
|
| 21 |
+
isHighTierModel := (strings.HasPrefix(modelName, "gpt-4.1") || strings.HasPrefix(modelName, "gpt-4o")) &&
|
| 22 |
+
!strings.Contains(modelName, "mini")
|
| 23 |
+
// 确定 search context size 对应的价格
|
| 24 |
+
var priceWebSearchPerThousandCalls float64
|
| 25 |
+
switch contextSize {
|
| 26 |
+
case "low":
|
| 27 |
+
if isHighTierModel {
|
| 28 |
+
priceWebSearchPerThousandCalls = WebSearchHighTierModelPriceLow
|
| 29 |
+
} else {
|
| 30 |
+
priceWebSearchPerThousandCalls = WebSearchPriceLow
|
| 31 |
+
}
|
| 32 |
+
case "medium":
|
| 33 |
+
if isHighTierModel {
|
| 34 |
+
priceWebSearchPerThousandCalls = WebSearchHighTierModelPriceMedium
|
| 35 |
+
} else {
|
| 36 |
+
priceWebSearchPerThousandCalls = WebSearchPriceMedium
|
| 37 |
+
}
|
| 38 |
+
case "high":
|
| 39 |
+
if isHighTierModel {
|
| 40 |
+
priceWebSearchPerThousandCalls = WebSearchHighTierModelPriceHigh
|
| 41 |
+
} else {
|
| 42 |
+
priceWebSearchPerThousandCalls = WebSearchPriceHigh
|
| 43 |
+
}
|
| 44 |
+
default:
|
| 45 |
+
// search context size 默认为 medium
|
| 46 |
+
if isHighTierModel {
|
| 47 |
+
priceWebSearchPerThousandCalls = WebSearchHighTierModelPriceMedium
|
| 48 |
+
} else {
|
| 49 |
+
priceWebSearchPerThousandCalls = WebSearchPriceMedium
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
return priceWebSearchPerThousandCalls
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func GetFileSearchPricePerThousand() float64 {
|
| 56 |
+
return FileSearchPrice
|
| 57 |
+
}
|
setting/rate_limit.go
CHANGED
|
@@ -1,6 +1,64 @@
|
|
| 1 |
package setting
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
var ModelRequestRateLimitEnabled = false
|
| 4 |
var ModelRequestRateLimitDurationMinutes = 1
|
| 5 |
var ModelRequestRateLimitCount = 0
|
| 6 |
var ModelRequestRateLimitSuccessCount = 1000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
package setting
|
| 2 |
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
"sync"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
var ModelRequestRateLimitEnabled = false
|
| 11 |
var ModelRequestRateLimitDurationMinutes = 1
|
| 12 |
var ModelRequestRateLimitCount = 0
|
| 13 |
var ModelRequestRateLimitSuccessCount = 1000
|
| 14 |
+
var ModelRequestRateLimitGroup = map[string][2]int{}
|
| 15 |
+
var ModelRequestRateLimitMutex sync.RWMutex
|
| 16 |
+
|
| 17 |
+
func ModelRequestRateLimitGroup2JSONString() string {
|
| 18 |
+
ModelRequestRateLimitMutex.RLock()
|
| 19 |
+
defer ModelRequestRateLimitMutex.RUnlock()
|
| 20 |
+
|
| 21 |
+
jsonBytes, err := json.Marshal(ModelRequestRateLimitGroup)
|
| 22 |
+
if err != nil {
|
| 23 |
+
common.SysError("error marshalling model ratio: " + err.Error())
|
| 24 |
+
}
|
| 25 |
+
return string(jsonBytes)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func UpdateModelRequestRateLimitGroupByJSONString(jsonStr string) error {
|
| 29 |
+
ModelRequestRateLimitMutex.RLock()
|
| 30 |
+
defer ModelRequestRateLimitMutex.RUnlock()
|
| 31 |
+
|
| 32 |
+
ModelRequestRateLimitGroup = make(map[string][2]int)
|
| 33 |
+
return json.Unmarshal([]byte(jsonStr), &ModelRequestRateLimitGroup)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func GetGroupRateLimit(group string) (totalCount, successCount int, found bool) {
|
| 37 |
+
ModelRequestRateLimitMutex.RLock()
|
| 38 |
+
defer ModelRequestRateLimitMutex.RUnlock()
|
| 39 |
+
|
| 40 |
+
if ModelRequestRateLimitGroup == nil {
|
| 41 |
+
return 0, 0, false
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
limits, found := ModelRequestRateLimitGroup[group]
|
| 45 |
+
if !found {
|
| 46 |
+
return 0, 0, false
|
| 47 |
+
}
|
| 48 |
+
return limits[0], limits[1], true
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func CheckModelRequestRateLimitGroup(jsonStr string) error {
|
| 52 |
+
checkModelRequestRateLimitGroup := make(map[string][2]int)
|
| 53 |
+
err := json.Unmarshal([]byte(jsonStr), &checkModelRequestRateLimitGroup)
|
| 54 |
+
if err != nil {
|
| 55 |
+
return err
|
| 56 |
+
}
|
| 57 |
+
for group, limits := range checkModelRequestRateLimitGroup {
|
| 58 |
+
if limits[0] < 0 || limits[1] < 1 {
|
| 59 |
+
return fmt.Errorf("group %s has negative rate limit values: [%d, %d]", group, limits[0], limits[1])
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return nil
|
| 64 |
+
}
|
setting/system_setting.go
CHANGED
|
@@ -3,6 +3,7 @@ package setting
|
|
| 3 |
var ServerAddress = "http://localhost:3000"
|
| 4 |
var WorkerUrl = ""
|
| 5 |
var WorkerValidKey = ""
|
|
|
|
| 6 |
|
| 7 |
func EnableWorker() bool {
|
| 8 |
return WorkerUrl != ""
|
|
|
|
| 3 |
var ServerAddress = "http://localhost:3000"
|
| 4 |
var WorkerUrl = ""
|
| 5 |
var WorkerValidKey = ""
|
| 6 |
+
var WorkerAllowHttpImageRequestEnabled = false
|
| 7 |
|
| 8 |
func EnableWorker() bool {
|
| 9 |
return WorkerUrl != ""
|
web/src/components/LogsTable.js
CHANGED
|
@@ -618,7 +618,6 @@ const LogsTable = () => {
|
|
| 618 |
</Paragraph>
|
| 619 |
);
|
| 620 |
}
|
| 621 |
-
|
| 622 |
let content = other?.claude
|
| 623 |
? renderClaudeModelPriceSimple(
|
| 624 |
other.model_ratio,
|
|
@@ -935,6 +934,13 @@ const LogsTable = () => {
|
|
| 935 |
other.model_price,
|
| 936 |
other.group_ratio,
|
| 937 |
other?.user_group_ratio,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
),
|
| 939 |
});
|
| 940 |
}
|
|
@@ -995,6 +1001,12 @@ const LogsTable = () => {
|
|
| 995 |
other?.image || false,
|
| 996 |
other?.image_ratio || 0,
|
| 997 |
other?.image_output || 0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
);
|
| 999 |
}
|
| 1000 |
expandDataLocal.push({
|
|
|
|
| 618 |
</Paragraph>
|
| 619 |
);
|
| 620 |
}
|
|
|
|
| 621 |
let content = other?.claude
|
| 622 |
? renderClaudeModelPriceSimple(
|
| 623 |
other.model_ratio,
|
|
|
|
| 934 |
other.model_price,
|
| 935 |
other.group_ratio,
|
| 936 |
other?.user_group_ratio,
|
| 937 |
+
false,
|
| 938 |
+
1.0,
|
| 939 |
+
undefined,
|
| 940 |
+
other.web_search || false,
|
| 941 |
+
other.web_search_call_count || 0,
|
| 942 |
+
other.file_search || false,
|
| 943 |
+
other.file_search_call_count || 0,
|
| 944 |
),
|
| 945 |
});
|
| 946 |
}
|
|
|
|
| 1001 |
other?.image || false,
|
| 1002 |
other?.image_ratio || 0,
|
| 1003 |
other?.image_output || 0,
|
| 1004 |
+
other?.web_search || false,
|
| 1005 |
+
other?.web_search_call_count || 0,
|
| 1006 |
+
other?.web_search_price || 0,
|
| 1007 |
+
other?.file_search || false,
|
| 1008 |
+
other?.file_search_call_count || 0,
|
| 1009 |
+
other?.file_search_price || 0,
|
| 1010 |
);
|
| 1011 |
}
|
| 1012 |
expandDataLocal.push({
|
web/src/components/PersonalSetting.js
CHANGED
|
@@ -57,6 +57,7 @@ const PersonalSetting = () => {
|
|
| 57 |
email_verification_code: '',
|
| 58 |
email: '',
|
| 59 |
self_account_deletion_confirmation: '',
|
|
|
|
| 60 |
set_new_password: '',
|
| 61 |
set_new_password_confirmation: '',
|
| 62 |
});
|
|
@@ -239,11 +240,24 @@ const PersonalSetting = () => {
|
|
| 239 |
};
|
| 240 |
|
| 241 |
const changePassword = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
|
| 243 |
showError(t('两次输入的密码不一致!'));
|
| 244 |
return;
|
| 245 |
}
|
| 246 |
const res = await API.put(`/api/user/self`, {
|
|
|
|
| 247 |
password: inputs.set_new_password,
|
| 248 |
});
|
| 249 |
const { success, message } = res.data;
|
|
@@ -816,8 +830,8 @@ const PersonalSetting = () => {
|
|
| 816 |
</div>
|
| 817 |
</Card>
|
| 818 |
<Card style={{ marginTop: 10 }}>
|
| 819 |
-
<Tabs type=
|
| 820 |
-
<TabPane tab={t('通知设置')} itemKey=
|
| 821 |
<div style={{ marginTop: 20 }}>
|
| 822 |
<Typography.Text strong>{t('通知方式')}</Typography.Text>
|
| 823 |
<div style={{ marginTop: 10 }}>
|
|
@@ -993,23 +1007,36 @@ const PersonalSetting = () => {
|
|
| 993 |
</Typography.Text>
|
| 994 |
</div>
|
| 995 |
</TabPane>
|
| 996 |
-
<TabPane tab={t('价格设置')} itemKey=
|
| 997 |
<div style={{ marginTop: 20 }}>
|
| 998 |
-
<Typography.Text strong>
|
|
|
|
|
|
|
| 999 |
<div style={{ marginTop: 10 }}>
|
| 1000 |
<Checkbox
|
| 1001 |
-
checked={
|
| 1002 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
>
|
| 1004 |
{t('接受未设置价格模型')}
|
| 1005 |
</Checkbox>
|
| 1006 |
-
<Typography.Text
|
| 1007 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
</Typography.Text>
|
| 1009 |
</div>
|
| 1010 |
</div>
|
| 1011 |
</TabPane>
|
| 1012 |
-
|
| 1013 |
</Tabs>
|
| 1014 |
<div style={{ marginTop: 20 }}>
|
| 1015 |
<Button type='primary' onClick={saveNotificationSettings}>
|
|
@@ -1118,6 +1145,16 @@ const PersonalSetting = () => {
|
|
| 1118 |
>
|
| 1119 |
<div style={{ marginTop: 20 }}>
|
| 1120 |
<Input
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1121 |
name='set_new_password'
|
| 1122 |
placeholder={t('新密码')}
|
| 1123 |
value={inputs.set_new_password}
|
|
|
|
| 57 |
email_verification_code: '',
|
| 58 |
email: '',
|
| 59 |
self_account_deletion_confirmation: '',
|
| 60 |
+
original_password: '',
|
| 61 |
set_new_password: '',
|
| 62 |
set_new_password_confirmation: '',
|
| 63 |
});
|
|
|
|
| 240 |
};
|
| 241 |
|
| 242 |
const changePassword = async () => {
|
| 243 |
+
if (inputs.original_password === '') {
|
| 244 |
+
showError(t('请输入原密码!'));
|
| 245 |
+
return;
|
| 246 |
+
}
|
| 247 |
+
if (inputs.set_new_password === '') {
|
| 248 |
+
showError(t('请输入新密码!'));
|
| 249 |
+
return;
|
| 250 |
+
}
|
| 251 |
+
if (inputs.original_password === inputs.set_new_password) {
|
| 252 |
+
showError(t('新密码需要和原密码不一致!'));
|
| 253 |
+
return;
|
| 254 |
+
}
|
| 255 |
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
|
| 256 |
showError(t('两次输入的密码不一致!'));
|
| 257 |
return;
|
| 258 |
}
|
| 259 |
const res = await API.put(`/api/user/self`, {
|
| 260 |
+
original_password: inputs.original_password,
|
| 261 |
password: inputs.set_new_password,
|
| 262 |
});
|
| 263 |
const { success, message } = res.data;
|
|
|
|
| 830 |
</div>
|
| 831 |
</Card>
|
| 832 |
<Card style={{ marginTop: 10 }}>
|
| 833 |
+
<Tabs type='line' defaultActiveKey='notification'>
|
| 834 |
+
<TabPane tab={t('通知设置')} itemKey='notification'>
|
| 835 |
<div style={{ marginTop: 20 }}>
|
| 836 |
<Typography.Text strong>{t('通知方式')}</Typography.Text>
|
| 837 |
<div style={{ marginTop: 10 }}>
|
|
|
|
| 1007 |
</Typography.Text>
|
| 1008 |
</div>
|
| 1009 |
</TabPane>
|
| 1010 |
+
<TabPane tab={t('价格设置')} itemKey='price'>
|
| 1011 |
<div style={{ marginTop: 20 }}>
|
| 1012 |
+
<Typography.Text strong>
|
| 1013 |
+
{t('接受未设置价格模型')}
|
| 1014 |
+
</Typography.Text>
|
| 1015 |
<div style={{ marginTop: 10 }}>
|
| 1016 |
<Checkbox
|
| 1017 |
+
checked={
|
| 1018 |
+
notificationSettings.acceptUnsetModelRatioModel
|
| 1019 |
+
}
|
| 1020 |
+
onChange={(e) =>
|
| 1021 |
+
handleNotificationSettingChange(
|
| 1022 |
+
'acceptUnsetModelRatioModel',
|
| 1023 |
+
e.target.checked,
|
| 1024 |
+
)
|
| 1025 |
+
}
|
| 1026 |
>
|
| 1027 |
{t('接受未设置价格模型')}
|
| 1028 |
</Checkbox>
|
| 1029 |
+
<Typography.Text
|
| 1030 |
+
type='secondary'
|
| 1031 |
+
style={{ marginTop: 8, display: 'block' }}
|
| 1032 |
+
>
|
| 1033 |
+
{t(
|
| 1034 |
+
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
|
| 1035 |
+
)}
|
| 1036 |
</Typography.Text>
|
| 1037 |
</div>
|
| 1038 |
</div>
|
| 1039 |
</TabPane>
|
|
|
|
| 1040 |
</Tabs>
|
| 1041 |
<div style={{ marginTop: 20 }}>
|
| 1042 |
<Button type='primary' onClick={saveNotificationSettings}>
|
|
|
|
| 1145 |
>
|
| 1146 |
<div style={{ marginTop: 20 }}>
|
| 1147 |
<Input
|
| 1148 |
+
name='original_password'
|
| 1149 |
+
placeholder={t('原密码')}
|
| 1150 |
+
type='password'
|
| 1151 |
+
value={inputs.original_password}
|
| 1152 |
+
onChange={(value) =>
|
| 1153 |
+
handleInputChange('original_password', value)
|
| 1154 |
+
}
|
| 1155 |
+
/>
|
| 1156 |
+
<Input
|
| 1157 |
+
style={{ marginTop: 20 }}
|
| 1158 |
name='set_new_password'
|
| 1159 |
placeholder={t('新密码')}
|
| 1160 |
value={inputs.set_new_password}
|
web/src/components/RateLimitSetting.js
CHANGED
|
@@ -13,6 +13,7 @@ const RateLimitSetting = () => {
|
|
| 13 |
ModelRequestRateLimitCount: 0,
|
| 14 |
ModelRequestRateLimitSuccessCount: 1000,
|
| 15 |
ModelRequestRateLimitDurationMinutes: 1,
|
|
|
|
| 16 |
});
|
| 17 |
|
| 18 |
let [loading, setLoading] = useState(false);
|
|
@@ -23,10 +24,14 @@ const RateLimitSetting = () => {
|
|
| 23 |
if (success) {
|
| 24 |
let newInputs = {};
|
| 25 |
data.forEach((item) => {
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
});
|
| 32 |
|
|
|
|
| 13 |
ModelRequestRateLimitCount: 0,
|
| 14 |
ModelRequestRateLimitSuccessCount: 1000,
|
| 15 |
ModelRequestRateLimitDurationMinutes: 1,
|
| 16 |
+
ModelRequestRateLimitGroup: '',
|
| 17 |
});
|
| 18 |
|
| 19 |
let [loading, setLoading] = useState(false);
|
|
|
|
| 24 |
if (success) {
|
| 25 |
let newInputs = {};
|
| 26 |
data.forEach((item) => {
|
| 27 |
+
if (item.key === 'ModelRequestRateLimitGroup') {
|
| 28 |
+
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if (item.key.endsWith('Enabled')) {
|
| 32 |
+
newInputs[item.key] = item.value === 'true' ? true : false;
|
| 33 |
+
} else {
|
| 34 |
+
newInputs[item.key] = item.value;
|
| 35 |
}
|
| 36 |
});
|
| 37 |
|
web/src/components/SystemSetting.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
| 19 |
verifyJSON,
|
| 20 |
} from '../helpers/utils';
|
| 21 |
import { API } from '../helpers/api';
|
| 22 |
-
import axios from
|
| 23 |
|
| 24 |
const SystemSetting = () => {
|
| 25 |
let [inputs, setInputs] = useState({
|
|
@@ -45,6 +45,7 @@ const SystemSetting = () => {
|
|
| 45 |
ServerAddress: '',
|
| 46 |
WorkerUrl: '',
|
| 47 |
WorkerValidKey: '',
|
|
|
|
| 48 |
EpayId: '',
|
| 49 |
EpayKey: '',
|
| 50 |
Price: 7.3,
|
|
@@ -111,6 +112,7 @@ const SystemSetting = () => {
|
|
| 111 |
case 'SMTPSSLEnabled':
|
| 112 |
case 'LinuxDOOAuthEnabled':
|
| 113 |
case 'oidc.enabled':
|
|
|
|
| 114 |
item.value = item.value === 'true';
|
| 115 |
break;
|
| 116 |
case 'Price':
|
|
@@ -206,7 +208,11 @@ const SystemSetting = () => {
|
|
| 206 |
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
| 207 |
const options = [
|
| 208 |
{ key: 'WorkerUrl', value: WorkerUrl },
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
if (inputs.WorkerValidKey !== '' || WorkerUrl === '') {
|
| 211 |
options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey });
|
| 212 |
}
|
|
@@ -302,7 +308,8 @@ const SystemSetting = () => {
|
|
| 302 |
const domain = emailToAdd.trim();
|
| 303 |
|
| 304 |
// 验证域名格式
|
| 305 |
-
const domainRegex =
|
|
|
|
| 306 |
if (!domainRegex.test(domain)) {
|
| 307 |
showError('邮箱域名格式不正确,请输入有效的域名,如 gmail.com');
|
| 308 |
return;
|
|
@@ -577,6 +584,12 @@ const SystemSetting = () => {
|
|
| 577 |
/>
|
| 578 |
</Col>
|
| 579 |
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
<Button onClick={submitWorker}>更新Worker设置</Button>
|
| 581 |
</Form.Section>
|
| 582 |
</Card>
|
|
@@ -799,7 +812,13 @@ const SystemSetting = () => {
|
|
| 799 |
onChange={(value) => setEmailToAdd(value)}
|
| 800 |
style={{ marginTop: 16 }}
|
| 801 |
suffix={
|
| 802 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
}
|
| 804 |
onEnterPress={handleAddEmail}
|
| 805 |
/>
|
|
|
|
| 19 |
verifyJSON,
|
| 20 |
} from '../helpers/utils';
|
| 21 |
import { API } from '../helpers/api';
|
| 22 |
+
import axios from 'axios';
|
| 23 |
|
| 24 |
const SystemSetting = () => {
|
| 25 |
let [inputs, setInputs] = useState({
|
|
|
|
| 45 |
ServerAddress: '',
|
| 46 |
WorkerUrl: '',
|
| 47 |
WorkerValidKey: '',
|
| 48 |
+
WorkerAllowHttpImageRequestEnabled: '',
|
| 49 |
EpayId: '',
|
| 50 |
EpayKey: '',
|
| 51 |
Price: 7.3,
|
|
|
|
| 112 |
case 'SMTPSSLEnabled':
|
| 113 |
case 'LinuxDOOAuthEnabled':
|
| 114 |
case 'oidc.enabled':
|
| 115 |
+
case 'WorkerAllowHttpImageRequestEnabled':
|
| 116 |
item.value = item.value === 'true';
|
| 117 |
break;
|
| 118 |
case 'Price':
|
|
|
|
| 208 |
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
| 209 |
const options = [
|
| 210 |
{ key: 'WorkerUrl', value: WorkerUrl },
|
| 211 |
+
{
|
| 212 |
+
key: 'WorkerAllowHttpImageRequestEnabled',
|
| 213 |
+
value: inputs.WorkerAllowHttpImageRequestEnabled ? 'true' : 'false',
|
| 214 |
+
},
|
| 215 |
+
];
|
| 216 |
if (inputs.WorkerValidKey !== '' || WorkerUrl === '') {
|
| 217 |
options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey });
|
| 218 |
}
|
|
|
|
| 308 |
const domain = emailToAdd.trim();
|
| 309 |
|
| 310 |
// 验证域名格式
|
| 311 |
+
const domainRegex =
|
| 312 |
+
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
| 313 |
if (!domainRegex.test(domain)) {
|
| 314 |
showError('邮箱域名格式不正确,请输入有效的域名,如 gmail.com');
|
| 315 |
return;
|
|
|
|
| 584 |
/>
|
| 585 |
</Col>
|
| 586 |
</Row>
|
| 587 |
+
<Form.Checkbox
|
| 588 |
+
field='WorkerAllowHttpImageRequestEnabled'
|
| 589 |
+
noLabel
|
| 590 |
+
>
|
| 591 |
+
允许 HTTP 协议图片请求(适用于自部署代理)
|
| 592 |
+
</Form.Checkbox>
|
| 593 |
<Button onClick={submitWorker}>更新Worker设置</Button>
|
| 594 |
</Form.Section>
|
| 595 |
</Card>
|
|
|
|
| 812 |
onChange={(value) => setEmailToAdd(value)}
|
| 813 |
style={{ marginTop: 16 }}
|
| 814 |
suffix={
|
| 815 |
+
<Button
|
| 816 |
+
theme='solid'
|
| 817 |
+
type='primary'
|
| 818 |
+
onClick={handleAddEmail}
|
| 819 |
+
>
|
| 820 |
+
添加
|
| 821 |
+
</Button>
|
| 822 |
}
|
| 823 |
onEnterPress={handleAddEmail}
|
| 824 |
/>
|
web/src/helpers/render.js
CHANGED
|
@@ -317,6 +317,12 @@ export function renderModelPrice(
|
|
| 317 |
image = false,
|
| 318 |
imageRatio = 1.0,
|
| 319 |
imageOutputTokens = 0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
) {
|
| 321 |
if (modelPrice !== -1) {
|
| 322 |
return i18next.t(
|
|
@@ -339,14 +345,17 @@ export function renderModelPrice(
|
|
| 339 |
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
| 340 |
let effectiveInputTokens =
|
| 341 |
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
| 342 |
-
// Handle image tokens if present
|
| 343 |
if (image && imageOutputTokens > 0) {
|
| 344 |
-
effectiveInputTokens =
|
|
|
|
| 345 |
}
|
| 346 |
|
| 347 |
let price =
|
| 348 |
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
| 349 |
-
(completionTokens / 1000000) * completionRatioPrice * groupRatio
|
|
|
|
|
|
|
| 350 |
|
| 351 |
return (
|
| 352 |
<>
|
|
@@ -391,9 +400,23 @@ export function renderModelPrice(
|
|
| 391 |
)}
|
| 392 |
</p>
|
| 393 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
<p></p>
|
| 395 |
<p>
|
| 396 |
-
{cacheTokens > 0 && !image
|
| 397 |
? i18next.t(
|
| 398 |
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
| 399 |
{
|
|
@@ -407,31 +430,82 @@ export function renderModelPrice(
|
|
| 407 |
total: price.toFixed(6),
|
| 408 |
},
|
| 409 |
)
|
| 410 |
-
: image && imageOutputTokens > 0
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
</p>
|
| 436 |
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
| 437 |
</article>
|
|
@@ -448,33 +522,56 @@ export function renderLogContent(
|
|
| 448 |
user_group_ratio,
|
| 449 |
image = false,
|
| 450 |
imageRatio = 1.0,
|
| 451 |
-
useUserGroupRatio = undefined
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
) {
|
| 453 |
-
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
|
| 454 |
const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
| 455 |
|
| 456 |
if (modelPrice !== -1) {
|
| 457 |
return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
|
| 458 |
price: modelPrice,
|
| 459 |
ratioType: ratioLabel,
|
| 460 |
-
ratio
|
| 461 |
});
|
| 462 |
} else {
|
| 463 |
if (image) {
|
| 464 |
-
return i18next.t(
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
} else {
|
| 472 |
-
return i18next.t(
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
| 478 |
}
|
| 479 |
}
|
| 480 |
}
|
|
|
|
| 317 |
image = false,
|
| 318 |
imageRatio = 1.0,
|
| 319 |
imageOutputTokens = 0,
|
| 320 |
+
webSearch = false,
|
| 321 |
+
webSearchCallCount = 0,
|
| 322 |
+
webSearchPrice = 0,
|
| 323 |
+
fileSearch = false,
|
| 324 |
+
fileSearchCallCount = 0,
|
| 325 |
+
fileSearchPrice = 0,
|
| 326 |
) {
|
| 327 |
if (modelPrice !== -1) {
|
| 328 |
return i18next.t(
|
|
|
|
| 345 |
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
| 346 |
let effectiveInputTokens =
|
| 347 |
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
| 348 |
+
// Handle image tokens if present
|
| 349 |
if (image && imageOutputTokens > 0) {
|
| 350 |
+
effectiveInputTokens =
|
| 351 |
+
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
|
| 352 |
}
|
| 353 |
|
| 354 |
let price =
|
| 355 |
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
| 356 |
+
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
| 357 |
+
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
|
| 358 |
+
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
|
| 359 |
|
| 360 |
return (
|
| 361 |
<>
|
|
|
|
| 400 |
)}
|
| 401 |
</p>
|
| 402 |
)}
|
| 403 |
+
{webSearch && webSearchCallCount > 0 && (
|
| 404 |
+
<p>
|
| 405 |
+
{i18next.t('Web搜索价格:${{price}} / 1K 次', {
|
| 406 |
+
price: webSearchPrice,
|
| 407 |
+
})}
|
| 408 |
+
</p>
|
| 409 |
+
)}
|
| 410 |
+
{fileSearch && fileSearchCallCount > 0 && (
|
| 411 |
+
<p>
|
| 412 |
+
{i18next.t('文件搜索价格:${{price}} / 1K 次', {
|
| 413 |
+
price: fileSearchPrice,
|
| 414 |
+
})}
|
| 415 |
+
</p>
|
| 416 |
+
)}
|
| 417 |
<p></p>
|
| 418 |
<p>
|
| 419 |
+
{cacheTokens > 0 && !image && !webSearch && !fileSearch
|
| 420 |
? i18next.t(
|
| 421 |
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
| 422 |
{
|
|
|
|
| 430 |
total: price.toFixed(6),
|
| 431 |
},
|
| 432 |
)
|
| 433 |
+
: image && imageOutputTokens > 0 && !webSearch && !fileSearch
|
| 434 |
+
? i18next.t(
|
| 435 |
+
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
| 436 |
+
{
|
| 437 |
+
nonImageInput: inputTokens - imageOutputTokens,
|
| 438 |
+
imageInput: imageOutputTokens,
|
| 439 |
+
imageRatio: imageRatio,
|
| 440 |
+
price: inputRatioPrice,
|
| 441 |
+
completion: completionTokens,
|
| 442 |
+
compPrice: completionRatioPrice,
|
| 443 |
+
ratio: groupRatio,
|
| 444 |
+
total: price.toFixed(6),
|
| 445 |
+
},
|
| 446 |
+
)
|
| 447 |
+
: webSearch && webSearchCallCount > 0 && !image && !fileSearch
|
| 448 |
+
? i18next.t(
|
| 449 |
+
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
|
| 450 |
+
{
|
| 451 |
+
input: inputTokens,
|
| 452 |
+
price: inputRatioPrice,
|
| 453 |
+
completion: completionTokens,
|
| 454 |
+
compPrice: completionRatioPrice,
|
| 455 |
+
ratio: groupRatio,
|
| 456 |
+
webSearchCallCount,
|
| 457 |
+
webSearchPrice,
|
| 458 |
+
total: price.toFixed(6),
|
| 459 |
+
},
|
| 460 |
+
)
|
| 461 |
+
: fileSearch &&
|
| 462 |
+
fileSearchCallCount > 0 &&
|
| 463 |
+
!image &&
|
| 464 |
+
!webSearch
|
| 465 |
+
? i18next.t(
|
| 466 |
+
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
| 467 |
+
{
|
| 468 |
+
input: inputTokens,
|
| 469 |
+
price: inputRatioPrice,
|
| 470 |
+
completion: completionTokens,
|
| 471 |
+
compPrice: completionRatioPrice,
|
| 472 |
+
ratio: groupRatio,
|
| 473 |
+
fileSearchCallCount,
|
| 474 |
+
fileSearchPrice,
|
| 475 |
+
total: price.toFixed(6),
|
| 476 |
+
},
|
| 477 |
+
)
|
| 478 |
+
: webSearch &&
|
| 479 |
+
webSearchCallCount > 0 &&
|
| 480 |
+
fileSearch &&
|
| 481 |
+
fileSearchCallCount > 0 &&
|
| 482 |
+
!image
|
| 483 |
+
? i18next.t(
|
| 484 |
+
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
| 485 |
+
{
|
| 486 |
+
input: inputTokens,
|
| 487 |
+
price: inputRatioPrice,
|
| 488 |
+
completion: completionTokens,
|
| 489 |
+
compPrice: completionRatioPrice,
|
| 490 |
+
ratio: groupRatio,
|
| 491 |
+
webSearchCallCount,
|
| 492 |
+
webSearchPrice,
|
| 493 |
+
fileSearchCallCount,
|
| 494 |
+
fileSearchPrice,
|
| 495 |
+
total: price.toFixed(6),
|
| 496 |
+
},
|
| 497 |
+
)
|
| 498 |
+
: i18next.t(
|
| 499 |
+
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
| 500 |
+
{
|
| 501 |
+
input: inputTokens,
|
| 502 |
+
price: inputRatioPrice,
|
| 503 |
+
completion: completionTokens,
|
| 504 |
+
compPrice: completionRatioPrice,
|
| 505 |
+
ratio: groupRatio,
|
| 506 |
+
total: price.toFixed(6),
|
| 507 |
+
},
|
| 508 |
+
)}
|
| 509 |
</p>
|
| 510 |
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
| 511 |
</article>
|
|
|
|
| 522 |
user_group_ratio,
|
| 523 |
image = false,
|
| 524 |
imageRatio = 1.0,
|
| 525 |
+
useUserGroupRatio = undefined,
|
| 526 |
+
webSearch = false,
|
| 527 |
+
webSearchCallCount = 0,
|
| 528 |
+
fileSearch = false,
|
| 529 |
+
fileSearchCallCount = 0,
|
| 530 |
) {
|
| 531 |
+
const ratioLabel = useUserGroupRatio
|
| 532 |
+
? i18next.t('专属倍率')
|
| 533 |
+
: i18next.t('分组倍率');
|
| 534 |
const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
| 535 |
|
| 536 |
if (modelPrice !== -1) {
|
| 537 |
return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
|
| 538 |
price: modelPrice,
|
| 539 |
ratioType: ratioLabel,
|
| 540 |
+
ratio,
|
| 541 |
});
|
| 542 |
} else {
|
| 543 |
if (image) {
|
| 544 |
+
return i18next.t(
|
| 545 |
+
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
|
| 546 |
+
{
|
| 547 |
+
modelRatio: modelRatio,
|
| 548 |
+
completionRatio: completionRatio,
|
| 549 |
+
imageRatio: imageRatio,
|
| 550 |
+
ratioType: ratioLabel,
|
| 551 |
+
ratio,
|
| 552 |
+
},
|
| 553 |
+
);
|
| 554 |
+
} else if (webSearch) {
|
| 555 |
+
return i18next.t(
|
| 556 |
+
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
|
| 557 |
+
{
|
| 558 |
+
modelRatio: modelRatio,
|
| 559 |
+
completionRatio: completionRatio,
|
| 560 |
+
ratioType: ratioLabel,
|
| 561 |
+
ratio,
|
| 562 |
+
webSearchCallCount,
|
| 563 |
+
},
|
| 564 |
+
);
|
| 565 |
} else {
|
| 566 |
+
return i18next.t(
|
| 567 |
+
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
|
| 568 |
+
{
|
| 569 |
+
modelRatio: modelRatio,
|
| 570 |
+
completionRatio: completionRatio,
|
| 571 |
+
ratioType: ratioLabel,
|
| 572 |
+
ratio,
|
| 573 |
+
},
|
| 574 |
+
);
|
| 575 |
}
|
| 576 |
}
|
| 577 |
}
|
web/src/i18n/locales/en.json
CHANGED
|
@@ -493,6 +493,7 @@
|
|
| 493 |
"默认": "default",
|
| 494 |
"图片演示": "Image demo",
|
| 495 |
"注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
|
|
|
|
| 496 |
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
| 497 |
"取消无限额度": "Cancel unlimited quota",
|
| 498 |
"取消": "Cancel",
|
|
@@ -1085,7 +1086,7 @@
|
|
| 1085 |
"没有账户?": "No account? ",
|
| 1086 |
"请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com",
|
| 1087 |
"默认 API 版本": "Default API Version",
|
| 1088 |
-
"请输入默认 API 版本,例如:
|
| 1089 |
"请为渠道命名": "Please name the channel",
|
| 1090 |
"请选择可以使用该渠道的分组": "Please select groups that can use this channel",
|
| 1091 |
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:",
|
|
@@ -1373,4 +1374,4 @@
|
|
| 1373 |
"适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
|
| 1374 |
"可在初始化后修改": "Can be modified after initialization",
|
| 1375 |
"初始化系统": "Initialize system"
|
| 1376 |
-
}
|
|
|
|
| 493 |
"默认": "default",
|
| 494 |
"图片演示": "Image demo",
|
| 495 |
"注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
|
| 496 |
+
"2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment",
|
| 497 |
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
| 498 |
"取消无限额度": "Cancel unlimited quota",
|
| 499 |
"取消": "Cancel",
|
|
|
|
| 1086 |
"没有账户?": "No account? ",
|
| 1087 |
"请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com",
|
| 1088 |
"默认 API 版本": "Default API Version",
|
| 1089 |
+
"请输入默认 API 版本,例如:2025-04-01-preview": "Please enter default API version, e.g.: 2025-04-01-preview.",
|
| 1090 |
"请为渠道命名": "Please name the channel",
|
| 1091 |
"请选择可以使用该渠道的分组": "Please select groups that can use this channel",
|
| 1092 |
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:",
|
|
|
|
| 1374 |
"适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
|
| 1375 |
"可在初始化后修改": "Can be modified after initialization",
|
| 1376 |
"初始化系统": "Initialize system"
|
| 1377 |
+
}
|
web/src/pages/Channel/EditChannel.js
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
| 24 |
TextArea,
|
| 25 |
Checkbox,
|
| 26 |
Banner,
|
| 27 |
-
Modal,
|
|
|
|
| 28 |
} from '@douyinfe/semi-ui';
|
| 29 |
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
| 30 |
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
|
@@ -306,7 +307,7 @@ const EditChannel = (props) => {
|
|
| 306 |
fetchModels().then();
|
| 307 |
fetchGroups().then();
|
| 308 |
if (isEdit) {
|
| 309 |
-
loadChannel().then(() => {
|
| 310 |
} else {
|
| 311 |
setInputs(originInputs);
|
| 312 |
let localModels = getChannelModels(inputs.type);
|
|
@@ -477,24 +478,26 @@ const EditChannel = (props) => {
|
|
| 477 |
type={'warning'}
|
| 478 |
description={
|
| 479 |
<>
|
| 480 |
-
{t(
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
|
|
|
|
|
|
| 493 |
|
| 494 |
-
|
| 495 |
-
>
|
| 496 |
-
|
| 497 |
-
</Typography.Text>
|
| 498 |
</>
|
| 499 |
}
|
| 500 |
></Banner>
|
|
@@ -522,7 +525,7 @@ const EditChannel = (props) => {
|
|
| 522 |
<Input
|
| 523 |
label={t('默认 API 版本')}
|
| 524 |
name='azure_other'
|
| 525 |
-
placeholder={t('请输入默认 API 版本,例如:
|
| 526 |
onChange={(value) => {
|
| 527 |
handleInputChange('other', value);
|
| 528 |
}}
|
|
@@ -584,25 +587,35 @@ const EditChannel = (props) => {
|
|
| 584 |
value={inputs.name}
|
| 585 |
autoComplete='new-password'
|
| 586 |
/>
|
| 587 |
-
{inputs.type !== 3 &&
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
<
|
| 593 |
-
<
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
<div style={{ marginTop: 10 }}>
|
| 607 |
<Typography.Text strong>{t('密钥')}:</Typography.Text>
|
| 608 |
</div>
|
|
@@ -761,10 +774,10 @@ const EditChannel = (props) => {
|
|
| 761 |
name='other'
|
| 762 |
placeholder={t(
|
| 763 |
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
)}
|
| 769 |
autosize={{ minRows: 2 }}
|
| 770 |
onChange={(value) => {
|
|
|
|
| 24 |
TextArea,
|
| 25 |
Checkbox,
|
| 26 |
Banner,
|
| 27 |
+
Modal,
|
| 28 |
+
ImagePreview,
|
| 29 |
} from '@douyinfe/semi-ui';
|
| 30 |
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
| 31 |
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
|
|
|
| 307 |
fetchModels().then();
|
| 308 |
fetchGroups().then();
|
| 309 |
if (isEdit) {
|
| 310 |
+
loadChannel().then(() => {});
|
| 311 |
} else {
|
| 312 |
setInputs(originInputs);
|
| 313 |
let localModels = getChannelModels(inputs.type);
|
|
|
|
| 478 |
type={'warning'}
|
| 479 |
description={
|
| 480 |
<>
|
| 481 |
+
{t(
|
| 482 |
+
'2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."',
|
| 483 |
+
)}
|
| 484 |
+
{/*<br />*/}
|
| 485 |
+
{/*<Typography.Text*/}
|
| 486 |
+
{/* style={{*/}
|
| 487 |
+
{/* color: 'rgba(var(--semi-blue-5), 1)',*/}
|
| 488 |
+
{/* userSelect: 'none',*/}
|
| 489 |
+
{/* cursor: 'pointer',*/}
|
| 490 |
+
{/* }}*/}
|
| 491 |
+
{/* onClick={() => {*/}
|
| 492 |
+
{/* setModalImageUrl(*/}
|
| 493 |
+
{/* '/azure_model_name.png',*/}
|
| 494 |
+
{/* );*/}
|
| 495 |
+
{/* setIsModalOpenurl(true)*/}
|
| 496 |
|
| 497 |
+
{/* }}*/}
|
| 498 |
+
{/*>*/}
|
| 499 |
+
{/* {t('查看示例')}*/}
|
| 500 |
+
{/*</Typography.Text>*/}
|
| 501 |
</>
|
| 502 |
}
|
| 503 |
></Banner>
|
|
|
|
| 525 |
<Input
|
| 526 |
label={t('默认 API 版本')}
|
| 527 |
name='azure_other'
|
| 528 |
+
placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')}
|
| 529 |
onChange={(value) => {
|
| 530 |
handleInputChange('other', value);
|
| 531 |
}}
|
|
|
|
| 587 |
value={inputs.name}
|
| 588 |
autoComplete='new-password'
|
| 589 |
/>
|
| 590 |
+
{inputs.type !== 3 &&
|
| 591 |
+
inputs.type !== 8 &&
|
| 592 |
+
inputs.type !== 22 &&
|
| 593 |
+
inputs.type !== 36 &&
|
| 594 |
+
inputs.type !== 45 && (
|
| 595 |
+
<>
|
| 596 |
+
<div style={{ marginTop: 10 }}>
|
| 597 |
+
<Typography.Text strong>{t('API地址')}:</Typography.Text>
|
| 598 |
+
</div>
|
| 599 |
+
<Tooltip
|
| 600 |
+
content={t(
|
| 601 |
+
'对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写',
|
| 602 |
+
)}
|
| 603 |
+
>
|
| 604 |
+
<Input
|
| 605 |
+
label={t('API地址')}
|
| 606 |
+
name='base_url'
|
| 607 |
+
placeholder={t(
|
| 608 |
+
'此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/',
|
| 609 |
+
)}
|
| 610 |
+
onChange={(value) => {
|
| 611 |
+
handleInputChange('base_url', value);
|
| 612 |
+
}}
|
| 613 |
+
value={inputs.base_url}
|
| 614 |
+
autoComplete='new-password'
|
| 615 |
+
/>
|
| 616 |
+
</Tooltip>
|
| 617 |
+
</>
|
| 618 |
+
)}
|
| 619 |
<div style={{ marginTop: 10 }}>
|
| 620 |
<Typography.Text strong>{t('密钥')}:</Typography.Text>
|
| 621 |
</div>
|
|
|
|
| 774 |
name='other'
|
| 775 |
placeholder={t(
|
| 776 |
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
| 777 |
+
'{\n' +
|
| 778 |
+
' "default": "us-central1",\n' +
|
| 779 |
+
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
|
| 780 |
+
'}',
|
| 781 |
)}
|
| 782 |
autosize={{ minRows: 2 }}
|
| 783 |
onChange={(value) => {
|
web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
| 6 |
showError,
|
| 7 |
showSuccess,
|
| 8 |
showWarning,
|
|
|
|
| 9 |
} from '../../../helpers';
|
| 10 |
import { useTranslation } from 'react-i18next';
|
| 11 |
|
|
@@ -18,6 +19,7 @@ export default function RequestRateLimit(props) {
|
|
| 18 |
ModelRequestRateLimitCount: -1,
|
| 19 |
ModelRequestRateLimitSuccessCount: 1000,
|
| 20 |
ModelRequestRateLimitDurationMinutes: 1,
|
|
|
|
| 21 |
});
|
| 22 |
const refForm = useRef();
|
| 23 |
const [inputsRow, setInputsRow] = useState(inputs);
|
|
@@ -46,6 +48,13 @@ export default function RequestRateLimit(props) {
|
|
| 46 |
if (res.includes(undefined))
|
| 47 |
return showError(t('部分保存失败,请重试'));
|
| 48 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
showSuccess(t('保存成功'));
|
| 50 |
props.refresh();
|
| 51 |
})
|
|
@@ -147,6 +156,41 @@ export default function RequestRateLimit(props) {
|
|
| 147 |
/>
|
| 148 |
</Col>
|
| 149 |
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
<Row>
|
| 151 |
<Button size='default' onClick={onSubmit}>
|
| 152 |
{t('保存模型速率限制')}
|
|
|
|
| 6 |
showError,
|
| 7 |
showSuccess,
|
| 8 |
showWarning,
|
| 9 |
+
verifyJSON,
|
| 10 |
} from '../../../helpers';
|
| 11 |
import { useTranslation } from 'react-i18next';
|
| 12 |
|
|
|
|
| 19 |
ModelRequestRateLimitCount: -1,
|
| 20 |
ModelRequestRateLimitSuccessCount: 1000,
|
| 21 |
ModelRequestRateLimitDurationMinutes: 1,
|
| 22 |
+
ModelRequestRateLimitGroup: '',
|
| 23 |
});
|
| 24 |
const refForm = useRef();
|
| 25 |
const [inputsRow, setInputsRow] = useState(inputs);
|
|
|
|
| 48 |
if (res.includes(undefined))
|
| 49 |
return showError(t('部分保存失败,请重试'));
|
| 50 |
}
|
| 51 |
+
|
| 52 |
+
for (let i = 0; i < res.length; i++) {
|
| 53 |
+
if (!res[i].data.success) {
|
| 54 |
+
return showError(res[i].data.message);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
showSuccess(t('保存成功'));
|
| 59 |
props.refresh();
|
| 60 |
})
|
|
|
|
| 156 |
/>
|
| 157 |
</Col>
|
| 158 |
</Row>
|
| 159 |
+
<Row>
|
| 160 |
+
<Col xs={24} sm={16}>
|
| 161 |
+
<Form.TextArea
|
| 162 |
+
label={t('分组速率限制')}
|
| 163 |
+
placeholder={t(
|
| 164 |
+
'{\n "default": [200, 100],\n "vip": [0, 1000]\n}',
|
| 165 |
+
)}
|
| 166 |
+
field={'ModelRequestRateLimitGroup'}
|
| 167 |
+
autosize={{ minRows: 5, maxRows: 15 }}
|
| 168 |
+
trigger='blur'
|
| 169 |
+
stopValidateWithError
|
| 170 |
+
rules={[
|
| 171 |
+
{
|
| 172 |
+
validator: (rule, value) => verifyJSON(value),
|
| 173 |
+
message: t('不是合法的 JSON 字符串'),
|
| 174 |
+
},
|
| 175 |
+
]}
|
| 176 |
+
extraText={
|
| 177 |
+
<div>
|
| 178 |
+
<p style={{ marginBottom: -15 }}>{t('说明:')}</p>
|
| 179 |
+
<ul>
|
| 180 |
+
<li>{t('使用 JSON 对象格式,格式为:{"组名": [最多请求次数, 最多请求完成次数]}')}</li>
|
| 181 |
+
<li>{t('示例:{"default": [200, 100], "vip": [0, 1000]}。')}</li>
|
| 182 |
+
<li>{t('[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。')}</li>
|
| 183 |
+
<li>{t('分组速率配置优先级高于全局速率限制。')}</li>
|
| 184 |
+
<li>{t('限制周期统一使用上方配置的“限制周期”值。')}</li>
|
| 185 |
+
</ul>
|
| 186 |
+
</div>
|
| 187 |
+
}
|
| 188 |
+
onChange={(value) => {
|
| 189 |
+
setInputs({ ...inputs, ModelRequestRateLimitGroup: value });
|
| 190 |
+
}}
|
| 191 |
+
/>
|
| 192 |
+
</Col>
|
| 193 |
+
</Row>
|
| 194 |
<Row>
|
| 195 |
<Button size='default' onClick={onSubmit}>
|
| 196 |
{t('保存模型速率限制')}
|