luowuyin commited on
Commit
a1cdbbc
·
1 Parent(s): 090668e

25:05:10 03:17:05 v0.7.1

Browse files
Files changed (46) hide show
  1. README.en.md +115 -128
  2. VERSION +1 -1
  3. constant/azure.go +5 -0
  4. constant/env.go +1 -1
  5. controller/channel-billing.go +25 -0
  6. controller/option.go +9 -0
  7. controller/user.go +25 -1
  8. BT.md → docs/installation/BT.md +3 -3
  9. Midjourney.md → docs/models/Midjourney.md +0 -0
  10. Rerank.md → docs/models/Rerank.md +0 -0
  11. Suno.md → docs/models/Suno.md +0 -0
  12. dto/dalle.go +10 -11
  13. dto/openai_response.go +30 -25
  14. middleware/distributor.go +1 -0
  15. middleware/model-rate-limit.go +35 -19
  16. model/option.go +6 -0
  17. model/user.go +1 -0
  18. relay/channel/api_request.go +67 -2
  19. relay/channel/gemini/relay-gemini.go +1 -0
  20. relay/channel/openai/adaptor.go +7 -8
  21. relay/channel/openai/helper.go +7 -0
  22. relay/channel/openai/relay-openai.go +26 -112
  23. relay/channel/openai/relay_responses.go +119 -0
  24. relay/channel/vertex/adaptor.go +1 -1
  25. relay/channel/xai/adaptor.go +20 -12
  26. relay/channel/xai/dto.go +13 -0
  27. relay/common/relay_info.go +49 -8
  28. relay/helper/common.go +15 -7
  29. relay/helper/model_mapped.go +33 -4
  30. relay/helper/price.go +1 -1
  31. relay/helper/stream_scanner.go +2 -1
  32. relay/relay-image.go +2 -2
  33. relay/relay-responses.go +5 -5
  34. relay/relay-text.go +47 -0
  35. service/cf_worker.go +1 -1
  36. setting/operation_setting/tools.go +57 -0
  37. setting/rate_limit.go +58 -0
  38. setting/system_setting.go +1 -0
  39. web/src/components/LogsTable.js +13 -1
  40. web/src/components/PersonalSetting.js +46 -9
  41. web/src/components/RateLimitSetting.js +9 -4
  42. web/src/components/SystemSetting.js +23 -4
  43. web/src/helpers/render.js +142 -45
  44. web/src/i18n/locales/en.json +3 -2
  45. web/src/pages/Channel/EditChannel.js +56 -43
  46. web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js +44 -0
README.en.md CHANGED
@@ -1,10 +1,13 @@
 
 
 
1
  <div align="center">
2
 
3
  ![new-api](/web/public/logo.png)
4
 
5
  # New API
6
 
7
- 🍥 Next Generation LLM Gateway and AI Asset Management System
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
- > - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and relevant laws and regulations. Not to be used for illegal purposes.
37
- > - This project is for personal learning only. Stability is not guaranteed, and no technical support is provided.
 
 
 
 
 
38
 
39
  ## ✨ Key Features
40
 
41
- 1. 🎨 New UI interface (some interfaces pending update)
42
- 2. 🌍 Multi-language support (work in progress)
43
- 3. 🎨 Added [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface support, [Integration Guide](Midjourney.md)
44
- 4. 💰 Online recharge support, configurable in system settings:
45
- - [x] EasyPay
46
- 5. 🔍 Query usage quota by key:
47
- - Works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)
48
- 6. 📑 Configurable items per page in pagination
49
- 7. 🔄 Compatible with original One API database (one-api.db)
50
- 8. 💵 Support per-request model pricing, configurable in System Settings - Operation Settings
51
- 9. ⚖️ Support channel **weighted random** selection
52
- 10. 📈 Data dashboard (console)
53
- 11. 🔒 Configurable model access per token
54
- 12. 🤖 Telegram authorization login support:
55
- 1. System Settings - Configure Login Registration - Allow Telegram Login
56
- 2. Send /setdomain command to [@Botfather](https://t.me/botfather)
57
- 3. Select your bot, then enter http(s)://your-website/login
58
- 4. Telegram Bot name is the bot username without @
59
- 13. 🎵 Added [Suno API](https://github.com/Suno-API/Suno-API) interface support, [Integration Guide](Suno.md)
60
- 14. 🔄 Support for Rerank models, compatible with Cohere and Jina, can integrate with Dify, [Integration Guide](Rerank.md)
61
- 15. **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - Support for OpenAI's Realtime API, including Azure channels
62
- 16. 🧠 Support for setting reasoning effort through model name suffix:
63
- - Add suffix `-high` to set high reasoning effort (e.g., `o3-mini-high`)
64
- - Add suffix `-medium` to set medium reasoning effort
65
- - Add suffix `-low` to set low reasoning effort
66
- 17. 🔄 Thinking to content option `thinking_to_content` in `Channel->Edit->Channel Extra Settings`, default is `false`, when `true`, the `reasoning_content` of the thinking content will be converted to `<think>` tags and concatenated to the content returned.
67
- 18. 🔄 Model rate limit, support setting total request limit and successful request limit in `System Settings->Rate Limit Settings`
68
- 19. 💰 Cache billing support, when enabled can charge a configurable ratio for cache hits:
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
- - [ ] Claude
76
 
77
  ## Model Support
78
- This version additionally supports:
79
- 1. Third-party model **gpts** (gpt-4-gizmo-*)
80
- 2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [Integration Guide](Midjourney.md)
81
- 3. Custom channels with full API URL support
82
- 4. [Suno API](https://github.com/Suno-API/Suno-API) interface, [Integration Guide](Suno.md)
83
- 5. Rerank models, supporting [Cohere](https://cohere.ai/) and [Jina](https://jina.ai/), [Integration Guide](Rerank.md)
84
- 6. Dify
85
-
86
- You can add custom models gpt-4-gizmo-* in channels. These are third-party models and cannot be called with official OpenAI keys.
87
-
88
- ## Additional Configurations Beyond One API
89
- - `GENERATE_DEFAULT_TOKEN`: Generate initial token for new users, default `false`
90
- - `STREAMING_TIMEOUT`: Set streaming response timeout, default 60 seconds
91
- - `DIFY_DEBUG`: Output workflow and node info to client for Dify channel, default `true`
92
- - `FORCE_STREAM_OPTION`: Override client stream_options parameter, default `true`
93
- - `GET_MEDIA_TOKEN`: Calculate image tokens, default `true`
94
- - `GET_MEDIA_TOKEN_NOT_STREAM`: Calculate image tokens in non-stream mode, default `true`
95
- - `UPDATE_TASK`: Update async tasks (Midjourney, Suno), default `true`
96
- - `GEMINI_MODEL_MAP`: Specify Gemini model versions (v1/v1beta), format: "model:version", comma-separated
97
- - `COHERE_SAFETY_SETTING`: Cohere model [safety settings](https://docs.cohere.com/docs/safety-modes#overview), options: `NONE`, `CONTEXTUAL`, `STRICT`, default `NONE`
98
- - `GEMINI_VISION_MAX_IMAGE_NUM`: Gemini model maximum image number, default `16`, set to `-1` to disable
99
- - `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default `20`
100
- - `CRYPTO_SECRET`: Encryption key for encrypting database content
101
- - `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, if not specified in channel settings, use this version, default `2024-12-01-preview`
102
- - `NOTIFICATION_LIMIT_DURATION_MINUTE`: Duration of notification limit in minutes, default `10`
103
- - `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications in the specified duration, default `2`
 
 
 
104
 
105
  ## Deployment
106
 
 
 
107
  > [!TIP]
108
- > Latest Docker image: `calciumion/new-api:latest`
109
- > Default account: root, password: 123456
110
 
111
- ### Multi-Server Deployment
112
- - Must set `SESSION_SECRET` environment variable, otherwise login state will not be consistent across multiple servers.
113
- - If using a public Redis, must set `CRYPTO_SECRET` environment variable, otherwise Redis content will not be able to be obtained in multi-server deployment.
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 with BT Panel
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 Deployment
 
 
126
 
127
- ### Using Docker Compose (Recommended)
128
  ```shell
129
- # Clone project
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
- #### Update Version
140
  ```shell
141
- docker-compose pull
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
- # MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed
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
- #### Update Version
156
- ```shell
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
- Alternatively, you can use Watchtower for automatic updates (not recommended, may cause database incompatibility):
167
- ```shell
168
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
169
- ```
170
 
171
- ## Channel Retry
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
- ## Integration Guides
192
- - [Midjourney Integration](Midjourney.md)
193
- - [Suno Integration](Suno.md)
 
 
 
 
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-gen AI B/C solution
199
- - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota by key
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  ## 🌟 Star History
202
 
203
- [![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](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
  ![new-api](/web/public/logo.png)
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
+ [![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)
VERSION CHANGED
@@ -1 +1 @@
1
- v0.7.0-alpha.2
 
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", "2024-12-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)
 
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
- ![8285bba413e770fe9620f1bf9b40d44e](https://github.com/user-attachments/assets/7a6fc03e-c457-45e4-b8f9-184508fc26b0)
 
1
+ 密钥为环境变量SESSION_SECRET
2
+
3
+ ![8285bba413e770fe9620f1bf9b40d44e](https://github.com/user-attachments/assets/7a6fc03e-c457-45e4-b8f9-184508fc26b0)
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 `json:"model"`
7
- Prompt string `json:"prompt" binding:"required"`
8
- N int `json:"n,omitempty"`
9
- Size string `json:"size,omitempty"`
10
- Quality string `json:"quality,omitempty"`
11
- ResponseFormat string `json:"response_format,omitempty"`
12
- Style string `json:"style,omitempty"`
13
- User string `json:"user,omitempty"`
14
- ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
 
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 `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 []interface{} `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,8 +238,12 @@ type ResponsesOutputContent struct {
238
  }
239
 
240
  const (
241
- BuildInTools_WebSearch = "web_search_preview"
242
- BuildInTools_FileSearch = "file_search"
 
 
 
 
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
- totalKey := fmt.Sprintf("rateLimit:%s", userId)
97
- // 初始化
98
- tb := limiter.New(ctx, rdb)
99
- allowed, err = tb.Allow(
100
- ctx,
101
- totalKey,
102
- limiter.WithCapacity(int64(totalMaxCount)*duration),
103
- limiter.WithRate(int64(totalMaxCount)),
104
- limiter.WithRequested(duration),
105
- )
106
-
107
- if err != nil {
108
- fmt.Println("检查总请求数限制失败:", err.Error())
109
- abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed")
110
- return
111
- }
112
-
113
- if !allowed {
114
- abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount))
 
 
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
- model_ = strings.Replace(model_, ".", "", -1)
 
 
 
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 = OpenaiResponsesHandler(c, resp, info)
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
- break
 
 
 
 
 
 
 
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
- request.Size = ""
32
- return request, nil
 
 
 
 
 
33
  }
34
 
35
  func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
36
  }
37
 
38
  func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
39
- return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
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
- if info.IsStream {
93
- err, usage = xAIStreamHandler(c, resp, info)
94
- } else {
95
- err, usage = xAIHandler(c, resp, info)
 
 
 
 
 
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: false,
174
- ApiType: apiType,
175
- ApiVersion: c.GetString("api_version"),
176
- ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
177
- Organization: c.GetString("channel_organization"),
178
- ChannelSetting: channelSetting,
179
- ParamOverride: paramOverride,
180
- RelayFormat: RelayFormatOpenAI,
 
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
- c.Writer.Header().Set("Content-Type", "text/event-stream")
16
- c.Writer.Header().Set("Cache-Control", "no-cache")
17
- c.Writer.Header().Set("Connection", "keep-alive")
18
- c.Writer.Header().Set("Transfer-Encoding", "chunked")
19
- c.Writer.Header().Set("X-Accel-Buffering", "no")
 
 
 
 
 
 
 
 
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\n", data)})
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
- if modelMap[info.OriginModelName] != "" {
20
- info.UpstreamModelName = modelMap[info.OriginModelName]
21
- info.IsModelMapped = true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: %d", 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) {
 
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, dall-e-3 1024x1792 or 1792x1024")
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 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024")
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, relayInfo *relaycommon.RelayInfo) (*dto.OpenAIResponsesRequest, error) {
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
- relayInfo := relaycommon.GenRelayInfo(c)
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="line" defaultActiveKey="notification">
820
- <TabPane tab={t('通知设置')} itemKey="notification">
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="price">
997
  <div style={{ marginTop: 20 }}>
998
- <Typography.Text strong>{t('接受未设置价格模型')}</Typography.Text>
 
 
999
  <div style={{ marginTop: 10 }}>
1000
  <Checkbox
1001
- checked={notificationSettings.acceptUnsetModelRatioModel}
1002
- onChange={e => handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)}
 
 
 
 
 
 
 
1003
  >
1004
  {t('接受未设置价格模型')}
1005
  </Checkbox>
1006
- <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
1007
- {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
 
 
 
 
 
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
- if (item.key.endsWith('Enabled')) {
27
- newInputs[item.key] = item.value === 'true' ? true : false;
28
- } else {
29
- newInputs[item.key] = item.value;
 
 
 
 
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 "axios";
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 = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
 
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 theme="solid" type="primary" onClick={handleAddEmail}>添加</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 = inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
 
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
- ? i18next.t(
412
- '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
413
- {
414
- nonImageInput: inputTokens - imageOutputTokens,
415
- imageInput: imageOutputTokens,
416
- imageRatio: imageRatio,
417
- price: inputRatioPrice,
418
- completion: completionTokens,
419
- compPrice: completionRatioPrice,
420
- ratio: groupRatio,
421
- total: price.toFixed(6),
422
- },
423
- )
424
- : i18next.t(
425
- '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
426
- {
427
- input: inputTokens,
428
- price: inputRatioPrice,
429
- completion: completionTokens,
430
- compPrice: completionRatioPrice,
431
- ratio: groupRatio,
432
- total: price.toFixed(6),
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 ? i18next.t('专属倍率') : i18next.t('分组倍率');
 
 
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('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', {
465
- modelRatio: modelRatio,
466
- completionRatio: completionRatio,
467
- imageRatio: imageRatio,
468
- ratioType: ratioLabel,
469
- ratio
470
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  } else {
472
- return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', {
473
- modelRatio: modelRatio,
474
- completionRatio: completionRatio,
475
- ratioType: ratioLabel,
476
- ratio
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 版本,例如:2024-12-01-preview": "Please enter default API version, e.g.: 2024-12-01-preview.",
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, ImagePreview
 
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('注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41')}
481
- <br />
482
- <Typography.Text
483
- style={{
484
- color: 'rgba(var(--semi-blue-5), 1)',
485
- userSelect: 'none',
486
- cursor: 'pointer',
487
- }}
488
- onClick={() => {
489
- setModalImageUrl(
490
- '/azure_model_name.png',
491
- );
492
- setIsModalOpenurl(true)
 
 
493
 
494
- }}
495
- >
496
- {t('查看示例')}
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 版本,例如:2024-12-01-preview')}
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 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
588
- <>
589
- <div style={{ marginTop: 10 }}>
590
- <Typography.Text strong>{t('API地址')}:</Typography.Text>
591
- </div>
592
- <Tooltip content={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}>
593
- <Input
594
- label={t('API地址')}
595
- name="base_url"
596
- placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
597
- onChange={(value) => {
598
- handleInputChange('base_url', value);
599
- }}
600
- value={inputs.base_url}
601
- autoComplete="new-password"
602
- />
603
- </Tooltip>
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
- '{\n' +
765
- ' "default": "us-central1",\n' +
766
- ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
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('保存模型速率限制')}