Compare commits
235 Commits
v0.8.8.5.1
...
v0.9.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3a38d27f5 | ||
|
|
754498a012 | ||
|
|
4226746675 | ||
|
|
94536be9be | ||
|
|
2c6a9245ee | ||
|
|
fc18a3c89e | ||
|
|
4f23e53002 | ||
|
|
005e9659e1 | ||
|
|
43c6bbb3ad | ||
|
|
def4d16c73 | ||
|
|
61ae19ac82 | ||
|
|
08add538a0 | ||
|
|
bd166b2f77 | ||
|
|
8b7384e47f | ||
|
|
60dc032cb8 | ||
|
|
d47190f1fd | ||
|
|
e581422810 | ||
|
|
ad151bb919 | ||
|
|
b5040e0182 | ||
|
|
3133e91d8e | ||
|
|
0837747428 | ||
|
|
518763cd08 | ||
|
|
2b862f65a2 | ||
|
|
cb53adef62 | ||
|
|
c3481f5a67 | ||
|
|
ba50b6fcc0 | ||
|
|
003246f113 | ||
|
|
13aee98d4a | ||
|
|
6c94573323 | ||
|
|
03a257bddb | ||
|
|
e02e1e8d4a | ||
|
|
57f1015197 | ||
|
|
974b93a8be | ||
|
|
652d71d799 | ||
|
|
f6d4c586eb | ||
|
|
adc7fbd424 | ||
|
|
cfc6bc8e5e | ||
|
|
da802ece3b | ||
|
|
1074f8acb1 | ||
|
|
a0e6a72b69 | ||
|
|
795cfd471a | ||
|
|
0a053ee633 | ||
|
|
85f81df2f8 | ||
|
|
94d9607447 | ||
|
|
2be4489d18 | ||
|
|
d34e4f1f28 | ||
|
|
11a81c25ef | ||
|
|
c18414cbe4 | ||
|
|
998305fd00 | ||
|
|
49ab1a3b38 | ||
|
|
c123ea3179 | ||
|
|
a6ad49dba0 | ||
|
|
3749be3e09 | ||
|
|
b67a42e0a8 | ||
|
|
9805d35a5d | ||
|
|
e3473e3c39 | ||
|
|
a1cab158ea | ||
|
|
9934cdc5bd | ||
|
|
c834694992 | ||
|
|
aa1f5c6e4e | ||
|
|
2d28fb3a73 | ||
|
|
206ed55db4 | ||
|
|
9b0913343c | ||
|
|
5696a62c27 | ||
|
|
11a7ac9b10 | ||
|
|
cbce487362 | ||
|
|
f8ca8d7cea | ||
|
|
732e5d2661 | ||
|
|
5d6fac69c4 | ||
|
|
5654d08086 | ||
|
|
73a7b33864 | ||
|
|
64a752a3b4 | ||
|
|
0ad918c21d | ||
|
|
5829bc69ca | ||
|
|
b591b4ebdf | ||
|
|
dd497d5bd8 | ||
|
|
f70cac54d1 | ||
|
|
f6a48434c1 | ||
|
|
c63b6b3ef8 | ||
|
|
28bd31a30b | ||
|
|
491013e27a | ||
|
|
0bb43aa464 | ||
|
|
0edc707657 | ||
|
|
68b7badb80 | ||
|
|
b57e97d2a1 | ||
|
|
eeb421513b | ||
|
|
ef1e380bbc | ||
|
|
2579b3c0ba | ||
|
|
d646a922ee | ||
|
|
8b2afcec90 | ||
|
|
726f1632b0 | ||
|
|
5a2dad2e16 | ||
|
|
039b00d695 | ||
|
|
1cb63063f7 | ||
|
|
5671503c28 | ||
|
|
50dafeaa0b | ||
|
|
1d4850e47a | ||
|
|
cc4f73dc7e | ||
|
|
067be3727e | ||
|
|
edeb4791c9 | ||
|
|
2f25e44e60 | ||
|
|
5fe1ce89ec | ||
|
|
03fc89da00 | ||
|
|
44e9b02b3f | ||
|
|
7f1f368065 | ||
|
|
89caccd4e0 | ||
|
|
6748b006b7 | ||
|
|
baf086d5b3 | ||
|
|
e2037ad756 | ||
|
|
d75e198304 | ||
|
|
223f0d0850 | ||
|
|
01cd279f9f | ||
|
|
196e2a0abb | ||
|
|
63b9457b6c | ||
|
|
0082b87f61 | ||
|
|
936b1f8d09 | ||
|
|
c1a545ac23 | ||
|
|
33925dd313 | ||
|
|
f5abbeb353 | ||
|
|
c13683e982 | ||
|
|
17bab355e4 | ||
|
|
e77effaf8b | ||
|
|
2e39323782 | ||
|
|
8db5356caf | ||
|
|
fa2edd9d3f | ||
|
|
7997a04a68 | ||
|
|
2c30b4cf60 | ||
|
|
38c3349a6a | ||
|
|
41cb01bac9 | ||
|
|
c2ef4c8e54 | ||
|
|
a7cd44e536 | ||
|
|
dc12ec6dfd | ||
|
|
6eec8851eb | ||
|
|
39c966efdd | ||
|
|
981023154b | ||
|
|
14a9a99e2d | ||
|
|
e74c6f5de7 | ||
|
|
d3170310ff | ||
|
|
03cfc05afd | ||
|
|
fa686207ed | ||
|
|
e863be7ec3 | ||
|
|
2d4edb3eca | ||
|
|
ba5333a092 | ||
|
|
53fa7255ec | ||
|
|
dddf772f19 | ||
|
|
9d6d580cbd | ||
|
|
c3696cd857 | ||
|
|
c7498b768c | ||
|
|
da17bdb688 | ||
|
|
c87a741fc9 | ||
|
|
4ad8eefaec | ||
|
|
e64b13c925 | ||
|
|
b97a683bfd | ||
|
|
6ea19b0ae2 | ||
|
|
dd9d2a150d | ||
|
|
195be56c46 | ||
|
|
78662e8194 | ||
|
|
c8f7aa76e7 | ||
|
|
543e7b0b6b | ||
|
|
42d2394585 | ||
|
|
94bd44d0f2 | ||
|
|
92022360de | ||
|
|
7f462a084c | ||
|
|
b77d64bc9f | ||
|
|
d1d945eaa0 | ||
|
|
28fdb8af37 | ||
|
|
dbde044213 | ||
|
|
870132a5cb | ||
|
|
ffa898c52d | ||
|
|
cdf27d60be | ||
|
|
1cc81deb69 | ||
|
|
1d578b73ce | ||
|
|
fdb6a3ce16 | ||
|
|
ca1f3c6e4c | ||
|
|
f942361f7b | ||
|
|
02fd80b703 | ||
|
|
d6b03d4760 | ||
|
|
cb75e25a1a | ||
|
|
9572e16dcb | ||
|
|
459fce196f | ||
|
|
ada434fb20 | ||
|
|
71ba3fa310 | ||
|
|
0727353afa | ||
|
|
fd2ff2a973 | ||
|
|
50f9195f2d | ||
|
|
a47fc5a76b | ||
|
|
72ffe61ad1 | ||
|
|
ea8cac7c10 | ||
|
|
8639699d49 | ||
|
|
f242220132 | ||
|
|
55dbdba636 | ||
|
|
03b670971b | ||
|
|
24860fdc05 | ||
|
|
229dd3a123 | ||
|
|
919eacd907 | ||
|
|
4cec55c9a4 | ||
|
|
aa8ec92976 | ||
|
|
44da9c9a28 | ||
|
|
c776a1edff | ||
|
|
a5cbef1a61 | ||
|
|
ae22ba593a | ||
|
|
4ad4ad7088 | ||
|
|
8bccda5649 | ||
|
|
2a804b6c02 | ||
|
|
3b61617cb1 | ||
|
|
ec28671aed | ||
|
|
c7c7229b8b | ||
|
|
2efc133997 | ||
|
|
df72ac1215 | ||
|
|
2fc0d7b2a7 | ||
|
|
3a9e394814 | ||
|
|
3d9d3da1ae | ||
|
|
8abd764eca | ||
|
|
7a31e481a6 | ||
|
|
b70d2655ed | ||
|
|
15cb2f1a9e | ||
|
|
2471367c92 | ||
|
|
962c40c1a7 | ||
|
|
f6c7828160 | ||
|
|
8b57da9a2b | ||
|
|
daa7a13505 | ||
|
|
cda4790219 | ||
|
|
c6bb1dcc0e | ||
|
|
f8e1b084cd | ||
|
|
7d869c9af1 | ||
|
|
1690b05629 | ||
|
|
563825492e | ||
|
|
eee37017e1 | ||
|
|
cf91cf1b14 | ||
|
|
49abd6aaf3 | ||
|
|
43263a3bc8 | ||
|
|
3feeca627c | ||
|
|
1c1e3386f8 | ||
|
|
a385c8a6f8 | ||
|
|
9b73696a98 |
@@ -47,7 +47,7 @@
|
||||
# 所有请求超时时间,单位秒,默认为0,表示不限制
|
||||
# RELAY_TIMEOUT=0
|
||||
# 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
|
||||
# STREAMING_TIMEOUT=120
|
||||
# STREAMING_TIMEOUT=300
|
||||
|
||||
# Gemini 识别图片 最大图片数量
|
||||
# GEMINI_VISION_MAX_IMAGE_NUM=16
|
||||
|
||||
42
README.en.md
@@ -40,6 +40,28 @@
|
||||
> - 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.
|
||||
> - 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.
|
||||
|
||||
<h2>🤝 Trusted Partners</h2>
|
||||
<p id="premium-sponsors"> </p>
|
||||
<p align="center"><strong>No particular order</strong></p>
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target=_blank><img
|
||||
src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
|
||||
/></a>
|
||||
<a href="https://bda.pku.edu.cn/" target=_blank><img
|
||||
src="./docs/images/pku.png" alt="Peking University" height="120"
|
||||
/></a>
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
|
||||
src="./docs/images/ucloud.png" alt="UCloud" height="120"
|
||||
/></a>
|
||||
<a href="https://www.aliyun.com/" target=_blank><img
|
||||
src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
|
||||
/></a>
|
||||
<a href="https://io.net/" target=_blank><img
|
||||
src="./docs/images/io-net.png" alt="IO.NET" height="120"
|
||||
/></a>
|
||||
</p>
|
||||
<p> </p>
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For detailed documentation, please visit our official Wiki: [https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||
@@ -100,7 +122,7 @@ This version supports multiple models, please refer to [API Documentation-Relay
|
||||
For detailed configuration instructions, please refer to [Installation Guide-Environment Variables Configuration](https://docs.newapi.pro/installation/environment-variables):
|
||||
|
||||
- `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
|
||||
- `STREAMING_TIMEOUT`: Streaming response timeout, default is 120 seconds
|
||||
- `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds
|
||||
- `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true`
|
||||
- `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true`
|
||||
- `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true`
|
||||
@@ -189,24 +211,6 @@ If you have any questions, please refer to [Help and Support](https://docs.newap
|
||||
- [Issue Feedback](https://docs.newapi.pro/support/feedback-issues)
|
||||
- [FAQ](https://docs.newapi.pro/support/faq)
|
||||
|
||||
## 🤝 Trusted Partners
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target="_blank"><img
|
||||
src="./docs/images/cherry-studio.svg" alt="Cherry Studio" height="58"
|
||||
/></a>
|
||||
|
||||
<a href="https://bda.pku.edu.cn/" target="_blank"><img
|
||||
src="./docs/images/pku.png" alt="Peking University" height="58"
|
||||
/></a>
|
||||
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target="_blank"><img
|
||||
src="./docs/images/ucloud.svg" alt="UCloud" height="58"
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><em>No particular order</em></p>
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||
|
||||
42
README.md
@@ -40,6 +40,28 @@
|
||||
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||
> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
<h2>🤝 我们信任的合作伙伴</h2>
|
||||
<p id="premium-sponsors"> </p>
|
||||
<p align="center"><strong>排名不分先后</strong></p>
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target=_blank><img
|
||||
src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
|
||||
/></a>
|
||||
<a href="https://bda.pku.edu.cn/" target=_blank><img
|
||||
src="./docs/images/pku.png" alt="北京大学" height="120"
|
||||
/></a>
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
|
||||
src="./docs/images/ucloud.png" alt="UCloud 优刻得" height="120"
|
||||
/></a>
|
||||
<a href="https://www.aliyun.com/" target=_blank><img
|
||||
src="./docs/images/aliyun.png" alt="阿里云" height="120"
|
||||
/></a>
|
||||
<a href="https://io.net/" target=_blank><img
|
||||
src="./docs/images/io-net.png" alt="IO.NET" height="120"
|
||||
/></a>
|
||||
</p>
|
||||
<p> </p>
|
||||
|
||||
## 📚 文档
|
||||
|
||||
详细文档请访问我们的官方Wiki:[https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||
@@ -100,7 +122,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
详细配置说明请参考[安装指南-环境变量配置](https://docs.newapi.pro/installation/environment-variables):
|
||||
|
||||
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
|
||||
- `STREAMING_TIMEOUT`:流式回复超时时间,默认120秒
|
||||
- `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒
|
||||
- `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
|
||||
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
|
||||
- `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
|
||||
@@ -188,24 +210,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
- [反馈问题](https://docs.newapi.pro/support/feedback-issues)
|
||||
- [常见问题](https://docs.newapi.pro/support/faq)
|
||||
|
||||
## 🤝 我们信任的合作伙伴
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target="_blank"><img
|
||||
src="./docs/images/cherry-studio.svg" alt="Cherry Studio" height="58"
|
||||
/></a>
|
||||
|
||||
<a href="https://bda.pku.edu.cn/" target="_blank"><img
|
||||
src="./docs/images/pku.png" alt="北京大学" height="58"
|
||||
/></a>
|
||||
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target="_blank"><img
|
||||
src="./docs/images/ucloud.svg" alt="UCloud 优刻得" height="58"
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><em>排名不分先后</em></p>
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||
|
||||
@@ -65,6 +65,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
|
||||
apiType = constant.APITypeCoze
|
||||
case constant.ChannelTypeJimeng:
|
||||
apiType = constant.APITypeJimeng
|
||||
case constant.ChannelTypeMoonshot:
|
||||
apiType = constant.APITypeMoonshot
|
||||
}
|
||||
if apiType == -1 {
|
||||
return constant.APITypeOpenAI, false
|
||||
|
||||
@@ -83,6 +83,7 @@ var GitHubClientId = ""
|
||||
var GitHubClientSecret = ""
|
||||
var LinuxDOClientId = ""
|
||||
var LinuxDOClientSecret = ""
|
||||
var LinuxDOMinimumTrustLevel = 0
|
||||
|
||||
var WeChatServerAddress = ""
|
||||
var WeChatServerToken = ""
|
||||
|
||||
21
common/copy.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antlabs/pcopy"
|
||||
)
|
||||
|
||||
func DeepCopy[T any](src *T) (*T, error) {
|
||||
if src == nil {
|
||||
return nil, fmt.Errorf("copy source cannot be nil")
|
||||
}
|
||||
var dst T
|
||||
err := pcopy.Copy(&dst, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if &dst == nil {
|
||||
return nil, fmt.Errorf("copy result cannot be nil")
|
||||
}
|
||||
return &dst, nil
|
||||
}
|
||||
@@ -12,4 +12,4 @@ var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
|
||||
var UsingMySQL = false
|
||||
var UsingClickHouse = false
|
||||
|
||||
var SQLitePath = "one-api.db?_busy_timeout=5000"
|
||||
var SQLitePath = "one-api.db?_busy_timeout=30000"
|
||||
|
||||
@@ -11,22 +11,22 @@ import "one-api/constant"
|
||||
// 例如:{"path":"/v1/chat/completions","method":"POST"}
|
||||
|
||||
type EndpointInfo struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
// defaultEndpointInfoMap 保存内置端点的默认 Path 与 Method
|
||||
var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
|
||||
constant.EndpointTypeOpenAI: {Path: "/v1/chat/completions", Method: "POST"},
|
||||
constant.EndpointTypeOpenAIResponse: {Path: "/v1/responses", Method: "POST"},
|
||||
constant.EndpointTypeAnthropic: {Path: "/v1/messages", Method: "POST"},
|
||||
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
||||
constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"},
|
||||
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
|
||||
constant.EndpointTypeOpenAI: {Path: "/v1/chat/completions", Method: "POST"},
|
||||
constant.EndpointTypeOpenAIResponse: {Path: "/v1/responses", Method: "POST"},
|
||||
constant.EndpointTypeAnthropic: {Path: "/v1/messages", Method: "POST"},
|
||||
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
||||
constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"},
|
||||
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
|
||||
}
|
||||
|
||||
// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
|
||||
func GetDefaultEndpointInfo(et constant.EndpointType) (EndpointInfo, bool) {
|
||||
info, ok := defaultEndpointInfoMap[et]
|
||||
return info, ok
|
||||
info, ok := defaultEndpointInfoMap[et]
|
||||
return info, ok
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/constant"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const KeyRequestBody = "key_request_body"
|
||||
@@ -31,6 +32,9 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//if DebugEnabled {
|
||||
// println("UnmarshalBodyReusable request body:", string(requestBody))
|
||||
//}
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
err = Unmarshal(requestBody, &v)
|
||||
|
||||
@@ -101,7 +101,7 @@ func InitEnv() {
|
||||
}
|
||||
|
||||
func initConstantEnv() {
|
||||
constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 120)
|
||||
constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 300)
|
||||
constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
||||
constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20)
|
||||
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
||||
|
||||
5
common/quota.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package common
|
||||
|
||||
func GetTrustQuota() int {
|
||||
return int(10 * QuotaPerUnit)
|
||||
}
|
||||
@@ -99,12 +99,75 @@ func GetJsonString(data any) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// MaskSensitiveInfo masks sensitive information like URLs, IPs in a string
|
||||
// MaskEmail masks a user email to prevent PII leakage in logs
|
||||
// Returns "***masked***" if email is empty, otherwise shows only the domain part
|
||||
func MaskEmail(email string) string {
|
||||
if email == "" {
|
||||
return "***masked***"
|
||||
}
|
||||
|
||||
// Find the @ symbol
|
||||
atIndex := strings.Index(email, "@")
|
||||
if atIndex == -1 {
|
||||
// No @ symbol found, return masked
|
||||
return "***masked***"
|
||||
}
|
||||
|
||||
// Return only the domain part with @ symbol
|
||||
return "***@" + email[atIndex+1:]
|
||||
}
|
||||
|
||||
// maskHostTail returns the tail parts of a domain/host that should be preserved.
|
||||
// It keeps 2 parts for likely country-code TLDs (e.g., co.uk, com.cn), otherwise keeps only the TLD.
|
||||
func maskHostTail(parts []string) []string {
|
||||
if len(parts) < 2 {
|
||||
return parts
|
||||
}
|
||||
lastPart := parts[len(parts)-1]
|
||||
secondLastPart := parts[len(parts)-2]
|
||||
if len(lastPart) == 2 && len(secondLastPart) <= 3 {
|
||||
// Likely country code TLD like co.uk, com.cn
|
||||
return []string{secondLastPart, lastPart}
|
||||
}
|
||||
return []string{lastPart}
|
||||
}
|
||||
|
||||
// maskHostForURL collapses subdomains and keeps only masked prefix + preserved tail.
|
||||
// Example: api.openai.com -> ***.com, sub.domain.co.uk -> ***.co.uk
|
||||
func maskHostForURL(host string) string {
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 2 {
|
||||
return "***"
|
||||
}
|
||||
tail := maskHostTail(parts)
|
||||
return "***." + strings.Join(tail, ".")
|
||||
}
|
||||
|
||||
// maskHostForPlainDomain masks a plain domain and reflects subdomain depth with multiple ***.
|
||||
// Example: openai.com -> ***.com, api.openai.com -> ***.***.com, sub.domain.co.uk -> ***.***.co.uk
|
||||
func maskHostForPlainDomain(domain string) string {
|
||||
parts := strings.Split(domain, ".")
|
||||
if len(parts) < 2 {
|
||||
return domain
|
||||
}
|
||||
tail := maskHostTail(parts)
|
||||
numStars := len(parts) - len(tail)
|
||||
if numStars < 1 {
|
||||
numStars = 1
|
||||
}
|
||||
stars := strings.TrimSuffix(strings.Repeat("***.", numStars), ".")
|
||||
return stars + "." + strings.Join(tail, ".")
|
||||
}
|
||||
|
||||
// MaskSensitiveInfo masks sensitive information like URLs, IPs, and domain names in a string
|
||||
// Example:
|
||||
// http://example.com -> http://***.com
|
||||
// https://api.test.org/v1/users/123?key=secret -> https://***.org/***/***/?key=***
|
||||
// https://sub.domain.co.uk/path/to/resource -> https://***.co.uk/***/***
|
||||
// 192.168.1.1 -> ***.***.***.***
|
||||
// openai.com -> ***.com
|
||||
// www.openai.com -> ***.***.com
|
||||
// api.openai.com -> ***.***.com
|
||||
func MaskSensitiveInfo(str string) string {
|
||||
// Mask URLs
|
||||
urlPattern := regexp.MustCompile(`(http|https)://[^\s/$.?#].[^\s]*`)
|
||||
@@ -119,32 +182,8 @@ func MaskSensitiveInfo(str string) string {
|
||||
return urlStr
|
||||
}
|
||||
|
||||
// Split host by dots
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 2 {
|
||||
// If less than 2 parts, just mask the whole host
|
||||
return u.Scheme + "://***" + u.Path
|
||||
}
|
||||
|
||||
// Keep the TLD (Top Level Domain) and mask the rest
|
||||
var maskedHost string
|
||||
if len(parts) == 2 {
|
||||
// example.com -> ***.com
|
||||
maskedHost = "***." + parts[len(parts)-1]
|
||||
} else {
|
||||
// Handle cases like sub.domain.co.uk or api.example.com
|
||||
// Keep last 2 parts if they look like country code TLD (co.uk, com.cn, etc.)
|
||||
lastPart := parts[len(parts)-1]
|
||||
secondLastPart := parts[len(parts)-2]
|
||||
|
||||
if len(lastPart) == 2 && len(secondLastPart) <= 3 {
|
||||
// Likely country code TLD like co.uk, com.cn
|
||||
maskedHost = "***." + secondLastPart + "." + lastPart
|
||||
} else {
|
||||
// Regular TLD like .com, .org
|
||||
maskedHost = "***." + lastPart
|
||||
}
|
||||
}
|
||||
// Mask host with unified logic
|
||||
maskedHost := maskHostForURL(host)
|
||||
|
||||
result := u.Scheme + "://" + maskedHost
|
||||
|
||||
@@ -184,6 +223,12 @@ func MaskSensitiveInfo(str string) string {
|
||||
return result
|
||||
})
|
||||
|
||||
// Mask domain names without protocol (like openai.com, www.openai.com)
|
||||
domainPattern := regexp.MustCompile(`\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b`)
|
||||
str = domainPattern.ReplaceAllStringFunc(str, func(domain string) string {
|
||||
return maskHostForPlainDomain(domain)
|
||||
})
|
||||
|
||||
// Mask IP addresses
|
||||
ipPattern := regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
|
||||
str = ipPattern.ReplaceAllString(str, "***.***.***.***")
|
||||
|
||||
24
common/sys_log.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SysLog(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
}
|
||||
|
||||
func SysError(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
}
|
||||
|
||||
func FatalLog(v ...any) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -31,5 +31,6 @@ const (
|
||||
APITypeXai
|
||||
APITypeCoze
|
||||
APITypeJimeng
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
APITypeMoonshot // this one is only for count, do not add any channel after this
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
)
|
||||
|
||||
@@ -3,6 +3,9 @@ package constant
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
ContextKeyTokenCountMeta ContextKey = "token_count_meta"
|
||||
ContextKeyPromptTokens ContextKey = "prompt_tokens"
|
||||
|
||||
ContextKeyOriginalModel ContextKey = "original_model"
|
||||
ContextKeyRequestStartTime ContextKey = "request_start_time"
|
||||
|
||||
@@ -22,6 +25,7 @@ const (
|
||||
ContextKeyChannelBaseUrl ContextKey = "base_url"
|
||||
ContextKeyChannelType ContextKey = "channel_type"
|
||||
ContextKeyChannelSetting ContextKey = "channel_setting"
|
||||
ContextKeyChannelOtherSetting ContextKey = "channel_other_setting"
|
||||
ContextKeyChannelParamOverride ContextKey = "param_override"
|
||||
ContextKeyChannelOrganization ContextKey = "channel_organization"
|
||||
ContextKeyChannelAutoBan ContextKey = "auto_ban"
|
||||
@@ -40,4 +44,6 @@ const (
|
||||
ContextKeyUserGroup ContextKey = "user_group"
|
||||
ContextKeyUsingGroup ContextKey = "group"
|
||||
ContextKeyUserName ContextKey = "username"
|
||||
|
||||
ContextKeySystemPromptOverride ContextKey = "system_prompt_override"
|
||||
)
|
||||
|
||||
@@ -135,7 +135,11 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
|
||||
for k := range headers {
|
||||
req.Header.Add(k, headers.Get(k))
|
||||
}
|
||||
res, err := service.GetHttpClient().Do(req)
|
||||
client, err := service.NewProxyHttpClient(channel.GetSetting().Proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -132,10 +132,27 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
newAPIError: newAPIError,
|
||||
}
|
||||
}
|
||||
request := buildTestRequest(testModel)
|
||||
|
||||
info := relaycommon.GenRelayInfo(c)
|
||||
// Determine relay format based on request path
|
||||
relayFormat := types.RelayFormatOpenAI
|
||||
if c.Request.URL.Path == "/v1/embeddings" {
|
||||
relayFormat = types.RelayFormatEmbedding
|
||||
}
|
||||
|
||||
err = helper.ModelMappedHelper(c, info, nil)
|
||||
info, err := relaycommon.GenRelayInfo(c, relayFormat, request, nil)
|
||||
|
||||
if err != nil {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeGenRelayInfoFailed),
|
||||
}
|
||||
}
|
||||
|
||||
info.InitChannelMeta(c)
|
||||
|
||||
err = helper.ModelMappedHelper(c, info, request)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
context: c,
|
||||
@@ -143,7 +160,9 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
newAPIError: types.NewError(err, types.ErrorCodeChannelModelMappedError),
|
||||
}
|
||||
}
|
||||
|
||||
testModel = info.UpstreamModelName
|
||||
request.Model = testModel
|
||||
|
||||
apiType, _ := common.ChannelType2APIType(channel.Type)
|
||||
adaptor := relay.GetAdaptor(apiType)
|
||||
@@ -155,13 +174,12 @@ func testChannel(channel *model.Channel, testModel string) testResult {
|
||||
}
|
||||
}
|
||||
|
||||
request := buildTestRequest(testModel)
|
||||
// 创建一个用于日志的 info 副本,移除 ApiKey
|
||||
logInfo := *info
|
||||
logInfo.ApiKey = ""
|
||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %+v ", channel.Id, testModel, logInfo))
|
||||
//// 创建一个用于日志的 info 副本,移除 ApiKey
|
||||
//logInfo := info
|
||||
//logInfo.ApiKey = ""
|
||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %+v ", channel.Id, testModel, info.ToString()))
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.GetMaxTokens()))
|
||||
priceData, err := helper.ModelPriceHelper(c, info, 0, request.GetTokenCountMeta())
|
||||
if err != nil {
|
||||
return testResult{
|
||||
context: c,
|
||||
@@ -427,7 +445,7 @@ func testAllChannels(notify bool) error {
|
||||
|
||||
// disable channel
|
||||
if isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
|
||||
go processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
}
|
||||
|
||||
// enable channel
|
||||
|
||||
@@ -3,101 +3,102 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MigrateConsoleSetting 迁移旧的控制台相关配置到 console_setting.*
|
||||
func MigrateConsoleSetting(c *gin.Context) {
|
||||
// 读取全部 option
|
||||
opts, err := model.AllOption()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
// 建立 map
|
||||
valMap := map[string]string{}
|
||||
for _, o := range opts {
|
||||
valMap[o.Key] = o.Value
|
||||
}
|
||||
// 读取全部 option
|
||||
opts, err := model.AllOption()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
// 建立 map
|
||||
valMap := map[string]string{}
|
||||
for _, o := range opts {
|
||||
valMap[o.Key] = o.Value
|
||||
}
|
||||
|
||||
// 处理 APIInfo
|
||||
if v := valMap["ApiInfo"]; v != "" {
|
||||
var arr []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||
if len(arr) > 50 {
|
||||
arr = arr[:50]
|
||||
}
|
||||
bytes, _ := json.Marshal(arr)
|
||||
model.UpdateOption("console_setting.api_info", string(bytes))
|
||||
}
|
||||
model.UpdateOption("ApiInfo", "")
|
||||
}
|
||||
// Announcements 直接搬
|
||||
if v := valMap["Announcements"]; v != "" {
|
||||
model.UpdateOption("console_setting.announcements", v)
|
||||
model.UpdateOption("Announcements", "")
|
||||
}
|
||||
// FAQ 转换
|
||||
if v := valMap["FAQ"]; v != "" {
|
||||
var arr []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||
out := []map[string]interface{}{}
|
||||
for _, item := range arr {
|
||||
q, _ := item["question"].(string)
|
||||
if q == "" {
|
||||
q, _ = item["title"].(string)
|
||||
}
|
||||
a, _ := item["answer"].(string)
|
||||
if a == "" {
|
||||
a, _ = item["content"].(string)
|
||||
}
|
||||
if q != "" && a != "" {
|
||||
out = append(out, map[string]interface{}{"question": q, "answer": a})
|
||||
}
|
||||
}
|
||||
if len(out) > 50 {
|
||||
out = out[:50]
|
||||
}
|
||||
bytes, _ := json.Marshal(out)
|
||||
model.UpdateOption("console_setting.faq", string(bytes))
|
||||
}
|
||||
model.UpdateOption("FAQ", "")
|
||||
}
|
||||
// Uptime Kuma 迁移到新的 groups 结构(console_setting.uptime_kuma_groups)
|
||||
url := valMap["UptimeKumaUrl"]
|
||||
slug := valMap["UptimeKumaSlug"]
|
||||
if url != "" && slug != "" {
|
||||
// 仅当同时存在 URL 与 Slug 时才进行迁移
|
||||
groups := []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"categoryName": "old",
|
||||
"url": url,
|
||||
"slug": slug,
|
||||
"description": "",
|
||||
},
|
||||
}
|
||||
bytes, _ := json.Marshal(groups)
|
||||
model.UpdateOption("console_setting.uptime_kuma_groups", string(bytes))
|
||||
}
|
||||
// 清空旧键内容
|
||||
if url != "" {
|
||||
model.UpdateOption("UptimeKumaUrl", "")
|
||||
}
|
||||
if slug != "" {
|
||||
model.UpdateOption("UptimeKumaSlug", "")
|
||||
}
|
||||
// 处理 APIInfo
|
||||
if v := valMap["ApiInfo"]; v != "" {
|
||||
var arr []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||
if len(arr) > 50 {
|
||||
arr = arr[:50]
|
||||
}
|
||||
bytes, _ := json.Marshal(arr)
|
||||
model.UpdateOption("console_setting.api_info", string(bytes))
|
||||
}
|
||||
model.UpdateOption("ApiInfo", "")
|
||||
}
|
||||
// Announcements 直接搬
|
||||
if v := valMap["Announcements"]; v != "" {
|
||||
model.UpdateOption("console_setting.announcements", v)
|
||||
model.UpdateOption("Announcements", "")
|
||||
}
|
||||
// FAQ 转换
|
||||
if v := valMap["FAQ"]; v != "" {
|
||||
var arr []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||
out := []map[string]interface{}{}
|
||||
for _, item := range arr {
|
||||
q, _ := item["question"].(string)
|
||||
if q == "" {
|
||||
q, _ = item["title"].(string)
|
||||
}
|
||||
a, _ := item["answer"].(string)
|
||||
if a == "" {
|
||||
a, _ = item["content"].(string)
|
||||
}
|
||||
if q != "" && a != "" {
|
||||
out = append(out, map[string]interface{}{"question": q, "answer": a})
|
||||
}
|
||||
}
|
||||
if len(out) > 50 {
|
||||
out = out[:50]
|
||||
}
|
||||
bytes, _ := json.Marshal(out)
|
||||
model.UpdateOption("console_setting.faq", string(bytes))
|
||||
}
|
||||
model.UpdateOption("FAQ", "")
|
||||
}
|
||||
// Uptime Kuma 迁移到新的 groups 结构(console_setting.uptime_kuma_groups)
|
||||
url := valMap["UptimeKumaUrl"]
|
||||
slug := valMap["UptimeKumaSlug"]
|
||||
if url != "" && slug != "" {
|
||||
// 仅当同时存在 URL 与 Slug 时才进行迁移
|
||||
groups := []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"categoryName": "old",
|
||||
"url": url,
|
||||
"slug": slug,
|
||||
"description": "",
|
||||
},
|
||||
}
|
||||
bytes, _ := json.Marshal(groups)
|
||||
model.UpdateOption("console_setting.uptime_kuma_groups", string(bytes))
|
||||
}
|
||||
// 清空旧键内容
|
||||
if url != "" {
|
||||
model.UpdateOption("UptimeKumaUrl", "")
|
||||
}
|
||||
if slug != "" {
|
||||
model.UpdateOption("UptimeKumaSlug", "")
|
||||
}
|
||||
|
||||
// 删除旧键记录
|
||||
oldKeys := []string{"ApiInfo", "Announcements", "FAQ", "UptimeKumaUrl", "UptimeKumaSlug"}
|
||||
model.DB.Where("key IN ?", oldKeys).Delete(&model.Option{})
|
||||
// 删除旧键记录
|
||||
oldKeys := []string{"ApiInfo", "Announcements", "FAQ", "UptimeKumaUrl", "UptimeKumaSlug"}
|
||||
model.DB.Where("key IN ?", oldKeys).Delete(&model.Option{})
|
||||
|
||||
// 重新加载 OptionMap
|
||||
model.InitOptionMap()
|
||||
common.SysLog("console setting migrated")
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "migrated"})
|
||||
}
|
||||
// 重新加载 OptionMap
|
||||
model.InitOptionMap()
|
||||
common.SysLog("console setting migrated")
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "migrated"})
|
||||
}
|
||||
|
||||
@@ -220,21 +220,29 @@ func LinuxdoOAuth(c *gin.Context) {
|
||||
}
|
||||
} else {
|
||||
if common.RegisterEnabled {
|
||||
user.Username = "linuxdo_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
user.DisplayName = linuxdoUser.Name
|
||||
user.Role = common.RoleCommonUser
|
||||
user.Status = common.UserStatusEnabled
|
||||
if linuxdoUser.TrustLevel >= common.LinuxDOMinimumTrustLevel {
|
||||
user.Username = "linuxdo_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
user.DisplayName = linuxdoUser.Name
|
||||
user.Role = common.RoleCommonUser
|
||||
user.Status = common.UserStatusEnabled
|
||||
|
||||
affCode := session.Get("aff")
|
||||
inviterId := 0
|
||||
if affCode != nil {
|
||||
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
|
||||
}
|
||||
affCode := session.Get("aff")
|
||||
inviterId := 0
|
||||
if affCode != nil {
|
||||
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
|
||||
}
|
||||
|
||||
if err := user.Insert(inviterId); err != nil {
|
||||
if err := user.Insert(inviterId); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
"message": "Linux DO 信任等级未达到管理员设置的最低信任等级",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
@@ -28,7 +29,7 @@ func UpdateMidjourneyTaskBulk() {
|
||||
continue
|
||||
}
|
||||
|
||||
common.LogInfo(ctx, fmt.Sprintf("检测到未完成的任务数有: %v", len(tasks)))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("检测到未完成的任务数有: %v", len(tasks)))
|
||||
taskChannelM := make(map[int][]string)
|
||||
taskM := make(map[string]*model.Midjourney)
|
||||
nullTaskIds := make([]int, 0)
|
||||
@@ -47,9 +48,9 @@ func UpdateMidjourneyTaskBulk() {
|
||||
"progress": "100%",
|
||||
})
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Fix null mj_id task error: %v", err))
|
||||
logger.LogError(ctx, fmt.Sprintf("Fix null mj_id task error: %v", err))
|
||||
} else {
|
||||
common.LogInfo(ctx, fmt.Sprintf("Fix null mj_id task success: %v", nullTaskIds))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("Fix null mj_id task success: %v", nullTaskIds))
|
||||
}
|
||||
}
|
||||
if len(taskChannelM) == 0 {
|
||||
@@ -57,20 +58,20 @@ func UpdateMidjourneyTaskBulk() {
|
||||
}
|
||||
|
||||
for channelId, taskIds := range taskChannelM {
|
||||
common.LogInfo(ctx, fmt.Sprintf("渠道 #%d 未完成的任务有: %d", channelId, len(taskIds)))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("渠道 #%d 未完成的任务有: %d", channelId, len(taskIds)))
|
||||
if len(taskIds) == 0 {
|
||||
continue
|
||||
}
|
||||
midjourneyChannel, err := model.CacheGetChannel(channelId)
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("CacheGetChannel: %v", err))
|
||||
logger.LogError(ctx, fmt.Sprintf("CacheGetChannel: %v", err))
|
||||
err := model.MjBulkUpdate(taskIds, map[string]any{
|
||||
"fail_reason": fmt.Sprintf("获取渠道信息失败,请联系管理员,渠道ID:%d", channelId),
|
||||
"status": "FAILURE",
|
||||
"progress": "100%",
|
||||
})
|
||||
if err != nil {
|
||||
common.LogInfo(ctx, fmt.Sprintf("UpdateMidjourneyTask error: %v", err))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("UpdateMidjourneyTask error: %v", err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -81,7 +82,7 @@ func UpdateMidjourneyTaskBulk() {
|
||||
})
|
||||
req, err := http.NewRequest("POST", requestUrl, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task error: %v", err))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task error: %v", err))
|
||||
continue
|
||||
}
|
||||
// 设置超时时间
|
||||
@@ -93,22 +94,22 @@ func UpdateMidjourneyTaskBulk() {
|
||||
req.Header.Set("mj-api-secret", midjourneyChannel.Key)
|
||||
resp, err := service.GetHttpClient().Do(req)
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task Do req error: %v", err))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task Do req error: %v", err))
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
||||
continue
|
||||
}
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task parse body error: %v", err))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task parse body error: %v", err))
|
||||
continue
|
||||
}
|
||||
var responseItems []dto.MidjourneyDto
|
||||
err = json.Unmarshal(responseBody, &responseItems)
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task parse body error2: %v, body: %s", err, string(responseBody)))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task parse body error2: %v, body: %s", err, string(responseBody)))
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
@@ -145,9 +146,25 @@ func UpdateMidjourneyTaskBulk() {
|
||||
buttonStr, _ := json.Marshal(responseItem.Buttons)
|
||||
task.Buttons = string(buttonStr)
|
||||
}
|
||||
// 映射 VideoUrl
|
||||
task.VideoUrl = responseItem.VideoUrl
|
||||
|
||||
// 映射 VideoUrls - 将数组序列化为 JSON 字符串
|
||||
if responseItem.VideoUrls != nil && len(responseItem.VideoUrls) > 0 {
|
||||
videoUrlsStr, err := json.Marshal(responseItem.VideoUrls)
|
||||
if err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("序列化 VideoUrls 失败: %v", err))
|
||||
task.VideoUrls = "[]" // 失败时设置为空数组
|
||||
} else {
|
||||
task.VideoUrls = string(videoUrlsStr)
|
||||
}
|
||||
} else {
|
||||
task.VideoUrls = "" // 空值时清空字段
|
||||
}
|
||||
|
||||
shouldReturnQuota := false
|
||||
if (task.Progress != "100%" && responseItem.FailReason != "") || (task.Progress == "100%" && task.Status == "FAILURE") {
|
||||
common.LogInfo(ctx, task.MjId+" 构建失败,"+task.FailReason)
|
||||
logger.LogInfo(ctx, task.MjId+" 构建失败,"+task.FailReason)
|
||||
task.Progress = "100%"
|
||||
if task.Quota != 0 {
|
||||
shouldReturnQuota = true
|
||||
@@ -155,14 +172,14 @@ func UpdateMidjourneyTaskBulk() {
|
||||
}
|
||||
err = task.Update()
|
||||
if err != nil {
|
||||
common.LogError(ctx, "UpdateMidjourneyTask task error: "+err.Error())
|
||||
logger.LogError(ctx, "UpdateMidjourneyTask task error: "+err.Error())
|
||||
} else {
|
||||
if shouldReturnQuota {
|
||||
err = model.IncreaseUserQuota(task.UserId, task.Quota, false)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "fail to increase user quota: "+err.Error())
|
||||
logger.LogError(ctx, "fail to increase user quota: "+err.Error())
|
||||
}
|
||||
logContent := fmt.Sprintf("构图失败 %s,补偿 %s", task.MjId, common.LogQuota(task.Quota))
|
||||
logContent := fmt.Sprintf("构图失败 %s,补偿 %s", task.MjId, logger.LogQuota(task.Quota))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
}
|
||||
}
|
||||
@@ -208,6 +225,20 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
|
||||
if oldTask.Progress != "100%" && newTask.FailReason != "" {
|
||||
return true
|
||||
}
|
||||
// 检查 VideoUrl 是否需要更新
|
||||
if oldTask.VideoUrl != newTask.VideoUrl {
|
||||
return true
|
||||
}
|
||||
// 检查 VideoUrls 是否需要更新
|
||||
if newTask.VideoUrls != nil && len(newTask.VideoUrls) > 0 {
|
||||
newVideoUrlsStr, _ := json.Marshal(newTask.VideoUrls)
|
||||
if oldTask.VideoUrls != string(newVideoUrlsStr) {
|
||||
return true
|
||||
}
|
||||
} else if oldTask.VideoUrls != "" {
|
||||
// 如果新数据没有 VideoUrls 但旧数据有,需要更新(清空)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -41,46 +41,47 @@ func GetStatus(c *gin.Context) {
|
||||
cs := console_setting.GetConsoleSetting()
|
||||
|
||||
data := gin.H{
|
||||
"version": common.Version,
|
||||
"start_time": common.StartTime,
|
||||
"email_verification": common.EmailVerificationEnabled,
|
||||
"github_oauth": common.GitHubOAuthEnabled,
|
||||
"github_client_id": common.GitHubClientId,
|
||||
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
||||
"linuxdo_client_id": common.LinuxDOClientId,
|
||||
"telegram_oauth": common.TelegramOAuthEnabled,
|
||||
"telegram_bot_name": common.TelegramBotName,
|
||||
"system_name": common.SystemName,
|
||||
"logo": common.Logo,
|
||||
"footer_html": common.Footer,
|
||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||
"wechat_login": common.WeChatAuthEnabled,
|
||||
"server_address": setting.ServerAddress,
|
||||
"price": setting.Price,
|
||||
"stripe_unit_price": setting.StripeUnitPrice,
|
||||
"min_topup": setting.MinTopUp,
|
||||
"stripe_min_topup": setting.StripeMinTopUp,
|
||||
"turnstile_check": common.TurnstileCheckEnabled,
|
||||
"turnstile_site_key": common.TurnstileSiteKey,
|
||||
"top_up_link": common.TopUpLink,
|
||||
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
|
||||
"quota_per_unit": common.QuotaPerUnit,
|
||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||
"enable_batch_update": common.BatchUpdateEnabled,
|
||||
"enable_drawing": common.DrawingEnabled,
|
||||
"enable_task": common.TaskEnabled,
|
||||
"enable_data_export": common.DataExportEnabled,
|
||||
"data_export_default_time": common.DataExportDefaultTime,
|
||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
||||
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||
"chats": setting.Chats,
|
||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
||||
"pay_methods": setting.PayMethods,
|
||||
"usd_exchange_rate": setting.USDExchangeRate,
|
||||
"version": common.Version,
|
||||
"start_time": common.StartTime,
|
||||
"email_verification": common.EmailVerificationEnabled,
|
||||
"github_oauth": common.GitHubOAuthEnabled,
|
||||
"github_client_id": common.GitHubClientId,
|
||||
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
||||
"linuxdo_client_id": common.LinuxDOClientId,
|
||||
"linuxdo_minimum_trust_level": common.LinuxDOMinimumTrustLevel,
|
||||
"telegram_oauth": common.TelegramOAuthEnabled,
|
||||
"telegram_bot_name": common.TelegramBotName,
|
||||
"system_name": common.SystemName,
|
||||
"logo": common.Logo,
|
||||
"footer_html": common.Footer,
|
||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||
"wechat_login": common.WeChatAuthEnabled,
|
||||
"server_address": setting.ServerAddress,
|
||||
"price": setting.Price,
|
||||
"stripe_unit_price": setting.StripeUnitPrice,
|
||||
"min_topup": setting.MinTopUp,
|
||||
"stripe_min_topup": setting.StripeMinTopUp,
|
||||
"turnstile_check": common.TurnstileCheckEnabled,
|
||||
"turnstile_site_key": common.TurnstileSiteKey,
|
||||
"top_up_link": common.TopUpLink,
|
||||
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
|
||||
"quota_per_unit": common.QuotaPerUnit,
|
||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||
"enable_batch_update": common.BatchUpdateEnabled,
|
||||
"enable_drawing": common.DrawingEnabled,
|
||||
"enable_task": common.TaskEnabled,
|
||||
"enable_data_export": common.DataExportEnabled,
|
||||
"data_export_default_time": common.DataExportDefaultTime,
|
||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
||||
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||
"chats": setting.Chats,
|
||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
||||
"pay_methods": setting.PayMethods,
|
||||
"usd_exchange_rate": setting.USDExchangeRate,
|
||||
|
||||
// 面板启用开关
|
||||
"api_info_enabled": cs.ApiInfoEnabled,
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"one-api/model"
|
||||
"net/http"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetMissingModels returns the list of model names that are referenced by channels
|
||||
// but do not have corresponding records in the models meta table.
|
||||
// This helps administrators quickly discover models that need configuration.
|
||||
func GetMissingModels(c *gin.Context) {
|
||||
missing, err := model.GetMissingModels()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
missing, err := model.GetMissingModels()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": missing,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": missing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"one-api/relay/channel/moonshot"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://platform.openai.com/docs/api-reference/models/list
|
||||
@@ -92,7 +93,9 @@ func init() {
|
||||
if !success || apiType == constant.APITypeAIProxyLibrary {
|
||||
continue
|
||||
}
|
||||
meta := &relaycommon.RelayInfo{ChannelType: i}
|
||||
meta := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{
|
||||
ChannelType: i,
|
||||
}}
|
||||
adaptor := relay.GetAdaptor(apiType)
|
||||
adaptor.Init(meta)
|
||||
channelId2Models[i] = adaptor.GetModelList()
|
||||
@@ -102,7 +105,7 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func ListModels(c *gin.Context) {
|
||||
func ListModels(c *gin.Context, modelType int) {
|
||||
userOpenAiModels := make([]dto.OpenAIModels, 0)
|
||||
|
||||
modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
|
||||
@@ -171,10 +174,41 @@ func ListModels(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": userOpenAiModels,
|
||||
})
|
||||
switch modelType {
|
||||
case constant.ChannelTypeAnthropic:
|
||||
useranthropicModels := make([]dto.AnthropicModel, len(userOpenAiModels))
|
||||
for i, model := range userOpenAiModels {
|
||||
useranthropicModels[i] = dto.AnthropicModel{
|
||||
ID: model.Id,
|
||||
CreatedAt: time.Unix(int64(model.Created), 0).UTC().Format(time.RFC3339),
|
||||
DisplayName: model.Id,
|
||||
Type: "model",
|
||||
}
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"data": useranthropicModels,
|
||||
"first_id": useranthropicModels[0].ID,
|
||||
"has_more": false,
|
||||
"last_id": useranthropicModels[len(useranthropicModels)-1].ID,
|
||||
})
|
||||
case constant.ChannelTypeGemini:
|
||||
userGeminiModels := make([]dto.GeminiModel, len(userOpenAiModels))
|
||||
for i, model := range userOpenAiModels {
|
||||
userGeminiModels[i] = dto.GeminiModel{
|
||||
Name: model.Id,
|
||||
DisplayName: model.Id,
|
||||
}
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"models": userGeminiModels,
|
||||
"nextPageToken": nil,
|
||||
})
|
||||
default:
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": userOpenAiModels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ChannelListModels(c *gin.Context) {
|
||||
@@ -198,10 +232,20 @@ func EnabledListModels(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func RetrieveModel(c *gin.Context) {
|
||||
func RetrieveModel(c *gin.Context, modelType int) {
|
||||
modelId := c.Param("model")
|
||||
if aiModel, ok := openAIModelsMap[modelId]; ok {
|
||||
c.JSON(200, aiModel)
|
||||
switch modelType {
|
||||
case constant.ChannelTypeAnthropic:
|
||||
c.JSON(200, dto.AnthropicModel{
|
||||
ID: aiModel.Id,
|
||||
CreatedAt: time.Unix(int64(aiModel.Created), 0).UTC().Format(time.RFC3339),
|
||||
DisplayName: aiModel.Id,
|
||||
Type: "model",
|
||||
})
|
||||
default:
|
||||
c.JSON(200, aiModel)
|
||||
}
|
||||
} else {
|
||||
openAIError := dto.OpenAIError{
|
||||
Message: fmt.Sprintf("The model '%s' does not exist", modelId),
|
||||
|
||||
@@ -1,178 +1,330 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetAllModelsMeta 获取模型列表(分页)
|
||||
func GetAllModelsMeta(c *gin.Context) {
|
||||
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
modelsMeta, err := model.GetAllModels(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
// 填充附加字段
|
||||
for _, m := range modelsMeta {
|
||||
fillModelExtra(m)
|
||||
}
|
||||
var total int64
|
||||
model.DB.Model(&model.Model{}).Count(&total)
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
modelsMeta, err := model.GetAllModels(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
// 批量填充附加字段,提升列表接口性能
|
||||
enrichModels(modelsMeta)
|
||||
var total int64
|
||||
model.DB.Model(&model.Model{}).Count(&total)
|
||||
|
||||
// 统计供应商计数(全部数据,不受分页影响)
|
||||
vendorCounts, _ := model.GetVendorModelCounts()
|
||||
// 统计供应商计数(全部数据,不受分页影响)
|
||||
vendorCounts, _ := model.GetVendorModelCounts()
|
||||
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(modelsMeta)
|
||||
common.ApiSuccess(c, gin.H{
|
||||
"items": modelsMeta,
|
||||
"total": total,
|
||||
"page": pageInfo.GetPage(),
|
||||
"page_size": pageInfo.GetPageSize(),
|
||||
"vendor_counts": vendorCounts,
|
||||
})
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(modelsMeta)
|
||||
common.ApiSuccess(c, gin.H{
|
||||
"items": modelsMeta,
|
||||
"total": total,
|
||||
"page": pageInfo.GetPage(),
|
||||
"page_size": pageInfo.GetPageSize(),
|
||||
"vendor_counts": vendorCounts,
|
||||
})
|
||||
}
|
||||
|
||||
// SearchModelsMeta 搜索模型列表
|
||||
func SearchModelsMeta(c *gin.Context) {
|
||||
|
||||
keyword := c.Query("keyword")
|
||||
vendor := c.Query("vendor")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
keyword := c.Query("keyword")
|
||||
vendor := c.Query("vendor")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
|
||||
modelsMeta, total, err := model.SearchModels(keyword, vendor, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
for _, m := range modelsMeta {
|
||||
fillModelExtra(m)
|
||||
}
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(modelsMeta)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
modelsMeta, total, err := model.SearchModels(keyword, vendor, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
// 批量填充附加字段,提升列表接口性能
|
||||
enrichModels(modelsMeta)
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(modelsMeta)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
// GetModelMeta 根据 ID 获取单条模型信息
|
||||
func GetModelMeta(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var m model.Model
|
||||
if err := model.DB.First(&m, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
fillModelExtra(&m)
|
||||
common.ApiSuccess(c, &m)
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var m model.Model
|
||||
if err := model.DB.First(&m, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
enrichModels([]*model.Model{&m})
|
||||
common.ApiSuccess(c, &m)
|
||||
}
|
||||
|
||||
// CreateModelMeta 新建模型
|
||||
func CreateModelMeta(c *gin.Context) {
|
||||
var m model.Model
|
||||
if err := c.ShouldBindJSON(&m); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if m.ModelName == "" {
|
||||
common.ApiErrorMsg(c, "模型名称不能为空")
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsModelNameDuplicated(0, m.ModelName); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "模型名称已存在")
|
||||
return
|
||||
}
|
||||
var m model.Model
|
||||
if err := c.ShouldBindJSON(&m); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if m.ModelName == "" {
|
||||
common.ApiErrorMsg(c, "模型名称不能为空")
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsModelNameDuplicated(0, m.ModelName); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "模型名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RefreshPricing()
|
||||
common.ApiSuccess(c, &m)
|
||||
if err := m.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RefreshPricing()
|
||||
common.ApiSuccess(c, &m)
|
||||
}
|
||||
|
||||
// UpdateModelMeta 更新模型
|
||||
func UpdateModelMeta(c *gin.Context) {
|
||||
statusOnly := c.Query("status_only") == "true"
|
||||
statusOnly := c.Query("status_only") == "true"
|
||||
|
||||
var m model.Model
|
||||
if err := c.ShouldBindJSON(&m); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if m.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少模型 ID")
|
||||
return
|
||||
}
|
||||
var m model.Model
|
||||
if err := c.ShouldBindJSON(&m); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if m.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少模型 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if statusOnly {
|
||||
// 只更新状态,防止误清空其他字段
|
||||
if err := model.DB.Model(&model.Model{}).Where("id = ?", m.Id).Update("status", m.Status).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsModelNameDuplicated(m.Id, m.ModelName); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "模型名称已存在")
|
||||
return
|
||||
}
|
||||
if statusOnly {
|
||||
// 只更新状态,防止误清空其他字段
|
||||
if err := model.DB.Model(&model.Model{}).Where("id = ?", m.Id).Update("status", m.Status).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsModelNameDuplicated(m.Id, m.ModelName); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "模型名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
model.RefreshPricing()
|
||||
common.ApiSuccess(c, &m)
|
||||
if err := m.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
model.RefreshPricing()
|
||||
common.ApiSuccess(c, &m)
|
||||
}
|
||||
|
||||
// DeleteModelMeta 删除模型
|
||||
func DeleteModelMeta(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := model.DB.Delete(&model.Model{}, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RefreshPricing()
|
||||
common.ApiSuccess(c, nil)
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := model.DB.Delete(&model.Model{}, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RefreshPricing()
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
|
||||
// 辅助函数:填充 Endpoints 和 BoundChannels 和 EnableGroups
|
||||
func fillModelExtra(m *model.Model) {
|
||||
if m.Endpoints == "" {
|
||||
eps := model.GetModelSupportEndpointTypes(m.ModelName)
|
||||
if b, err := json.Marshal(eps); err == nil {
|
||||
m.Endpoints = string(b)
|
||||
}
|
||||
}
|
||||
if channels, err := model.GetBoundChannels(m.ModelName); err == nil {
|
||||
m.BoundChannels = channels
|
||||
}
|
||||
// 填充启用分组
|
||||
m.EnableGroups = model.GetModelEnableGroups(m.ModelName)
|
||||
// 填充计费类型
|
||||
m.QuotaType = model.GetModelQuotaType(m.ModelName)
|
||||
// enrichModels 批量填充附加信息:端点、渠道、分组、计费类型,避免 N+1 查询
|
||||
func enrichModels(models []*model.Model) {
|
||||
if len(models) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 1) 拆分精确与规则匹配
|
||||
exactNames := make([]string, 0)
|
||||
exactIdx := make(map[string][]int) // modelName -> indices in models
|
||||
ruleIndices := make([]int, 0)
|
||||
for i, m := range models {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
if m.NameRule == model.NameRuleExact {
|
||||
exactNames = append(exactNames, m.ModelName)
|
||||
exactIdx[m.ModelName] = append(exactIdx[m.ModelName], i)
|
||||
} else {
|
||||
ruleIndices = append(ruleIndices, i)
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 批量查询精确模型的绑定渠道
|
||||
channelsByModel, _ := model.GetBoundChannelsByModelsMap(exactNames)
|
||||
|
||||
// 3) 精确模型:端点从缓存、渠道批量映射、分组/计费类型从缓存
|
||||
for name, indices := range exactIdx {
|
||||
chs := channelsByModel[name]
|
||||
for _, idx := range indices {
|
||||
mm := models[idx]
|
||||
if mm.Endpoints == "" {
|
||||
eps := model.GetModelSupportEndpointTypes(mm.ModelName)
|
||||
if b, err := json.Marshal(eps); err == nil {
|
||||
mm.Endpoints = string(b)
|
||||
}
|
||||
}
|
||||
mm.BoundChannels = chs
|
||||
mm.EnableGroups = model.GetModelEnableGroups(mm.ModelName)
|
||||
mm.QuotaTypes = model.GetModelQuotaTypes(mm.ModelName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ruleIndices) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 4) 一次性读取定价缓存,内存匹配所有规则模型
|
||||
pricings := model.GetPricing()
|
||||
|
||||
// 为全部规则模型收集匹配名集合、端点并集、分组并集、配额集合
|
||||
matchedNamesByIdx := make(map[int][]string)
|
||||
endpointSetByIdx := make(map[int]map[constant.EndpointType]struct{})
|
||||
groupSetByIdx := make(map[int]map[string]struct{})
|
||||
quotaSetByIdx := make(map[int]map[int]struct{})
|
||||
|
||||
for _, p := range pricings {
|
||||
for _, idx := range ruleIndices {
|
||||
mm := models[idx]
|
||||
var matched bool
|
||||
switch mm.NameRule {
|
||||
case model.NameRulePrefix:
|
||||
matched = strings.HasPrefix(p.ModelName, mm.ModelName)
|
||||
case model.NameRuleSuffix:
|
||||
matched = strings.HasSuffix(p.ModelName, mm.ModelName)
|
||||
case model.NameRuleContains:
|
||||
matched = strings.Contains(p.ModelName, mm.ModelName)
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
matchedNamesByIdx[idx] = append(matchedNamesByIdx[idx], p.ModelName)
|
||||
|
||||
es := endpointSetByIdx[idx]
|
||||
if es == nil {
|
||||
es = make(map[constant.EndpointType]struct{})
|
||||
endpointSetByIdx[idx] = es
|
||||
}
|
||||
for _, et := range p.SupportedEndpointTypes {
|
||||
es[et] = struct{}{}
|
||||
}
|
||||
|
||||
gs := groupSetByIdx[idx]
|
||||
if gs == nil {
|
||||
gs = make(map[string]struct{})
|
||||
groupSetByIdx[idx] = gs
|
||||
}
|
||||
for _, g := range p.EnableGroup {
|
||||
gs[g] = struct{}{}
|
||||
}
|
||||
|
||||
qs := quotaSetByIdx[idx]
|
||||
if qs == nil {
|
||||
qs = make(map[int]struct{})
|
||||
quotaSetByIdx[idx] = qs
|
||||
}
|
||||
qs[p.QuotaType] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 5) 汇总所有匹配到的模型名称,批量查询一次渠道
|
||||
allMatchedSet := make(map[string]struct{})
|
||||
for _, names := range matchedNamesByIdx {
|
||||
for _, n := range names {
|
||||
allMatchedSet[n] = struct{}{}
|
||||
}
|
||||
}
|
||||
allMatched := make([]string, 0, len(allMatchedSet))
|
||||
for n := range allMatchedSet {
|
||||
allMatched = append(allMatched, n)
|
||||
}
|
||||
matchedChannelsByModel, _ := model.GetBoundChannelsByModelsMap(allMatched)
|
||||
|
||||
// 6) 回填每个规则模型的并集信息
|
||||
for _, idx := range ruleIndices {
|
||||
mm := models[idx]
|
||||
|
||||
// 端点并集 -> 序列化
|
||||
if es, ok := endpointSetByIdx[idx]; ok && mm.Endpoints == "" {
|
||||
eps := make([]constant.EndpointType, 0, len(es))
|
||||
for et := range es {
|
||||
eps = append(eps, et)
|
||||
}
|
||||
if b, err := json.Marshal(eps); err == nil {
|
||||
mm.Endpoints = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// 分组并集
|
||||
if gs, ok := groupSetByIdx[idx]; ok {
|
||||
groups := make([]string, 0, len(gs))
|
||||
for g := range gs {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
mm.EnableGroups = groups
|
||||
}
|
||||
|
||||
// 配额类型集合(保持去重并排序)
|
||||
if qs, ok := quotaSetByIdx[idx]; ok {
|
||||
arr := make([]int, 0, len(qs))
|
||||
for k := range qs {
|
||||
arr = append(arr, k)
|
||||
}
|
||||
sort.Ints(arr)
|
||||
mm.QuotaTypes = arr
|
||||
}
|
||||
|
||||
// 渠道并集
|
||||
names := matchedNamesByIdx[idx]
|
||||
channelSet := make(map[string]model.BoundChannel)
|
||||
for _, n := range names {
|
||||
for _, ch := range matchedChannelsByModel[n] {
|
||||
key := ch.Name + "_" + strconv.Itoa(ch.Type)
|
||||
channelSet[key] = ch
|
||||
}
|
||||
}
|
||||
if len(channelSet) > 0 {
|
||||
chs := make([]model.BoundChannel, 0, len(channelSet))
|
||||
for _, ch := range channelSet {
|
||||
chs = append(chs, ch)
|
||||
}
|
||||
mm.BoundChannels = chs
|
||||
}
|
||||
|
||||
// 匹配信息
|
||||
mm.MatchedModels = names
|
||||
mm.MatchedCount = len(names)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||
}
|
||||
|
||||
if oidcResponse.AccessToken == "" {
|
||||
common.SysError("OIDC 获取 Token 失败,请检查设置!")
|
||||
common.SysLog("OIDC 获取 Token 失败,请检查设置!")
|
||||
return nil, errors.New("OIDC 获取 Token 失败,请检查设置!")
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||
}
|
||||
defer res2.Body.Close()
|
||||
if res2.StatusCode != http.StatusOK {
|
||||
common.SysError("OIDC 获取用户信息失败!请检查设置!")
|
||||
common.SysLog("OIDC 获取用户信息失败!请检查设置!")
|
||||
return nil, errors.New("OIDC 获取用户信息失败!请检查设置!")
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||
return nil, err
|
||||
}
|
||||
if oidcUser.OpenID == "" || oidcUser.Email == "" {
|
||||
common.SysError("OIDC 获取用户信息为空!请检查设置!")
|
||||
common.SysLog("OIDC 获取用户信息为空!请检查设置!")
|
||||
return nil, errors.New("OIDC 获取用户信息为空!请检查设置!")
|
||||
}
|
||||
return &oidcUser, nil
|
||||
|
||||
@@ -56,5 +56,5 @@ func Playground(c *gin.Context) {
|
||||
//middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
|
||||
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
|
||||
|
||||
Relay(c)
|
||||
Relay(c, types.RelayFormatOpenAI)
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strconv"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetPrefillGroups 获取预填组列表,可通过 ?type=xxx 过滤
|
||||
func GetPrefillGroups(c *gin.Context) {
|
||||
groupType := c.Query("type")
|
||||
groups, err := model.GetAllPrefillGroups(groupType)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, groups)
|
||||
groupType := c.Query("type")
|
||||
groups, err := model.GetAllPrefillGroups(groupType)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, groups)
|
||||
}
|
||||
|
||||
// CreatePrefillGroup 创建新的预填组
|
||||
func CreatePrefillGroup(c *gin.Context) {
|
||||
var g model.PrefillGroup
|
||||
if err := c.ShouldBindJSON(&g); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if g.Name == "" || g.Type == "" {
|
||||
common.ApiErrorMsg(c, "组名称和类型不能为空")
|
||||
return
|
||||
}
|
||||
// 创建前检查名称
|
||||
if dup, err := model.IsPrefillGroupNameDuplicated(0, g.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "组名称已存在")
|
||||
return
|
||||
}
|
||||
var g model.PrefillGroup
|
||||
if err := c.ShouldBindJSON(&g); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if g.Name == "" || g.Type == "" {
|
||||
common.ApiErrorMsg(c, "组名称和类型不能为空")
|
||||
return
|
||||
}
|
||||
// 创建前检查名称
|
||||
if dup, err := model.IsPrefillGroupNameDuplicated(0, g.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "组名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := g.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &g)
|
||||
if err := g.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &g)
|
||||
}
|
||||
|
||||
// UpdatePrefillGroup 更新预填组
|
||||
func UpdatePrefillGroup(c *gin.Context) {
|
||||
var g model.PrefillGroup
|
||||
if err := c.ShouldBindJSON(&g); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if g.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少组 ID")
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsPrefillGroupNameDuplicated(g.Id, g.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "组名称已存在")
|
||||
return
|
||||
}
|
||||
var g model.PrefillGroup
|
||||
if err := c.ShouldBindJSON(&g); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if g.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少组 ID")
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsPrefillGroupNameDuplicated(g.Id, g.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "组名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := g.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &g)
|
||||
if err := g.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &g)
|
||||
}
|
||||
|
||||
// DeletePrefillGroup 删除预填组
|
||||
func DeletePrefillGroup(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := model.DeletePrefillGroupByID(id); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := model.DeletePrefillGroupByID(id); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
|
||||
@@ -39,13 +39,14 @@ func GetPricing(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"vendors": model.GetVendors(),
|
||||
"group_ratio": groupRatio,
|
||||
"usable_group": usableGroup,
|
||||
"supported_endpoint": model.GetSupportedEndpointMap(),
|
||||
})
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"vendors": model.GetVendors(),
|
||||
"group_ratio": groupRatio,
|
||||
"usable_group": usableGroup,
|
||||
"supported_endpoint": model.GetSupportedEndpointMap(),
|
||||
"auto_groups": setting.AutoGroups,
|
||||
})
|
||||
}
|
||||
|
||||
func ResetModelRatio(c *gin.Context) {
|
||||
|
||||
@@ -1,474 +1,474 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/logger"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/setting/ratio_setting"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeoutSeconds = 10
|
||||
defaultEndpoint = "/api/ratio_config"
|
||||
maxConcurrentFetches = 8
|
||||
defaultTimeoutSeconds = 10
|
||||
defaultEndpoint = "/api/ratio_config"
|
||||
maxConcurrentFetches = 8
|
||||
)
|
||||
|
||||
var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
|
||||
|
||||
type upstreamResult struct {
|
||||
Name string `json:"name"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
Err string `json:"err,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
func FetchUpstreamRatios(c *gin.Context) {
|
||||
var req dto.UpstreamRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
var req dto.UpstreamRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Timeout <= 0 {
|
||||
req.Timeout = defaultTimeoutSeconds
|
||||
}
|
||||
if req.Timeout <= 0 {
|
||||
req.Timeout = defaultTimeoutSeconds
|
||||
}
|
||||
|
||||
var upstreams []dto.UpstreamDTO
|
||||
var upstreams []dto.UpstreamDTO
|
||||
|
||||
if len(req.Upstreams) > 0 {
|
||||
for _, u := range req.Upstreams {
|
||||
if strings.HasPrefix(u.BaseURL, "http") {
|
||||
if u.Endpoint == "" {
|
||||
u.Endpoint = defaultEndpoint
|
||||
}
|
||||
u.BaseURL = strings.TrimRight(u.BaseURL, "/")
|
||||
upstreams = append(upstreams, u)
|
||||
}
|
||||
}
|
||||
} else if len(req.ChannelIDs) > 0 {
|
||||
intIds := make([]int, 0, len(req.ChannelIDs))
|
||||
for _, id64 := range req.ChannelIDs {
|
||||
intIds = append(intIds, int(id64))
|
||||
}
|
||||
dbChannels, err := model.GetChannelsByIds(intIds)
|
||||
if err != nil {
|
||||
common.LogError(c.Request.Context(), "failed to query channels: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询渠道失败"})
|
||||
return
|
||||
}
|
||||
for _, ch := range dbChannels {
|
||||
if base := ch.GetBaseURL(); strings.HasPrefix(base, "http") {
|
||||
upstreams = append(upstreams, dto.UpstreamDTO{
|
||||
ID: ch.Id,
|
||||
Name: ch.Name,
|
||||
BaseURL: strings.TrimRight(base, "/"),
|
||||
Endpoint: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(req.Upstreams) > 0 {
|
||||
for _, u := range req.Upstreams {
|
||||
if strings.HasPrefix(u.BaseURL, "http") {
|
||||
if u.Endpoint == "" {
|
||||
u.Endpoint = defaultEndpoint
|
||||
}
|
||||
u.BaseURL = strings.TrimRight(u.BaseURL, "/")
|
||||
upstreams = append(upstreams, u)
|
||||
}
|
||||
}
|
||||
} else if len(req.ChannelIDs) > 0 {
|
||||
intIds := make([]int, 0, len(req.ChannelIDs))
|
||||
for _, id64 := range req.ChannelIDs {
|
||||
intIds = append(intIds, int(id64))
|
||||
}
|
||||
dbChannels, err := model.GetChannelsByIds(intIds)
|
||||
if err != nil {
|
||||
logger.LogError(c.Request.Context(), "failed to query channels: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询渠道失败"})
|
||||
return
|
||||
}
|
||||
for _, ch := range dbChannels {
|
||||
if base := ch.GetBaseURL(); strings.HasPrefix(base, "http") {
|
||||
upstreams = append(upstreams, dto.UpstreamDTO{
|
||||
ID: ch.Id,
|
||||
Name: ch.Name,
|
||||
BaseURL: strings.TrimRight(base, "/"),
|
||||
Endpoint: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upstreams) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "无有效上游渠道"})
|
||||
return
|
||||
}
|
||||
if len(upstreams) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "无有效上游渠道"})
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan upstreamResult, len(upstreams))
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan upstreamResult, len(upstreams))
|
||||
|
||||
sem := make(chan struct{}, maxConcurrentFetches)
|
||||
sem := make(chan struct{}, maxConcurrentFetches)
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}}
|
||||
client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}}
|
||||
|
||||
for _, chn := range upstreams {
|
||||
wg.Add(1)
|
||||
go func(chItem dto.UpstreamDTO) {
|
||||
defer wg.Done()
|
||||
for _, chn := range upstreams {
|
||||
wg.Add(1)
|
||||
go func(chItem dto.UpstreamDTO) {
|
||||
defer wg.Done()
|
||||
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
|
||||
endpoint := chItem.Endpoint
|
||||
if endpoint == "" {
|
||||
endpoint = defaultEndpoint
|
||||
} else if !strings.HasPrefix(endpoint, "/") {
|
||||
endpoint = "/" + endpoint
|
||||
}
|
||||
fullURL := chItem.BaseURL + endpoint
|
||||
endpoint := chItem.Endpoint
|
||||
if endpoint == "" {
|
||||
endpoint = defaultEndpoint
|
||||
} else if !strings.HasPrefix(endpoint, "/") {
|
||||
endpoint = "/" + endpoint
|
||||
}
|
||||
fullURL := chItem.BaseURL + endpoint
|
||||
|
||||
uniqueName := chItem.Name
|
||||
if chItem.ID != 0 {
|
||||
uniqueName = fmt.Sprintf("%s(%d)", chItem.Name, chItem.ID)
|
||||
}
|
||||
uniqueName := chItem.Name
|
||||
if chItem.ID != 0 {
|
||||
uniqueName = fmt.Sprintf("%s(%d)", chItem.Name, chItem.ID)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration(req.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration(req.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
|
||||
if err != nil {
|
||||
common.LogWarn(c.Request.Context(), "build request failed: "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||
return
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
|
||||
if err != nil {
|
||||
logger.LogWarn(c.Request.Context(), "build request failed: "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
common.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
common.LogWarn(c.Request.Context(), "non-200 from "+chItem.Name+": "+resp.Status)
|
||||
ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
|
||||
return
|
||||
}
|
||||
// 兼容两种上游接口格式:
|
||||
// type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price
|
||||
// type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
|
||||
var body struct {
|
||||
Success bool `json:"success"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
logger.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.LogWarn(c.Request.Context(), "non-200 from "+chItem.Name+": "+resp.Status)
|
||||
ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
|
||||
return
|
||||
}
|
||||
// 兼容两种上游接口格式:
|
||||
// type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price
|
||||
// type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
|
||||
var body struct {
|
||||
Success bool `json:"success"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
common.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||
return
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
logger.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||
return
|
||||
}
|
||||
|
||||
if !body.Success {
|
||||
ch <- upstreamResult{Name: uniqueName, Err: body.Message}
|
||||
return
|
||||
}
|
||||
if !body.Success {
|
||||
ch <- upstreamResult{Name: uniqueName, Err: body.Message}
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试按 type1 解析
|
||||
var type1Data map[string]any
|
||||
if err := json.Unmarshal(body.Data, &type1Data); err == nil {
|
||||
// 如果包含至少一个 ratioTypes 字段,则认为是 type1
|
||||
isType1 := false
|
||||
for _, rt := range ratioTypes {
|
||||
if _, ok := type1Data[rt]; ok {
|
||||
isType1 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isType1 {
|
||||
ch <- upstreamResult{Name: uniqueName, Data: type1Data}
|
||||
return
|
||||
}
|
||||
}
|
||||
// 尝试按 type1 解析
|
||||
var type1Data map[string]any
|
||||
if err := json.Unmarshal(body.Data, &type1Data); err == nil {
|
||||
// 如果包含至少一个 ratioTypes 字段,则认为是 type1
|
||||
isType1 := false
|
||||
for _, rt := range ratioTypes {
|
||||
if _, ok := type1Data[rt]; ok {
|
||||
isType1 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isType1 {
|
||||
ch <- upstreamResult{Name: uniqueName, Data: type1Data}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是 type1,则尝试按 type2 (/api/pricing) 解析
|
||||
var pricingItems []struct {
|
||||
ModelName string `json:"model_name"`
|
||||
QuotaType int `json:"quota_type"`
|
||||
ModelRatio float64 `json:"model_ratio"`
|
||||
ModelPrice float64 `json:"model_price"`
|
||||
CompletionRatio float64 `json:"completion_ratio"`
|
||||
}
|
||||
if err := json.Unmarshal(body.Data, &pricingItems); err != nil {
|
||||
common.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: "无法解析上游返回数据"}
|
||||
return
|
||||
}
|
||||
// 如果不是 type1,则尝试按 type2 (/api/pricing) 解析
|
||||
var pricingItems []struct {
|
||||
ModelName string `json:"model_name"`
|
||||
QuotaType int `json:"quota_type"`
|
||||
ModelRatio float64 `json:"model_ratio"`
|
||||
ModelPrice float64 `json:"model_price"`
|
||||
CompletionRatio float64 `json:"completion_ratio"`
|
||||
}
|
||||
if err := json.Unmarshal(body.Data, &pricingItems); err != nil {
|
||||
logger.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error())
|
||||
ch <- upstreamResult{Name: uniqueName, Err: "无法解析上游返回数据"}
|
||||
return
|
||||
}
|
||||
|
||||
modelRatioMap := make(map[string]float64)
|
||||
completionRatioMap := make(map[string]float64)
|
||||
modelPriceMap := make(map[string]float64)
|
||||
modelRatioMap := make(map[string]float64)
|
||||
completionRatioMap := make(map[string]float64)
|
||||
modelPriceMap := make(map[string]float64)
|
||||
|
||||
for _, item := range pricingItems {
|
||||
if item.QuotaType == 1 {
|
||||
modelPriceMap[item.ModelName] = item.ModelPrice
|
||||
} else {
|
||||
modelRatioMap[item.ModelName] = item.ModelRatio
|
||||
// completionRatio 可能为 0,此时也直接赋值,保持与上游一致
|
||||
completionRatioMap[item.ModelName] = item.CompletionRatio
|
||||
}
|
||||
}
|
||||
for _, item := range pricingItems {
|
||||
if item.QuotaType == 1 {
|
||||
modelPriceMap[item.ModelName] = item.ModelPrice
|
||||
} else {
|
||||
modelRatioMap[item.ModelName] = item.ModelRatio
|
||||
// completionRatio 可能为 0,此时也直接赋值,保持与上游一致
|
||||
completionRatioMap[item.ModelName] = item.CompletionRatio
|
||||
}
|
||||
}
|
||||
|
||||
converted := make(map[string]any)
|
||||
converted := make(map[string]any)
|
||||
|
||||
if len(modelRatioMap) > 0 {
|
||||
ratioAny := make(map[string]any, len(modelRatioMap))
|
||||
for k, v := range modelRatioMap {
|
||||
ratioAny[k] = v
|
||||
}
|
||||
converted["model_ratio"] = ratioAny
|
||||
}
|
||||
if len(modelRatioMap) > 0 {
|
||||
ratioAny := make(map[string]any, len(modelRatioMap))
|
||||
for k, v := range modelRatioMap {
|
||||
ratioAny[k] = v
|
||||
}
|
||||
converted["model_ratio"] = ratioAny
|
||||
}
|
||||
|
||||
if len(completionRatioMap) > 0 {
|
||||
compAny := make(map[string]any, len(completionRatioMap))
|
||||
for k, v := range completionRatioMap {
|
||||
compAny[k] = v
|
||||
}
|
||||
converted["completion_ratio"] = compAny
|
||||
}
|
||||
if len(completionRatioMap) > 0 {
|
||||
compAny := make(map[string]any, len(completionRatioMap))
|
||||
for k, v := range completionRatioMap {
|
||||
compAny[k] = v
|
||||
}
|
||||
converted["completion_ratio"] = compAny
|
||||
}
|
||||
|
||||
if len(modelPriceMap) > 0 {
|
||||
priceAny := make(map[string]any, len(modelPriceMap))
|
||||
for k, v := range modelPriceMap {
|
||||
priceAny[k] = v
|
||||
}
|
||||
converted["model_price"] = priceAny
|
||||
}
|
||||
if len(modelPriceMap) > 0 {
|
||||
priceAny := make(map[string]any, len(modelPriceMap))
|
||||
for k, v := range modelPriceMap {
|
||||
priceAny[k] = v
|
||||
}
|
||||
converted["model_price"] = priceAny
|
||||
}
|
||||
|
||||
ch <- upstreamResult{Name: uniqueName, Data: converted}
|
||||
}(chn)
|
||||
}
|
||||
ch <- upstreamResult{Name: uniqueName, Data: converted}
|
||||
}(chn)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
localData := ratio_setting.GetExposedData()
|
||||
localData := ratio_setting.GetExposedData()
|
||||
|
||||
var testResults []dto.TestResult
|
||||
var successfulChannels []struct {
|
||||
name string
|
||||
data map[string]any
|
||||
}
|
||||
var testResults []dto.TestResult
|
||||
var successfulChannels []struct {
|
||||
name string
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
for r := range ch {
|
||||
if r.Err != "" {
|
||||
testResults = append(testResults, dto.TestResult{
|
||||
Name: r.Name,
|
||||
Status: "error",
|
||||
Error: r.Err,
|
||||
})
|
||||
} else {
|
||||
testResults = append(testResults, dto.TestResult{
|
||||
Name: r.Name,
|
||||
Status: "success",
|
||||
})
|
||||
successfulChannels = append(successfulChannels, struct {
|
||||
name string
|
||||
data map[string]any
|
||||
}{name: r.Name, data: r.Data})
|
||||
}
|
||||
}
|
||||
for r := range ch {
|
||||
if r.Err != "" {
|
||||
testResults = append(testResults, dto.TestResult{
|
||||
Name: r.Name,
|
||||
Status: "error",
|
||||
Error: r.Err,
|
||||
})
|
||||
} else {
|
||||
testResults = append(testResults, dto.TestResult{
|
||||
Name: r.Name,
|
||||
Status: "success",
|
||||
})
|
||||
successfulChannels = append(successfulChannels, struct {
|
||||
name string
|
||||
data map[string]any
|
||||
}{name: r.Name, data: r.Data})
|
||||
}
|
||||
}
|
||||
|
||||
differences := buildDifferences(localData, successfulChannels)
|
||||
differences := buildDifferences(localData, successfulChannels)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"differences": differences,
|
||||
"test_results": testResults,
|
||||
},
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"differences": differences,
|
||||
"test_results": testResults,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func buildDifferences(localData map[string]any, successfulChannels []struct {
|
||||
name string
|
||||
data map[string]any
|
||||
name string
|
||||
data map[string]any
|
||||
}) map[string]map[string]dto.DifferenceItem {
|
||||
differences := make(map[string]map[string]dto.DifferenceItem)
|
||||
differences := make(map[string]map[string]dto.DifferenceItem)
|
||||
|
||||
allModels := make(map[string]struct{})
|
||||
|
||||
for _, ratioType := range ratioTypes {
|
||||
if localRatioAny, ok := localData[ratioType]; ok {
|
||||
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||||
for modelName := range localRatio {
|
||||
allModels[modelName] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, channel := range successfulChannels {
|
||||
for _, ratioType := range ratioTypes {
|
||||
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||||
for modelName := range upstreamRatio {
|
||||
allModels[modelName] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allModels := make(map[string]struct{})
|
||||
|
||||
confidenceMap := make(map[string]map[string]bool)
|
||||
|
||||
// 预处理阶段:检查pricing接口的可信度
|
||||
for _, channel := range successfulChannels {
|
||||
confidenceMap[channel.name] = make(map[string]bool)
|
||||
|
||||
modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any)
|
||||
completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any)
|
||||
|
||||
if hasModelRatio && hasCompletionRatio {
|
||||
// 遍历所有模型,检查是否满足不可信条件
|
||||
for modelName := range allModels {
|
||||
// 默认为可信
|
||||
confidenceMap[channel.name][modelName] = true
|
||||
|
||||
// 检查是否满足不可信条件:model_ratio为37.5且completion_ratio为1
|
||||
if modelRatioVal, ok := modelRatios[modelName]; ok {
|
||||
if completionRatioVal, ok := completionRatios[modelName]; ok {
|
||||
// 转换为float64进行比较
|
||||
if modelRatioFloat, ok := modelRatioVal.(float64); ok {
|
||||
if completionRatioFloat, ok := completionRatioVal.(float64); ok {
|
||||
if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 {
|
||||
confidenceMap[channel.name][modelName] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果不是从pricing接口获取的数据,则全部标记为可信
|
||||
for modelName := range allModels {
|
||||
confidenceMap[channel.name][modelName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ratioType := range ratioTypes {
|
||||
if localRatioAny, ok := localData[ratioType]; ok {
|
||||
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||||
for modelName := range localRatio {
|
||||
allModels[modelName] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for modelName := range allModels {
|
||||
for _, ratioType := range ratioTypes {
|
||||
var localValue interface{} = nil
|
||||
if localRatioAny, ok := localData[ratioType]; ok {
|
||||
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||||
if val, exists := localRatio[modelName]; exists {
|
||||
localValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, channel := range successfulChannels {
|
||||
for _, ratioType := range ratioTypes {
|
||||
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||||
for modelName := range upstreamRatio {
|
||||
allModels[modelName] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upstreamValues := make(map[string]interface{})
|
||||
confidenceValues := make(map[string]bool)
|
||||
hasUpstreamValue := false
|
||||
hasDifference := false
|
||||
confidenceMap := make(map[string]map[string]bool)
|
||||
|
||||
for _, channel := range successfulChannels {
|
||||
var upstreamValue interface{} = nil
|
||||
|
||||
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||||
if val, exists := upstreamRatio[modelName]; exists {
|
||||
upstreamValue = val
|
||||
hasUpstreamValue = true
|
||||
|
||||
if localValue != nil && localValue != val {
|
||||
hasDifference = true
|
||||
} else if localValue == val {
|
||||
upstreamValue = "same"
|
||||
}
|
||||
}
|
||||
}
|
||||
if upstreamValue == nil && localValue == nil {
|
||||
upstreamValue = "same"
|
||||
}
|
||||
|
||||
if localValue == nil && upstreamValue != nil && upstreamValue != "same" {
|
||||
hasDifference = true
|
||||
}
|
||||
|
||||
upstreamValues[channel.name] = upstreamValue
|
||||
|
||||
confidenceValues[channel.name] = confidenceMap[channel.name][modelName]
|
||||
}
|
||||
// 预处理阶段:检查pricing接口的可信度
|
||||
for _, channel := range successfulChannels {
|
||||
confidenceMap[channel.name] = make(map[string]bool)
|
||||
|
||||
shouldInclude := false
|
||||
|
||||
if localValue != nil {
|
||||
if hasDifference {
|
||||
shouldInclude = true
|
||||
}
|
||||
} else {
|
||||
if hasUpstreamValue {
|
||||
shouldInclude = true
|
||||
}
|
||||
}
|
||||
modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any)
|
||||
completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any)
|
||||
|
||||
if shouldInclude {
|
||||
if differences[modelName] == nil {
|
||||
differences[modelName] = make(map[string]dto.DifferenceItem)
|
||||
}
|
||||
differences[modelName][ratioType] = dto.DifferenceItem{
|
||||
Current: localValue,
|
||||
Upstreams: upstreamValues,
|
||||
Confidence: confidenceValues,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasModelRatio && hasCompletionRatio {
|
||||
// 遍历所有模型,检查是否满足不可信条件
|
||||
for modelName := range allModels {
|
||||
// 默认为可信
|
||||
confidenceMap[channel.name][modelName] = true
|
||||
|
||||
channelHasDiff := make(map[string]bool)
|
||||
for _, ratioMap := range differences {
|
||||
for _, item := range ratioMap {
|
||||
for chName, val := range item.Upstreams {
|
||||
if val != nil && val != "same" {
|
||||
channelHasDiff[chName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查是否满足不可信条件:model_ratio为37.5且completion_ratio为1
|
||||
if modelRatioVal, ok := modelRatios[modelName]; ok {
|
||||
if completionRatioVal, ok := completionRatios[modelName]; ok {
|
||||
// 转换为float64进行比较
|
||||
if modelRatioFloat, ok := modelRatioVal.(float64); ok {
|
||||
if completionRatioFloat, ok := completionRatioVal.(float64); ok {
|
||||
if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 {
|
||||
confidenceMap[channel.name][modelName] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果不是从pricing接口获取的数据,则全部标记为可信
|
||||
for modelName := range allModels {
|
||||
confidenceMap[channel.name][modelName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for modelName, ratioMap := range differences {
|
||||
for ratioType, item := range ratioMap {
|
||||
for chName := range item.Upstreams {
|
||||
if !channelHasDiff[chName] {
|
||||
delete(item.Upstreams, chName)
|
||||
delete(item.Confidence, chName)
|
||||
}
|
||||
}
|
||||
for modelName := range allModels {
|
||||
for _, ratioType := range ratioTypes {
|
||||
var localValue interface{} = nil
|
||||
if localRatioAny, ok := localData[ratioType]; ok {
|
||||
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||||
if val, exists := localRatio[modelName]; exists {
|
||||
localValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allSame := true
|
||||
for _, v := range item.Upstreams {
|
||||
if v != "same" {
|
||||
allSame = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(item.Upstreams) == 0 || allSame {
|
||||
delete(ratioMap, ratioType)
|
||||
} else {
|
||||
differences[modelName][ratioType] = item
|
||||
}
|
||||
}
|
||||
upstreamValues := make(map[string]interface{})
|
||||
confidenceValues := make(map[string]bool)
|
||||
hasUpstreamValue := false
|
||||
hasDifference := false
|
||||
|
||||
if len(ratioMap) == 0 {
|
||||
delete(differences, modelName)
|
||||
}
|
||||
}
|
||||
for _, channel := range successfulChannels {
|
||||
var upstreamValue interface{} = nil
|
||||
|
||||
return differences
|
||||
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||||
if val, exists := upstreamRatio[modelName]; exists {
|
||||
upstreamValue = val
|
||||
hasUpstreamValue = true
|
||||
|
||||
if localValue != nil && localValue != val {
|
||||
hasDifference = true
|
||||
} else if localValue == val {
|
||||
upstreamValue = "same"
|
||||
}
|
||||
}
|
||||
}
|
||||
if upstreamValue == nil && localValue == nil {
|
||||
upstreamValue = "same"
|
||||
}
|
||||
|
||||
if localValue == nil && upstreamValue != nil && upstreamValue != "same" {
|
||||
hasDifference = true
|
||||
}
|
||||
|
||||
upstreamValues[channel.name] = upstreamValue
|
||||
|
||||
confidenceValues[channel.name] = confidenceMap[channel.name][modelName]
|
||||
}
|
||||
|
||||
shouldInclude := false
|
||||
|
||||
if localValue != nil {
|
||||
if hasDifference {
|
||||
shouldInclude = true
|
||||
}
|
||||
} else {
|
||||
if hasUpstreamValue {
|
||||
shouldInclude = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInclude {
|
||||
if differences[modelName] == nil {
|
||||
differences[modelName] = make(map[string]dto.DifferenceItem)
|
||||
}
|
||||
differences[modelName][ratioType] = dto.DifferenceItem{
|
||||
Current: localValue,
|
||||
Upstreams: upstreamValues,
|
||||
Confidence: confidenceValues,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channelHasDiff := make(map[string]bool)
|
||||
for _, ratioMap := range differences {
|
||||
for _, item := range ratioMap {
|
||||
for chName, val := range item.Upstreams {
|
||||
if val != nil && val != "same" {
|
||||
channelHasDiff[chName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for modelName, ratioMap := range differences {
|
||||
for ratioType, item := range ratioMap {
|
||||
for chName := range item.Upstreams {
|
||||
if !channelHasDiff[chName] {
|
||||
delete(item.Upstreams, chName)
|
||||
delete(item.Confidence, chName)
|
||||
}
|
||||
}
|
||||
|
||||
allSame := true
|
||||
for _, v := range item.Upstreams {
|
||||
if v != "same" {
|
||||
allSame = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(item.Upstreams) == 0 || allSame {
|
||||
delete(ratioMap, ratioType)
|
||||
} else {
|
||||
differences[modelName][ratioType] = item
|
||||
}
|
||||
}
|
||||
|
||||
if len(ratioMap) == 0 {
|
||||
delete(differences, modelName)
|
||||
}
|
||||
}
|
||||
|
||||
return differences
|
||||
}
|
||||
|
||||
func GetSyncableChannels(c *gin.Context) {
|
||||
channels, err := model.GetAllChannels(0, 0, true, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
channels, err := model.GetAllChannels(0, 0, true, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var syncableChannels []dto.SyncableChannel
|
||||
for _, channel := range channels {
|
||||
if channel.GetBaseURL() != "" {
|
||||
syncableChannels = append(syncableChannels, dto.SyncableChannel{
|
||||
ID: channel.Id,
|
||||
Name: channel.Name,
|
||||
BaseURL: channel.GetBaseURL(),
|
||||
Status: channel.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
var syncableChannels []dto.SyncableChannel
|
||||
for _, channel := range channels {
|
||||
if channel.GetBaseURL() != "" {
|
||||
syncableChannels = append(syncableChannels, dto.SyncableChannel{
|
||||
ID: channel.Id,
|
||||
Name: channel.Name,
|
||||
BaseURL: channel.GetBaseURL(),
|
||||
Status: channel.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": syncableChannels,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": syncableChannels,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,21 +2,23 @@ package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
constant2 "one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/middleware"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
@@ -24,100 +26,168 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func relayHandler(c *gin.Context, relayMode int) *types.NewAPIError {
|
||||
func relayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewAPIError {
|
||||
var err *types.NewAPIError
|
||||
switch relayMode {
|
||||
switch info.RelayMode {
|
||||
case relayconstant.RelayModeImagesGenerations, relayconstant.RelayModeImagesEdits:
|
||||
err = relay.ImageHelper(c)
|
||||
err = relay.ImageHelper(c, info)
|
||||
case relayconstant.RelayModeAudioSpeech:
|
||||
fallthrough
|
||||
case relayconstant.RelayModeAudioTranslation:
|
||||
fallthrough
|
||||
case relayconstant.RelayModeAudioTranscription:
|
||||
err = relay.AudioHelper(c)
|
||||
err = relay.AudioHelper(c, info)
|
||||
case relayconstant.RelayModeRerank:
|
||||
err = relay.RerankHelper(c, relayMode)
|
||||
err = relay.RerankHelper(c, info)
|
||||
case relayconstant.RelayModeEmbeddings:
|
||||
err = relay.EmbeddingHelper(c)
|
||||
err = relay.EmbeddingHelper(c, info)
|
||||
case relayconstant.RelayModeResponses:
|
||||
err = relay.ResponsesHelper(c)
|
||||
case relayconstant.RelayModeGemini:
|
||||
err = relay.GeminiHelper(c)
|
||||
err = relay.ResponsesHelper(c, info)
|
||||
default:
|
||||
err = relay.TextHelper(c)
|
||||
err = relay.TextHelper(c, info)
|
||||
}
|
||||
|
||||
if constant2.ErrorLogEnabled && err != nil && types.IsRecordErrorLog(err) {
|
||||
// 保存错误日志到mysql中
|
||||
userId := c.GetInt("id")
|
||||
tokenName := c.GetString("token_name")
|
||||
modelName := c.GetString("original_model")
|
||||
tokenId := c.GetInt("token_id")
|
||||
userGroup := c.GetString("group")
|
||||
channelId := c.GetInt("channel_id")
|
||||
other := make(map[string]interface{})
|
||||
other["error_type"] = err.GetErrorType()
|
||||
other["error_code"] = err.GetErrorCode()
|
||||
other["status_code"] = err.StatusCode
|
||||
other["channel_id"] = channelId
|
||||
other["channel_name"] = c.GetString("channel_name")
|
||||
other["channel_type"] = c.GetInt("channel_type")
|
||||
adminInfo := make(map[string]interface{})
|
||||
adminInfo["use_channel"] = c.GetStringSlice("use_channel")
|
||||
isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey)
|
||||
if isMultiKey {
|
||||
adminInfo["is_multi_key"] = true
|
||||
adminInfo["multi_key_index"] = common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex)
|
||||
}
|
||||
other["admin_info"] = adminInfo
|
||||
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveError(), tokenId, 0, false, userGroup, other)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func Relay(c *gin.Context) {
|
||||
relayMode := relayconstant.Path2RelayMode(c.Request.URL.Path)
|
||||
func geminiRelayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewAPIError {
|
||||
var err *types.NewAPIError
|
||||
if strings.Contains(c.Request.URL.Path, "embed") {
|
||||
err = relay.GeminiEmbeddingHandler(c, info)
|
||||
} else {
|
||||
err = relay.GeminiHelper(c, info)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Relay(c *gin.Context, relayFormat types.RelayFormat) {
|
||||
|
||||
requestId := c.GetString(common.RequestIdKey)
|
||||
group := c.GetString("group")
|
||||
originalModel := c.GetString("original_model")
|
||||
var newAPIError *types.NewAPIError
|
||||
group := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
originalModel := common.GetContextKeyString(c, constant.ContextKeyOriginalModel)
|
||||
|
||||
var (
|
||||
newAPIError *types.NewAPIError
|
||||
ws *websocket.Conn
|
||||
)
|
||||
|
||||
if relayFormat == types.RelayFormatOpenAIRealtime {
|
||||
var err error
|
||||
ws, err = upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
helper.WssError(c, ws, types.NewError(err, types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()).ToOpenAIError())
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if newAPIError != nil {
|
||||
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
|
||||
switch relayFormat {
|
||||
case types.RelayFormatOpenAIRealtime:
|
||||
helper.WssError(c, ws, newAPIError.ToOpenAIError())
|
||||
case types.RelayFormatClaude:
|
||||
c.JSON(newAPIError.StatusCode, gin.H{
|
||||
"type": "error",
|
||||
"error": newAPIError.ToClaudeError(),
|
||||
})
|
||||
default:
|
||||
c.JSON(newAPIError.StatusCode, gin.H{
|
||||
"error": newAPIError.ToOpenAIError(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
request, err := helper.GetAndValidateRequest(c, relayFormat)
|
||||
if err != nil {
|
||||
newAPIError = types.NewError(err, types.ErrorCodeInvalidRequest)
|
||||
return
|
||||
}
|
||||
|
||||
relayInfo, err := relaycommon.GenRelayInfo(c, relayFormat, request, ws)
|
||||
if err != nil {
|
||||
newAPIError = types.NewError(err, types.ErrorCodeGenRelayInfoFailed)
|
||||
return
|
||||
}
|
||||
|
||||
meta := request.GetTokenCountMeta()
|
||||
|
||||
if setting.ShouldCheckPromptSensitive() {
|
||||
contains, words := service.CheckSensitiveText(meta.CombineText)
|
||||
if contains {
|
||||
logger.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(words, ", ")))
|
||||
newAPIError = types.NewError(err, types.ErrorCodeSensitiveWordsDetected)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tokens, err := service.CountRequestToken(c, meta, relayInfo)
|
||||
if err != nil {
|
||||
newAPIError = types.NewError(err, types.ErrorCodeCountTokenFailed)
|
||||
return
|
||||
}
|
||||
|
||||
relayInfo.SetPromptTokens(tokens)
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, relayInfo, tokens, meta)
|
||||
if err != nil {
|
||||
newAPIError = types.NewError(err, types.ErrorCodeModelPriceError)
|
||||
return
|
||||
}
|
||||
|
||||
// common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta)
|
||||
|
||||
preConsumedQuota, newAPIError := service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
|
||||
if newAPIError != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Only return quota if downstream failed and quota was actually pre-consumed
|
||||
if newAPIError != nil && preConsumedQuota != 0 {
|
||||
service.ReturnPreConsumedQuota(c, relayInfo, preConsumedQuota)
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i <= common.RetryTimes; i++ {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
logger.LogError(c, err.Error())
|
||||
newAPIError = err
|
||||
break
|
||||
}
|
||||
|
||||
newAPIError = relayRequest(c, relayMode, channel)
|
||||
addUsedChannel(c, channel.Id)
|
||||
requestBody, _ := common.GetRequestBody(c)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
|
||||
if newAPIError == nil {
|
||||
return // 成功处理请求,直接返回
|
||||
switch relayFormat {
|
||||
case types.RelayFormatOpenAIRealtime:
|
||||
newAPIError = relay.WssHelper(c, relayInfo)
|
||||
case types.RelayFormatClaude:
|
||||
newAPIError = relay.ClaudeHelper(c, relayInfo)
|
||||
case types.RelayFormatGemini:
|
||||
newAPIError = geminiRelayHandler(c, relayInfo)
|
||||
default:
|
||||
newAPIError = relayHandler(c, relayInfo)
|
||||
}
|
||||
|
||||
go processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
if newAPIError == nil {
|
||||
return
|
||||
}
|
||||
|
||||
processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
|
||||
if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useChannel := c.GetStringSlice("use_channel")
|
||||
if len(useChannel) > 1 {
|
||||
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]"))
|
||||
common.LogInfo(c, retryLogStr)
|
||||
}
|
||||
|
||||
if newAPIError != nil {
|
||||
//if newAPIError.StatusCode == http.StatusTooManyRequests {
|
||||
// common.LogError(c, fmt.Sprintf("origin 429 error: %s", newAPIError.Error()))
|
||||
// newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
|
||||
//}
|
||||
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
|
||||
c.JSON(newAPIError.StatusCode, gin.H{
|
||||
"error": newAPIError.ToOpenAIError(),
|
||||
})
|
||||
logger.LogInfo(c, retryLogStr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,122 +198,6 @@ var upgrader = websocket.Upgrader{
|
||||
},
|
||||
}
|
||||
|
||||
func WssRelay(c *gin.Context) {
|
||||
// 将 HTTP 连接升级为 WebSocket 连接
|
||||
|
||||
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
defer ws.Close()
|
||||
|
||||
if err != nil {
|
||||
helper.WssError(c, ws, types.NewError(err, types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()).ToOpenAIError())
|
||||
return
|
||||
}
|
||||
|
||||
relayMode := relayconstant.Path2RelayMode(c.Request.URL.Path)
|
||||
requestId := c.GetString(common.RequestIdKey)
|
||||
group := c.GetString("group")
|
||||
//wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01
|
||||
originalModel := c.GetString("original_model")
|
||||
var newAPIError *types.NewAPIError
|
||||
|
||||
for i := 0; i <= common.RetryTimes; i++ {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
newAPIError = err
|
||||
break
|
||||
}
|
||||
|
||||
newAPIError = wssRequest(c, ws, relayMode, channel)
|
||||
|
||||
if newAPIError == nil {
|
||||
return // 成功处理请求,直接返回
|
||||
}
|
||||
|
||||
go processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
|
||||
if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
|
||||
break
|
||||
}
|
||||
}
|
||||
useChannel := c.GetStringSlice("use_channel")
|
||||
if len(useChannel) > 1 {
|
||||
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]"))
|
||||
common.LogInfo(c, retryLogStr)
|
||||
}
|
||||
|
||||
if newAPIError != nil {
|
||||
//if newAPIError.StatusCode == http.StatusTooManyRequests {
|
||||
// newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
|
||||
//}
|
||||
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
|
||||
helper.WssError(c, ws, newAPIError.ToOpenAIError())
|
||||
}
|
||||
}
|
||||
|
||||
func RelayClaude(c *gin.Context) {
|
||||
//relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
||||
requestId := c.GetString(common.RequestIdKey)
|
||||
group := c.GetString("group")
|
||||
originalModel := c.GetString("original_model")
|
||||
var newAPIError *types.NewAPIError
|
||||
|
||||
for i := 0; i <= common.RetryTimes; i++ {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
newAPIError = err
|
||||
break
|
||||
}
|
||||
|
||||
newAPIError = claudeRequest(c, channel)
|
||||
|
||||
if newAPIError == nil {
|
||||
return // 成功处理请求,直接返回
|
||||
}
|
||||
|
||||
go processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
|
||||
if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
|
||||
break
|
||||
}
|
||||
}
|
||||
useChannel := c.GetStringSlice("use_channel")
|
||||
if len(useChannel) > 1 {
|
||||
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]"))
|
||||
common.LogInfo(c, retryLogStr)
|
||||
}
|
||||
|
||||
if newAPIError != nil {
|
||||
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
|
||||
c.JSON(newAPIError.StatusCode, gin.H{
|
||||
"type": "error",
|
||||
"error": newAPIError.ToClaudeError(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func relayRequest(c *gin.Context, relayMode int, channel *model.Channel) *types.NewAPIError {
|
||||
addUsedChannel(c, channel.Id)
|
||||
requestBody, _ := common.GetRequestBody(c)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
return relayHandler(c, relayMode)
|
||||
}
|
||||
|
||||
func wssRequest(c *gin.Context, ws *websocket.Conn, relayMode int, channel *model.Channel) *types.NewAPIError {
|
||||
addUsedChannel(c, channel.Id)
|
||||
requestBody, _ := common.GetRequestBody(c)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
return relay.WssHelper(c, ws)
|
||||
}
|
||||
|
||||
func claudeRequest(c *gin.Context, channel *model.Channel) *types.NewAPIError {
|
||||
addUsedChannel(c, channel.Id)
|
||||
requestBody, _ := common.GetRequestBody(c)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
return relay.ClaudeHelper(c)
|
||||
}
|
||||
|
||||
func addUsedChannel(c *gin.Context, channelId int) {
|
||||
useChannel := c.GetStringSlice("use_channel")
|
||||
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
|
||||
@@ -266,10 +220,10 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
|
||||
}
|
||||
channel, selectGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
|
||||
if err != nil {
|
||||
return nil, types.NewError(errors.New(fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error())), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
|
||||
return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
if channel == nil {
|
||||
return nil, types.NewError(errors.New(fmt.Sprintf("分组 %s 下模型 %s 的可用渠道不存在(数据库一致性已被破坏,retry)", selectGroup, originalModel)), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
|
||||
return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(数据库一致性已被破坏,retry)", selectGroup, originalModel), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
newAPIError := middleware.SetupContextForSelectedChannel(c, channel, originalModel)
|
||||
if newAPIError != nil {
|
||||
@@ -308,10 +262,6 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
|
||||
return true
|
||||
}
|
||||
if openaiErr.StatusCode == http.StatusBadRequest {
|
||||
channelType := c.GetInt("channel_type")
|
||||
if channelType == constant.ChannelTypeAnthropic {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if openaiErr.StatusCode == 408 {
|
||||
@@ -325,44 +275,84 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
|
||||
}
|
||||
|
||||
func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) {
|
||||
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
|
||||
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
|
||||
common.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
|
||||
if service.ShouldDisableChannel(channelError.ChannelId, err) && channelError.AutoBan {
|
||||
service.DisableChannel(channelError, err.Error())
|
||||
logger.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
|
||||
|
||||
gopool.Go(func() {
|
||||
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
|
||||
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
|
||||
if service.ShouldDisableChannel(channelError.ChannelId, err) && channelError.AutoBan {
|
||||
service.DisableChannel(channelError, err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
if constant.ErrorLogEnabled && types.IsRecordErrorLog(err) {
|
||||
// 保存错误日志到mysql中
|
||||
userId := c.GetInt("id")
|
||||
tokenName := c.GetString("token_name")
|
||||
modelName := c.GetString("original_model")
|
||||
tokenId := c.GetInt("token_id")
|
||||
userGroup := c.GetString("group")
|
||||
channelId := c.GetInt("channel_id")
|
||||
other := make(map[string]interface{})
|
||||
other["error_type"] = err.GetErrorType()
|
||||
other["error_code"] = err.GetErrorCode()
|
||||
other["status_code"] = err.StatusCode
|
||||
other["channel_id"] = channelId
|
||||
other["channel_name"] = c.GetString("channel_name")
|
||||
other["channel_type"] = c.GetInt("channel_type")
|
||||
adminInfo := make(map[string]interface{})
|
||||
adminInfo["use_channel"] = c.GetStringSlice("use_channel")
|
||||
isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey)
|
||||
if isMultiKey {
|
||||
adminInfo["is_multi_key"] = true
|
||||
adminInfo["multi_key_index"] = common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex)
|
||||
}
|
||||
other["admin_info"] = adminInfo
|
||||
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveError(), tokenId, 0, false, userGroup, other)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func RelayMidjourney(c *gin.Context) {
|
||||
relayMode := c.GetInt("relay_mode")
|
||||
var err *dto.MidjourneyResponse
|
||||
switch relayMode {
|
||||
relayInfo, err := relaycommon.GenRelayInfo(c, types.RelayFormatMjProxy, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"description": fmt.Sprintf("failed to generate relay info: %s", err.Error()),
|
||||
"type": "upstream_error",
|
||||
"code": 4,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var mjErr *dto.MidjourneyResponse
|
||||
switch relayInfo.RelayMode {
|
||||
case relayconstant.RelayModeMidjourneyNotify:
|
||||
err = relay.RelayMidjourneyNotify(c)
|
||||
mjErr = relay.RelayMidjourneyNotify(c)
|
||||
case relayconstant.RelayModeMidjourneyTaskFetch, relayconstant.RelayModeMidjourneyTaskFetchByCondition:
|
||||
err = relay.RelayMidjourneyTask(c, relayMode)
|
||||
mjErr = relay.RelayMidjourneyTask(c, relayInfo.RelayMode)
|
||||
case relayconstant.RelayModeMidjourneyTaskImageSeed:
|
||||
err = relay.RelayMidjourneyTaskImageSeed(c)
|
||||
mjErr = relay.RelayMidjourneyTaskImageSeed(c)
|
||||
case relayconstant.RelayModeSwapFace:
|
||||
err = relay.RelaySwapFace(c)
|
||||
mjErr = relay.RelaySwapFace(c, relayInfo)
|
||||
default:
|
||||
err = relay.RelayMidjourneySubmit(c, relayMode)
|
||||
mjErr = relay.RelayMidjourneySubmit(c, relayInfo)
|
||||
}
|
||||
//err = relayMidjourneySubmit(c, relayMode)
|
||||
log.Println(err)
|
||||
if err != nil {
|
||||
log.Println(mjErr)
|
||||
if mjErr != nil {
|
||||
statusCode := http.StatusBadRequest
|
||||
if err.Code == 30 {
|
||||
err.Result = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
||||
if mjErr.Code == 30 {
|
||||
mjErr.Result = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
||||
statusCode = http.StatusTooManyRequests
|
||||
}
|
||||
c.JSON(statusCode, gin.H{
|
||||
"description": fmt.Sprintf("%s %s", err.Description, err.Result),
|
||||
"description": fmt.Sprintf("%s %s", mjErr.Description, mjErr.Result),
|
||||
"type": "upstream_error",
|
||||
"code": err.Code,
|
||||
"code": mjErr.Code,
|
||||
})
|
||||
channelId := c.GetInt("channel_id")
|
||||
common.LogError(c, fmt.Sprintf("relay error (channel #%d, status code %d): %s", channelId, statusCode, fmt.Sprintf("%s %s", err.Description, err.Result)))
|
||||
logger.LogError(c, fmt.Sprintf("relay error (channel #%d, status code %d): %s", channelId, statusCode, fmt.Sprintf("%s %s", mjErr.Description, mjErr.Result)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +394,7 @@ func RelayTask(c *gin.Context) {
|
||||
for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
|
||||
channel, newAPIError := getChannel(c, group, originalModel, i)
|
||||
if newAPIError != nil {
|
||||
common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", newAPIError.Error()))
|
||||
logger.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", newAPIError.Error()))
|
||||
taskErr = service.TaskErrorWrapperLocal(newAPIError.Err, "get_channel_failed", http.StatusInternalServerError)
|
||||
break
|
||||
}
|
||||
@@ -412,7 +402,7 @@ func RelayTask(c *gin.Context) {
|
||||
useChannel := c.GetStringSlice("use_channel")
|
||||
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
|
||||
c.Set("use_channel", useChannel)
|
||||
common.LogInfo(c, fmt.Sprintf("using channel #%d to retry (remain times %d)", channel.Id, i))
|
||||
logger.LogInfo(c, fmt.Sprintf("using channel #%d to retry (remain times %d)", channel.Id, i))
|
||||
//middleware.SetupContextForSelectedChannel(c, channel, originalModel)
|
||||
|
||||
requestBody, _ := common.GetRequestBody(c)
|
||||
@@ -422,7 +412,7 @@ func RelayTask(c *gin.Context) {
|
||||
useChannel := c.GetStringSlice("use_channel")
|
||||
if len(useChannel) > 1 {
|
||||
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]"))
|
||||
common.LogInfo(c, retryLogStr)
|
||||
logger.LogInfo(c, retryLogStr)
|
||||
}
|
||||
if taskErr != nil {
|
||||
if taskErr.StatusCode == http.StatusTooManyRequests {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
"sort"
|
||||
@@ -54,9 +55,9 @@ func UpdateTaskBulk() {
|
||||
"progress": "100%",
|
||||
})
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Fix null task_id task error: %v", err))
|
||||
logger.LogError(ctx, fmt.Sprintf("Fix null task_id task error: %v", err))
|
||||
} else {
|
||||
common.LogInfo(ctx, fmt.Sprintf("Fix null task_id task success: %v", nullTaskIds))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("Fix null task_id task success: %v", nullTaskIds))
|
||||
}
|
||||
}
|
||||
if len(taskChannelM) == 0 {
|
||||
@@ -86,14 +87,14 @@ func UpdateSunoTaskAll(ctx context.Context, taskChannelM map[int][]string, taskM
|
||||
for channelId, taskIds := range taskChannelM {
|
||||
err := updateSunoTaskAll(ctx, channelId, taskIds, taskM)
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("渠道 #%d 更新异步任务失败: %d", channelId, err.Error()))
|
||||
logger.LogError(ctx, fmt.Sprintf("渠道 #%d 更新异步任务失败: %d", channelId, err.Error()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, taskM map[string]*model.Task) error {
|
||||
common.LogInfo(ctx, fmt.Sprintf("渠道 #%d 未完成的任务有: %d", channelId, len(taskIds)))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("渠道 #%d 未完成的任务有: %d", channelId, len(taskIds)))
|
||||
if len(taskIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -106,7 +107,7 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
|
||||
"progress": "100%",
|
||||
})
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("UpdateMidjourneyTask error2: %v", err))
|
||||
common.SysLog(fmt.Sprintf("UpdateMidjourneyTask error2: %v", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -118,23 +119,23 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
|
||||
"ids": taskIds,
|
||||
})
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Get Task Do req error: %v", err))
|
||||
common.SysLog(fmt.Sprintf("Get Task Do req error: %v", err))
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
||||
return errors.New(fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Get Task parse body error: %v", err))
|
||||
common.SysLog(fmt.Sprintf("Get Task parse body error: %v", err))
|
||||
return err
|
||||
}
|
||||
var responseItems dto.TaskResponse[[]dto.SunoDataResponse]
|
||||
err = json.Unmarshal(responseBody, &responseItems)
|
||||
if err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Get Task parse body error2: %v, body: %s", err, string(responseBody)))
|
||||
logger.LogError(ctx, fmt.Sprintf("Get Task parse body error2: %v, body: %s", err, string(responseBody)))
|
||||
return err
|
||||
}
|
||||
if !responseItems.IsSuccess() {
|
||||
@@ -154,19 +155,19 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
|
||||
task.StartTime = lo.If(responseItem.StartTime != 0, responseItem.StartTime).Else(task.StartTime)
|
||||
task.FinishTime = lo.If(responseItem.FinishTime != 0, responseItem.FinishTime).Else(task.FinishTime)
|
||||
if responseItem.FailReason != "" || task.Status == model.TaskStatusFailure {
|
||||
common.LogInfo(ctx, task.TaskID+" 构建失败,"+task.FailReason)
|
||||
logger.LogInfo(ctx, task.TaskID+" 构建失败,"+task.FailReason)
|
||||
task.Progress = "100%"
|
||||
//err = model.CacheUpdateUserQuota(task.UserId) ?
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error update user quota cache: "+err.Error())
|
||||
logger.LogError(ctx, "error update user quota cache: "+err.Error())
|
||||
} else {
|
||||
quota := task.Quota
|
||||
if quota != 0 {
|
||||
err = model.IncreaseUserQuota(task.UserId, quota, false)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "fail to increase user quota: "+err.Error())
|
||||
logger.LogError(ctx, "fail to increase user quota: "+err.Error())
|
||||
}
|
||||
logContent := fmt.Sprintf("异步任务执行失败 %s,补偿 %s", task.TaskID, common.LogQuota(quota))
|
||||
logContent := fmt.Sprintf("异步任务执行失败 %s,补偿 %s", task.TaskID, logger.LogQuota(quota))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
}
|
||||
}
|
||||
@@ -178,7 +179,7 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
|
||||
|
||||
err = task.Update()
|
||||
if err != nil {
|
||||
common.SysError("UpdateMidjourneyTask task error: " + err.Error())
|
||||
common.SysLog("UpdateMidjourneyTask task error: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/relay"
|
||||
"one-api/relay/channel"
|
||||
@@ -18,14 +19,14 @@ import (
|
||||
func UpdateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, taskChannelM map[int][]string, taskM map[string]*model.Task) error {
|
||||
for channelId, taskIds := range taskChannelM {
|
||||
if err := updateVideoTaskAll(ctx, platform, channelId, taskIds, taskM); err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Channel #%d failed to update video async tasks: %s", channelId, err.Error()))
|
||||
logger.LogError(ctx, fmt.Sprintf("Channel #%d failed to update video async tasks: %s", channelId, err.Error()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, channelId int, taskIds []string, taskM map[string]*model.Task) error {
|
||||
common.LogInfo(ctx, fmt.Sprintf("Channel #%d pending video tasks: %d", channelId, len(taskIds)))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("Channel #%d pending video tasks: %d", channelId, len(taskIds)))
|
||||
if len(taskIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -37,7 +38,7 @@ func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, cha
|
||||
"progress": "100%",
|
||||
})
|
||||
if errUpdate != nil {
|
||||
common.SysError(fmt.Sprintf("UpdateVideoTask error: %v", errUpdate))
|
||||
common.SysLog(fmt.Sprintf("UpdateVideoTask error: %v", errUpdate))
|
||||
}
|
||||
return fmt.Errorf("CacheGetChannel failed: %w", err)
|
||||
}
|
||||
@@ -47,7 +48,7 @@ func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, cha
|
||||
}
|
||||
for _, taskId := range taskIds {
|
||||
if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error()))
|
||||
logger.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -61,7 +62,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
||||
|
||||
task := taskM[taskId]
|
||||
if task == nil {
|
||||
common.LogError(ctx, fmt.Sprintf("Task %s not found in taskM", taskId))
|
||||
logger.LogError(ctx, fmt.Sprintf("Task %s not found in taskM", taskId))
|
||||
return fmt.Errorf("task %s not found", taskId)
|
||||
}
|
||||
resp, err := adaptor.FetchTask(baseURL, channel.Key, map[string]any{
|
||||
@@ -112,7 +113,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
||||
task.StartTime = now
|
||||
}
|
||||
case model.TaskStatusSuccess:
|
||||
task.Progress = "100%"
|
||||
task.Progress = "100%"
|
||||
if task.FinishTime == 0 {
|
||||
task.FinishTime = now
|
||||
}
|
||||
@@ -124,13 +125,13 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
||||
task.FinishTime = now
|
||||
}
|
||||
task.FailReason = taskResult.Reason
|
||||
common.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
|
||||
logger.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
|
||||
quota := task.Quota
|
||||
if quota != 0 {
|
||||
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
|
||||
common.LogError(ctx, "Failed to increase user quota: "+err.Error())
|
||||
logger.LogError(ctx, "Failed to increase user quota: "+err.Error())
|
||||
}
|
||||
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, common.LogQuota(quota))
|
||||
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
|
||||
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||
}
|
||||
default:
|
||||
@@ -140,7 +141,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
||||
task.Progress = taskResult.Progress
|
||||
}
|
||||
if err := task.Update(); err != nil {
|
||||
common.SysError("UpdateVideoTask task error: " + err.Error())
|
||||
common.SysLog("UpdateVideoTask task error: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -102,7 +102,7 @@ func AddToken(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "生成令牌失败",
|
||||
})
|
||||
common.SysError("failed to generate token key: " + err.Error())
|
||||
common.SysLog("failed to generate token key: " + err.Error())
|
||||
return
|
||||
}
|
||||
cleanToken := model.Token{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
@@ -231,7 +232,7 @@ func EpayNotify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
log.Printf("易支付回调更新用户成功 %v", topUp)
|
||||
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", common.LogQuota(quotaToAdd), topUp.Money))
|
||||
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money))
|
||||
}
|
||||
} else {
|
||||
log.Printf("易支付异常回调: %v", verifyInfo)
|
||||
|
||||
@@ -70,7 +70,7 @@ func Setup2FA(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "生成2FA密钥失败",
|
||||
})
|
||||
common.SysError("生成TOTP密钥失败: " + err.Error())
|
||||
common.SysLog("生成TOTP密钥失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func Setup2FA(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "生成备用码失败",
|
||||
})
|
||||
common.SysError("生成备用码失败: " + err.Error())
|
||||
common.SysLog("生成备用码失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func Setup2FA(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "保存备用码失败",
|
||||
})
|
||||
common.SysError("保存备用码失败: " + err.Error())
|
||||
common.SysLog("保存备用码失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ func Get2FAStatus(c *gin.Context) {
|
||||
// 获取剩余备用码数量
|
||||
backupCount, err := model.GetUnusedBackupCodeCount(userId)
|
||||
if err != nil {
|
||||
common.SysError("获取备用码数量失败: " + err.Error())
|
||||
common.SysLog("获取备用码数量失败: " + err.Error())
|
||||
} else {
|
||||
status["backup_codes_remaining"] = backupCount
|
||||
}
|
||||
@@ -368,7 +368,7 @@ func RegenerateBackupCodes(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "生成备用码失败",
|
||||
})
|
||||
common.SysError("生成备用码失败: " + err.Error())
|
||||
common.SysLog("生成备用码失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ func RegenerateBackupCodes(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "保存备用码失败",
|
||||
})
|
||||
common.SysError("保存备用码失败: " + err.Error())
|
||||
common.SysLog("保存备用码失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
@@ -62,7 +63,7 @@ func Login(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 检查是否启用2FA
|
||||
if model.IsTwoFAEnabled(user.Id) {
|
||||
// 设置pending session,等待2FA验证
|
||||
@@ -77,7 +78,7 @@ func Login(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "请输入两步验证码",
|
||||
"success": true,
|
||||
@@ -87,7 +88,7 @@ func Login(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
setupLogin(&user, c)
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ func Register(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "数据库错误,请稍后重试",
|
||||
})
|
||||
common.SysError(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
|
||||
common.SysLog(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
@@ -235,7 +236,7 @@ func Register(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "生成默认令牌失败",
|
||||
})
|
||||
common.SysError("failed to generate token key: " + err.Error())
|
||||
common.SysLog("failed to generate token key: " + err.Error())
|
||||
return
|
||||
}
|
||||
// 生成默认令牌
|
||||
@@ -342,7 +343,7 @@ func GenerateAccessToken(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "生成失败",
|
||||
})
|
||||
common.SysError("failed to generate key: " + err.Error())
|
||||
common.SysLog("failed to generate key: " + err.Error())
|
||||
return
|
||||
}
|
||||
user.SetAccessToken(key)
|
||||
@@ -517,7 +518,7 @@ func UpdateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if originUser.Quota != updatedUser.Quota {
|
||||
model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
|
||||
model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", logger.LogQuota(originUser.Quota), logger.LogQuota(updatedUser.Quota)))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
@@ -843,18 +844,64 @@ type topUpRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var topUpLock = sync.Mutex{}
|
||||
var topUpLocks sync.Map
|
||||
var topUpCreateLock sync.Mutex
|
||||
|
||||
type topUpTryLock struct {
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
func newTopUpTryLock() *topUpTryLock {
|
||||
return &topUpTryLock{ch: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
func (l *topUpTryLock) TryLock() bool {
|
||||
select {
|
||||
case l.ch <- struct{}{}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (l *topUpTryLock) Unlock() {
|
||||
select {
|
||||
case <-l.ch:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func getTopUpLock(userID int) *topUpTryLock {
|
||||
if v, ok := topUpLocks.Load(userID); ok {
|
||||
return v.(*topUpTryLock)
|
||||
}
|
||||
topUpCreateLock.Lock()
|
||||
defer topUpCreateLock.Unlock()
|
||||
if v, ok := topUpLocks.Load(userID); ok {
|
||||
return v.(*topUpTryLock)
|
||||
}
|
||||
l := newTopUpTryLock()
|
||||
topUpLocks.Store(userID, l)
|
||||
return l
|
||||
}
|
||||
|
||||
func TopUp(c *gin.Context) {
|
||||
topUpLock.Lock()
|
||||
defer topUpLock.Unlock()
|
||||
id := c.GetInt("id")
|
||||
lock := getTopUpLock(id)
|
||||
if !lock.TryLock() {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "充值处理中,请稍后重试",
|
||||
})
|
||||
return
|
||||
}
|
||||
defer lock.Unlock()
|
||||
req := topUpRequest{}
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
id := c.GetInt("id")
|
||||
quota, err := model.Redeem(req.Key, id)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
@@ -865,7 +912,6 @@ func TopUp(c *gin.Context) {
|
||||
"message": "",
|
||||
"data": quota,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
type UpdateUserSettingRequest struct {
|
||||
|
||||
@@ -1,124 +1,124 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strconv"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetAllVendors 获取供应商列表(分页)
|
||||
func GetAllVendors(c *gin.Context) {
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
vendors, err := model.GetAllVendors(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var total int64
|
||||
model.DB.Model(&model.Vendor{}).Count(&total)
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(vendors)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
vendors, err := model.GetAllVendors(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var total int64
|
||||
model.DB.Model(&model.Vendor{}).Count(&total)
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(vendors)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
// SearchVendors 搜索供应商
|
||||
func SearchVendors(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
vendors, total, err := model.SearchVendors(keyword, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(vendors)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
keyword := c.Query("keyword")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
vendors, total, err := model.SearchVendors(keyword, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(vendors)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
// GetVendorMeta 根据 ID 获取供应商
|
||||
func GetVendorMeta(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
v, err := model.GetVendorByID(id)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, v)
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
v, err := model.GetVendorByID(id)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, v)
|
||||
}
|
||||
|
||||
// CreateVendorMeta 新建供应商
|
||||
func CreateVendorMeta(c *gin.Context) {
|
||||
var v model.Vendor
|
||||
if err := c.ShouldBindJSON(&v); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if v.Name == "" {
|
||||
common.ApiErrorMsg(c, "供应商名称不能为空")
|
||||
return
|
||||
}
|
||||
// 创建前先检查名称
|
||||
if dup, err := model.IsVendorNameDuplicated(0, v.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "供应商名称已存在")
|
||||
return
|
||||
}
|
||||
var v model.Vendor
|
||||
if err := c.ShouldBindJSON(&v); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if v.Name == "" {
|
||||
common.ApiErrorMsg(c, "供应商名称不能为空")
|
||||
return
|
||||
}
|
||||
// 创建前先检查名称
|
||||
if dup, err := model.IsVendorNameDuplicated(0, v.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "供应商名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := v.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &v)
|
||||
if err := v.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &v)
|
||||
}
|
||||
|
||||
// UpdateVendorMeta 更新供应商
|
||||
func UpdateVendorMeta(c *gin.Context) {
|
||||
var v model.Vendor
|
||||
if err := c.ShouldBindJSON(&v); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if v.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少供应商 ID")
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsVendorNameDuplicated(v.Id, v.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "供应商名称已存在")
|
||||
return
|
||||
}
|
||||
var v model.Vendor
|
||||
if err := c.ShouldBindJSON(&v); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if v.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少供应商 ID")
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsVendorNameDuplicated(v.Id, v.Name); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "供应商名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := v.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &v)
|
||||
if err := v.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &v)
|
||||
}
|
||||
|
||||
// DeleteVendorMeta 删除供应商
|
||||
func DeleteVendorMeta(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := model.DB.Delete(&model.Vendor{}, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := model.DB.Delete(&model.Vendor{}, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
- REDIS_CONN_STRING=redis://redis
|
||||
- TZ=Asia/Shanghai
|
||||
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录
|
||||
# - STREAMING_TIMEOUT=120 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
|
||||
# - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
|
||||
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
|
||||
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
|
||||
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# One API – Web 界面后端接口文档
|
||||
# New API – Web 界面后端接口文档
|
||||
|
||||
> 本文档汇总了 **One API** 后端提供给前端 Web 界面的全部 REST 接口(不含 *Relay* 相关接口)。
|
||||
> 本文档汇总了 **New API** 后端提供给前端 Web 界面的全部 REST 接口(不含 *Relay* 相关接口)。
|
||||
>
|
||||
> 接口前缀统一为 `https://<your-domain>`,以下仅列出 **路径**、**HTTP 方法**、**鉴权要求** 与 **功能简介**。
|
||||
>
|
||||
@@ -62,6 +62,8 @@
|
||||
| GET | /api/user/groups | 公开 | 列出所有分组(无鉴权版) |
|
||||
|
||||
### 5.2 用户自身操作 (需登录)
|
||||
| 方法 | 路径 | 鉴权 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/user/self/groups | 用户 | 获取自己所在分组 |
|
||||
| GET | /api/user/self | 用户 | 获取个人资料 |
|
||||
| GET | /api/user/models | 用户 | 获取模型可见性 |
|
||||
@@ -192,4 +194,4 @@
|
||||
|
||||
---
|
||||
|
||||
> **更新日期**:2025.07.17
|
||||
> **更新日期**:2025.07.17
|
||||
|
||||
BIN
docs/images/aliyun.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/images/cherry-studio.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea5e5d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #23af69;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ea5756;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
|
||||
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
|
||||
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
|
||||
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
|
||||
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
|
||||
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
|
||||
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
|
||||
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
|
||||
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
|
||||
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
|
||||
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
|
||||
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
|
||||
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.5 KiB |
BIN
docs/images/io-net.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 12 KiB |
BIN
docs/images/ucloud.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
24
dto/audio.go
@@ -1,5 +1,11 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"one-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AudioRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input string `json:"input"`
|
||||
@@ -8,6 +14,24 @@ type AudioRequest struct {
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
}
|
||||
|
||||
func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
meta := &types.TokenCountMeta{
|
||||
CombineText: r.Input,
|
||||
TokenType: types.TokenTypeTextNumber,
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func (r *AudioRequest) IsStream(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *AudioRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
type AudioResponse struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
@@ -6,4 +6,9 @@ type ChannelSettings struct {
|
||||
Proxy string `json:"proxy"`
|
||||
PassThroughBodyEnabled bool `json:"pass_through_body_enabled,omitempty"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
SystemPromptOverride bool `json:"system_prompt_override,omitempty"`
|
||||
}
|
||||
|
||||
type ChannelOtherSettings struct {
|
||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||
}
|
||||
|
||||
146
dto/claude.go
@@ -5,6 +5,9 @@ import (
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ClaudeMetadata struct {
|
||||
@@ -81,7 +84,7 @@ func (c *ClaudeMediaMessage) GetStringContent() string {
|
||||
}
|
||||
|
||||
func (c *ClaudeMediaMessage) GetJsonRowString() string {
|
||||
jsonContent, _ := json.Marshal(c)
|
||||
jsonContent, _ := common.Marshal(c)
|
||||
return string(jsonContent)
|
||||
}
|
||||
|
||||
@@ -199,6 +202,147 @@ type ClaudeRequest struct {
|
||||
Thinking *Thinking `json:"thinking,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var tokenCountMeta = types.TokenCountMeta{
|
||||
TokenType: types.TokenTypeTokenizer,
|
||||
MaxTokens: int(c.MaxTokens),
|
||||
}
|
||||
|
||||
var texts = make([]string, 0)
|
||||
var fileMeta = make([]*types.FileMeta, 0)
|
||||
|
||||
// system
|
||||
if c.System != nil {
|
||||
if c.IsStringSystem() {
|
||||
sys := c.GetStringSystem()
|
||||
if sys != "" {
|
||||
texts = append(texts, sys)
|
||||
}
|
||||
} else {
|
||||
systemMedia := c.ParseSystem()
|
||||
for _, media := range systemMedia {
|
||||
switch media.Type {
|
||||
case "text":
|
||||
texts = append(texts, media.GetText())
|
||||
case "image":
|
||||
if media.Source != nil {
|
||||
data := media.Source.Url
|
||||
if data == "" {
|
||||
data = common.Interface2String(media.Source.Data)
|
||||
}
|
||||
if data != "" {
|
||||
fileMeta = append(fileMeta, &types.FileMeta{FileType: types.FileTypeImage, OriginData: data})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// messages
|
||||
for _, message := range c.Messages {
|
||||
tokenCountMeta.MessagesCount++
|
||||
texts = append(texts, message.Role)
|
||||
if message.IsStringContent() {
|
||||
content := message.GetStringContent()
|
||||
if content != "" {
|
||||
texts = append(texts, content)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
content, _ := message.ParseContent()
|
||||
for _, media := range content {
|
||||
switch media.Type {
|
||||
case "text":
|
||||
texts = append(texts, media.GetText())
|
||||
case "image":
|
||||
if media.Source != nil {
|
||||
data := media.Source.Url
|
||||
if data == "" {
|
||||
data = common.Interface2String(media.Source.Data)
|
||||
}
|
||||
if data != "" {
|
||||
fileMeta = append(fileMeta, &types.FileMeta{FileType: types.FileTypeImage, OriginData: data})
|
||||
}
|
||||
}
|
||||
case "tool_use":
|
||||
if media.Name != "" {
|
||||
texts = append(texts, media.Name)
|
||||
}
|
||||
if media.Input != nil {
|
||||
b, _ := common.Marshal(media.Input)
|
||||
texts = append(texts, string(b))
|
||||
}
|
||||
case "tool_result":
|
||||
if media.Content != nil {
|
||||
b, _ := common.Marshal(media.Content)
|
||||
texts = append(texts, string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tools
|
||||
if c.Tools != nil {
|
||||
tools := c.GetTools()
|
||||
normalTools, webSearchTools := ProcessTools(tools)
|
||||
if normalTools != nil {
|
||||
for _, t := range normalTools {
|
||||
tokenCountMeta.ToolsCount++
|
||||
if t.Name != "" {
|
||||
texts = append(texts, t.Name)
|
||||
}
|
||||
if t.Description != "" {
|
||||
texts = append(texts, t.Description)
|
||||
}
|
||||
if t.InputSchema != nil {
|
||||
b, _ := common.Marshal(t.InputSchema)
|
||||
texts = append(texts, string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
if webSearchTools != nil {
|
||||
for _, t := range webSearchTools {
|
||||
tokenCountMeta.ToolsCount++
|
||||
if t.Name != "" {
|
||||
texts = append(texts, t.Name)
|
||||
}
|
||||
if t.UserLocation != nil {
|
||||
b, _ := common.Marshal(t.UserLocation)
|
||||
texts = append(texts, string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokenCountMeta.CombineText = strings.Join(texts, "\n")
|
||||
tokenCountMeta.Files = fileMeta
|
||||
return &tokenCountMeta
|
||||
}
|
||||
|
||||
func (c *ClaudeRequest) IsStream(ctx *gin.Context) bool {
|
||||
return c.Stream
|
||||
}
|
||||
|
||||
func (c *ClaudeRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
c.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClaudeRequest) SearchToolNameByToolCallId(toolCallId string) string {
|
||||
for _, message := range c.Messages {
|
||||
content, _ := message.ParseContent()
|
||||
for _, mediaMessage := range content {
|
||||
if mediaMessage.Id == toolCallId {
|
||||
return mediaMessage.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddTool 添加工具到请求中
|
||||
func (c *ClaudeRequest) AddTool(tool any) {
|
||||
if c.Tools == nil {
|
||||
|
||||
29
dto/dalle.go
@@ -1,29 +0,0 @@
|
||||
package dto
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type ImageRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
|
||||
Background string `json:"background,omitempty"`
|
||||
Moderation string `json:"moderation,omitempty"`
|
||||
OutputFormat string `json:"output_format,omitempty"`
|
||||
Watermark *bool `json:"watermark,omitempty"`
|
||||
}
|
||||
|
||||
type ImageResponse struct {
|
||||
Data []ImageData `json:"data"`
|
||||
Created int64 `json:"created"`
|
||||
}
|
||||
type ImageData struct {
|
||||
Url string `json:"url"`
|
||||
B64Json string `json:"b64_json"`
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type EmbeddingOptions struct {
|
||||
Seed int `json:"seed,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
@@ -24,9 +31,32 @@ type EmbeddingRequest struct {
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||
}
|
||||
|
||||
func (r EmbeddingRequest) ParseInput() []string {
|
||||
func (r *EmbeddingRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var texts = make([]string, 0)
|
||||
|
||||
inputs := r.ParseInput()
|
||||
for _, input := range inputs {
|
||||
texts = append(texts, input)
|
||||
}
|
||||
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: strings.Join(texts, "\n"),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *EmbeddingRequest) IsStream(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *EmbeddingRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
func (r *EmbeddingRequest) ParseInput() []string {
|
||||
if r.Input == nil {
|
||||
return nil
|
||||
return make([]string, 0)
|
||||
}
|
||||
var input []string
|
||||
switch r.Input.(type) {
|
||||
|
||||
161
dto/gemini.go
@@ -2,17 +2,116 @@ package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GeminiChatRequest struct {
|
||||
Contents []GeminiChatContent `json:"contents"`
|
||||
SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
|
||||
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
|
||||
Tools []GeminiChatTool `json:"tools,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"`
|
||||
SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"`
|
||||
}
|
||||
|
||||
func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var files []*types.FileMeta = make([]*types.FileMeta, 0)
|
||||
|
||||
var maxTokens int
|
||||
|
||||
if r.GenerationConfig.MaxOutputTokens > 0 {
|
||||
maxTokens = int(r.GenerationConfig.MaxOutputTokens)
|
||||
}
|
||||
|
||||
var inputTexts []string
|
||||
for _, content := range r.Contents {
|
||||
for _, part := range content.Parts {
|
||||
if part.Text != "" {
|
||||
inputTexts = append(inputTexts, part.Text)
|
||||
}
|
||||
if part.InlineData != nil && part.InlineData.Data != "" {
|
||||
if strings.HasPrefix(part.InlineData.MimeType, "image/") {
|
||||
files = append(files, &types.FileMeta{
|
||||
FileType: types.FileTypeImage,
|
||||
OriginData: part.InlineData.Data,
|
||||
})
|
||||
} else if strings.HasPrefix(part.InlineData.MimeType, "audio/") {
|
||||
files = append(files, &types.FileMeta{
|
||||
FileType: types.FileTypeAudio,
|
||||
OriginData: part.InlineData.Data,
|
||||
})
|
||||
} else if strings.HasPrefix(part.InlineData.MimeType, "video/") {
|
||||
files = append(files, &types.FileMeta{
|
||||
FileType: types.FileTypeVideo,
|
||||
OriginData: part.InlineData.Data,
|
||||
})
|
||||
} else {
|
||||
files = append(files, &types.FileMeta{
|
||||
FileType: types.FileTypeFile,
|
||||
OriginData: part.InlineData.Data,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputText := strings.Join(inputTexts, "\n")
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: inputText,
|
||||
Files: files,
|
||||
MaxTokens: maxTokens,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GeminiChatRequest) IsStream(c *gin.Context) bool {
|
||||
if c.Query("alt") == "sse" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *GeminiChatRequest) SetModelName(modelName string) {
|
||||
// GeminiChatRequest does not have a model field, so this method does nothing.
|
||||
}
|
||||
|
||||
func (r *GeminiChatRequest) GetTools() []GeminiChatTool {
|
||||
var tools []GeminiChatTool
|
||||
if strings.HasSuffix(string(r.Tools), "[") {
|
||||
// is array
|
||||
if err := common.Unmarshal(r.Tools, &tools); err != nil {
|
||||
logger.LogError(nil, "error_unmarshalling_tools: "+err.Error())
|
||||
return nil
|
||||
}
|
||||
} else if strings.HasPrefix(string(r.Tools), "{") {
|
||||
// is object
|
||||
singleTool := GeminiChatTool{}
|
||||
if err := common.Unmarshal(r.Tools, &singleTool); err != nil {
|
||||
logger.LogError(nil, "error_unmarshalling_single_tool: "+err.Error())
|
||||
return nil
|
||||
}
|
||||
tools = []GeminiChatTool{singleTool}
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
func (r *GeminiChatRequest) SetTools(tools []GeminiChatTool) {
|
||||
if len(tools) == 0 {
|
||||
r.Tools = json.RawMessage("[]")
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal the tools to JSON
|
||||
data, err := common.Marshal(tools)
|
||||
if err != nil {
|
||||
logger.LogError(nil, "error_marshalling_tools: "+err.Error())
|
||||
return
|
||||
}
|
||||
r.Tools = data
|
||||
}
|
||||
|
||||
type GeminiThinkingConfig struct {
|
||||
IncludeThoughts bool `json:"includeThoughts,omitempty"`
|
||||
ThinkingBudget *int `json:"thinkingBudget,omitempty"`
|
||||
@@ -210,16 +309,76 @@ type GeminiImagePrediction struct {
|
||||
|
||||
// Embedding related structs
|
||||
type GeminiEmbeddingRequest struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
Content GeminiChatContent `json:"content"`
|
||||
TaskType string `json:"taskType,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
OutputDimensionality int `json:"outputDimensionality,omitempty"`
|
||||
}
|
||||
|
||||
func (r *GeminiEmbeddingRequest) IsStream(c *gin.Context) bool {
|
||||
// Gemini embedding requests are not streamed
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *GeminiEmbeddingRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var inputTexts []string
|
||||
for _, part := range r.Content.Parts {
|
||||
if part.Text != "" {
|
||||
inputTexts = append(inputTexts, part.Text)
|
||||
}
|
||||
}
|
||||
inputText := strings.Join(inputTexts, "\n")
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: inputText,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GeminiEmbeddingRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
type GeminiBatchEmbeddingRequest struct {
|
||||
Requests []*GeminiEmbeddingRequest `json:"requests"`
|
||||
}
|
||||
|
||||
func (r *GeminiBatchEmbeddingRequest) IsStream(c *gin.Context) bool {
|
||||
// Gemini batch embedding requests are not streamed
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *GeminiBatchEmbeddingRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var inputTexts []string
|
||||
for _, request := range r.Requests {
|
||||
meta := request.GetTokenCountMeta()
|
||||
if meta != nil && meta.CombineText != "" {
|
||||
inputTexts = append(inputTexts, meta.CombineText)
|
||||
}
|
||||
}
|
||||
inputText := strings.Join(inputTexts, "\n")
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: inputText,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GeminiBatchEmbeddingRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
for _, req := range r.Requests {
|
||||
req.SetModelName(modelName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type GeminiEmbeddingResponse struct {
|
||||
Embedding ContentEmbedding `json:"embedding"`
|
||||
}
|
||||
|
||||
type GeminiBatchEmbeddingResponse struct {
|
||||
Embeddings []*ContentEmbedding `json:"embeddings"`
|
||||
}
|
||||
|
||||
type ContentEmbedding struct {
|
||||
Values []float64 `json:"values"`
|
||||
}
|
||||
|
||||
80
dto/openai_image.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ImageRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
N uint `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Style json.RawMessage `json:"style,omitempty"`
|
||||
User json.RawMessage `json:"user,omitempty"`
|
||||
ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
|
||||
Background json.RawMessage `json:"background,omitempty"`
|
||||
Moderation json.RawMessage `json:"moderation,omitempty"`
|
||||
OutputFormat json.RawMessage `json:"output_format,omitempty"`
|
||||
OutputCompression json.RawMessage `json:"output_compression,omitempty"`
|
||||
PartialImages json.RawMessage `json:"partial_images,omitempty"`
|
||||
// Stream bool `json:"stream,omitempty"`
|
||||
Watermark *bool `json:"watermark,omitempty"`
|
||||
}
|
||||
|
||||
func (i *ImageRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var sizeRatio = 1.0
|
||||
var qualityRatio = 1.0
|
||||
|
||||
if strings.HasPrefix(i.Model, "dall-e") {
|
||||
// Size
|
||||
if i.Size == "256x256" {
|
||||
sizeRatio = 0.4
|
||||
} else if i.Size == "512x512" {
|
||||
sizeRatio = 0.45
|
||||
} else if i.Size == "1024x1024" {
|
||||
sizeRatio = 1
|
||||
} else if i.Size == "1024x1792" || i.Size == "1792x1024" {
|
||||
sizeRatio = 2
|
||||
}
|
||||
|
||||
if i.Model == "dall-e-3" && i.Quality == "hd" {
|
||||
qualityRatio = 2.0
|
||||
if i.Size == "1024x1792" || i.Size == "1792x1024" {
|
||||
qualityRatio = 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not support token count for dalle
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: i.Prompt,
|
||||
MaxTokens: 1584,
|
||||
ImagePriceRatio: sizeRatio * qualityRatio * float64(i.N),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ImageRequest) IsStream(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *ImageRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
i.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
type ImageResponse struct {
|
||||
Data []ImageData `json:"data"`
|
||||
Created int64 `json:"created"`
|
||||
}
|
||||
type ImageData struct {
|
||||
Url string `json:"url"`
|
||||
B64Json string `json:"b64_json"`
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
}
|
||||
@@ -2,8 +2,12 @@ package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ResponseFormat struct {
|
||||
@@ -29,6 +33,7 @@ type GeneralOpenAIRequest struct {
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||
Verbosity json.RawMessage `json:"verbosity,omitempty"` // gpt-5
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
@@ -53,7 +58,7 @@ type GeneralOpenAIRequest struct {
|
||||
Modalities json.RawMessage `json:"modalities,omitempty"`
|
||||
Audio json.RawMessage `json:"audio,omitempty"`
|
||||
EnableThinking any `json:"enable_thinking,omitempty"` // ali
|
||||
THINKING json.RawMessage `json:"thinking,omitempty"` // doubao
|
||||
THINKING json.RawMessage `json:"thinking,omitempty"` // doubao,zhipu_v4
|
||||
ExtraBody json.RawMessage `json:"extra_body,omitempty"`
|
||||
SearchParameters any `json:"search_parameters,omitempty"` //xai
|
||||
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
|
||||
@@ -66,6 +71,124 @@ type GeneralOpenAIRequest struct {
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var tokenCountMeta types.TokenCountMeta
|
||||
var texts = make([]string, 0)
|
||||
var fileMeta = make([]*types.FileMeta, 0)
|
||||
|
||||
if r.Prompt != nil {
|
||||
switch v := r.Prompt.(type) {
|
||||
case string:
|
||||
texts = append(texts, v)
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok {
|
||||
texts = append(texts, str)
|
||||
}
|
||||
}
|
||||
default:
|
||||
texts = append(texts, fmt.Sprintf("%v", r.Prompt))
|
||||
}
|
||||
}
|
||||
|
||||
if r.Input != nil {
|
||||
inputs := r.ParseInput()
|
||||
texts = append(texts, inputs...)
|
||||
}
|
||||
|
||||
if r.MaxCompletionTokens > r.MaxTokens {
|
||||
tokenCountMeta.MaxTokens = int(r.MaxCompletionTokens)
|
||||
} else {
|
||||
tokenCountMeta.MaxTokens = int(r.MaxTokens)
|
||||
}
|
||||
|
||||
for _, message := range r.Messages {
|
||||
tokenCountMeta.MessagesCount++
|
||||
texts = append(texts, message.Role)
|
||||
if message.Content != nil {
|
||||
if message.Name != nil {
|
||||
tokenCountMeta.NameCount++
|
||||
texts = append(texts, *message.Name)
|
||||
}
|
||||
arrayContent := message.ParseContent()
|
||||
for _, m := range arrayContent {
|
||||
if m.Type == ContentTypeImageURL {
|
||||
imageUrl := m.GetImageMedia()
|
||||
if imageUrl != nil {
|
||||
if imageUrl.Url != "" {
|
||||
meta := &types.FileMeta{
|
||||
FileType: types.FileTypeImage,
|
||||
}
|
||||
meta.OriginData = imageUrl.Url
|
||||
meta.Detail = imageUrl.Detail
|
||||
fileMeta = append(fileMeta, meta)
|
||||
}
|
||||
}
|
||||
} else if m.Type == ContentTypeInputAudio {
|
||||
inputAudio := m.GetInputAudio()
|
||||
if inputAudio != nil {
|
||||
meta := &types.FileMeta{
|
||||
FileType: types.FileTypeAudio,
|
||||
}
|
||||
meta.OriginData = inputAudio.Data
|
||||
fileMeta = append(fileMeta, meta)
|
||||
}
|
||||
} else if m.Type == ContentTypeFile {
|
||||
file := m.GetFile()
|
||||
if file != nil {
|
||||
meta := &types.FileMeta{
|
||||
FileType: types.FileTypeFile,
|
||||
}
|
||||
meta.OriginData = file.FileData
|
||||
fileMeta = append(fileMeta, meta)
|
||||
}
|
||||
} else if m.Type == ContentTypeVideoUrl {
|
||||
videoUrl := m.GetVideoUrl()
|
||||
if videoUrl != nil && videoUrl.Url != "" {
|
||||
meta := &types.FileMeta{
|
||||
FileType: types.FileTypeVideo,
|
||||
}
|
||||
meta.OriginData = videoUrl.Url
|
||||
fileMeta = append(fileMeta, meta)
|
||||
}
|
||||
} else {
|
||||
texts = append(texts, m.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Tools != nil {
|
||||
openaiTools := r.Tools
|
||||
for _, tool := range openaiTools {
|
||||
tokenCountMeta.ToolsCount++
|
||||
texts = append(texts, tool.Function.Name)
|
||||
if tool.Function.Description != "" {
|
||||
texts = append(texts, tool.Function.Description)
|
||||
}
|
||||
if tool.Function.Parameters != nil {
|
||||
texts = append(texts, fmt.Sprintf("%v", tool.Function.Parameters))
|
||||
}
|
||||
}
|
||||
//toolTokens := CountTokenInput(countStr, request.Model)
|
||||
//tkm += 8
|
||||
//tkm += toolTokens
|
||||
}
|
||||
tokenCountMeta.CombineText = strings.Join(texts, "\n")
|
||||
tokenCountMeta.Files = fileMeta
|
||||
return &tokenCountMeta
|
||||
}
|
||||
|
||||
func (r *GeneralOpenAIRequest) IsStream(c *gin.Context) bool {
|
||||
return r.Stream
|
||||
}
|
||||
|
||||
func (r *GeneralOpenAIRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GeneralOpenAIRequest) ToMap() map[string]any {
|
||||
result := make(map[string]any)
|
||||
data, _ := common.Marshal(r)
|
||||
@@ -78,6 +201,8 @@ func (r *GeneralOpenAIRequest) GetSystemRoleName() string {
|
||||
if !strings.HasPrefix(r.Model, "o1-mini") && !strings.HasPrefix(r.Model, "o1-preview") {
|
||||
return "developer"
|
||||
}
|
||||
} else if strings.HasPrefix(r.Model, "gpt-5") {
|
||||
return "developer"
|
||||
}
|
||||
return "system"
|
||||
}
|
||||
@@ -199,6 +324,21 @@ func (m *MediaContent) GetFile() *MessageFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MediaContent) GetVideoUrl() *MessageVideoUrl {
|
||||
if m.VideoUrl != nil {
|
||||
if _, ok := m.VideoUrl.(*MessageVideoUrl); ok {
|
||||
return m.VideoUrl.(*MessageVideoUrl)
|
||||
}
|
||||
if itemMap, ok := m.VideoUrl.(map[string]any); ok {
|
||||
out := &MessageVideoUrl{
|
||||
Url: common.Interface2String(itemMap["url"]),
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MessageImageUrl struct {
|
||||
Url string `json:"url"`
|
||||
Detail string `json:"detail"`
|
||||
@@ -230,6 +370,7 @@ const (
|
||||
ContentTypeInputAudio = "input_audio"
|
||||
ContentTypeFile = "file"
|
||||
ContentTypeVideoUrl = "video_url" // 阿里百炼视频识别
|
||||
//ContentTypeAudioUrl = "audio_url"
|
||||
)
|
||||
|
||||
func (m *Message) GetPrefix() bool {
|
||||
@@ -620,7 +761,7 @@ type WebSearchOptions struct {
|
||||
// https://platform.openai.com/docs/api-reference/responses/create
|
||||
type OpenAIResponsesRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Input any `json:"input,omitempty"`
|
||||
Include json.RawMessage `json:"include,omitempty"`
|
||||
Instructions json.RawMessage `json:"instructions,omitempty"`
|
||||
MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
|
||||
@@ -642,28 +783,155 @@ type OpenAIResponsesRequest struct {
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var fileMeta = make([]*types.FileMeta, 0)
|
||||
var texts = make([]string, 0)
|
||||
|
||||
if r.Input != nil {
|
||||
inputs := r.ParseInput()
|
||||
for _, input := range inputs {
|
||||
if input.Type == "input_image" {
|
||||
if input.ImageUrl != "" {
|
||||
fileMeta = append(fileMeta, &types.FileMeta{
|
||||
FileType: types.FileTypeImage,
|
||||
OriginData: input.ImageUrl,
|
||||
Detail: input.Detail,
|
||||
})
|
||||
}
|
||||
} else if input.Type == "input_file" {
|
||||
if input.FileUrl != "" {
|
||||
fileMeta = append(fileMeta, &types.FileMeta{
|
||||
FileType: types.FileTypeFile,
|
||||
OriginData: input.FileUrl,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
texts = append(texts, input.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Instructions) > 0 {
|
||||
texts = append(texts, string(r.Instructions))
|
||||
}
|
||||
|
||||
if len(r.Metadata) > 0 {
|
||||
texts = append(texts, string(r.Metadata))
|
||||
}
|
||||
|
||||
if len(r.Text) > 0 {
|
||||
texts = append(texts, string(r.Text))
|
||||
}
|
||||
|
||||
if len(r.ToolChoice) > 0 {
|
||||
texts = append(texts, string(r.ToolChoice))
|
||||
}
|
||||
|
||||
if len(r.Prompt) > 0 {
|
||||
texts = append(texts, string(r.Prompt))
|
||||
}
|
||||
|
||||
if len(r.Tools) > 0 {
|
||||
toolStr, _ := common.Marshal(r.Tools)
|
||||
texts = append(texts, string(toolStr))
|
||||
}
|
||||
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: strings.Join(texts, "\n"),
|
||||
Files: fileMeta,
|
||||
MaxTokens: int(r.MaxOutputTokens),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesRequest) IsStream(c *gin.Context) bool {
|
||||
return r.Stream
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
type Reasoning struct {
|
||||
Effort string `json:"effort,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
//type ResponsesToolsCall struct {
|
||||
// Type string `json:"type"`
|
||||
// // Web Search
|
||||
// UserLocation json.RawMessage `json:"user_location,omitempty"`
|
||||
// SearchContextSize string `json:"search_context_size,omitempty"`
|
||||
// // File Search
|
||||
// VectorStoreIds []string `json:"vector_store_ids,omitempty"`
|
||||
// MaxNumResults uint `json:"max_num_results,omitempty"`
|
||||
// Filters json.RawMessage `json:"filters,omitempty"`
|
||||
// // Computer Use
|
||||
// DisplayWidth uint `json:"display_width,omitempty"`
|
||||
// DisplayHeight uint `json:"display_height,omitempty"`
|
||||
// Environment string `json:"environment,omitempty"`
|
||||
// // Function
|
||||
// Name string `json:"name,omitempty"`
|
||||
// Description string `json:"description,omitempty"`
|
||||
// Parameters json.RawMessage `json:"parameters,omitempty"`
|
||||
// Function json.RawMessage `json:"function,omitempty"`
|
||||
// Container json.RawMessage `json:"container,omitempty"`
|
||||
//}
|
||||
type MediaInput struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
FileUrl string `json:"file_url,omitempty"`
|
||||
ImageUrl string `json:"image_url,omitempty"`
|
||||
Detail string `json:"detail,omitempty"` // 仅 input_image 有效
|
||||
}
|
||||
|
||||
// ParseInput parses the Responses API `input` field into a normalized slice of MediaInput.
|
||||
// Reference implementation mirrors Message.ParseContent:
|
||||
// - input can be a string, treated as an input_text item
|
||||
// - input can be an array of objects with a `type` field
|
||||
// supported types: input_text, input_image, input_file
|
||||
func (r *OpenAIResponsesRequest) ParseInput() []MediaInput {
|
||||
if r.Input == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var inputs []MediaInput
|
||||
|
||||
// Try string first
|
||||
if str, ok := r.Input.(string); ok {
|
||||
inputs = append(inputs, MediaInput{Type: "input_text", Text: str})
|
||||
return inputs
|
||||
}
|
||||
|
||||
// Try array of parts
|
||||
if array, ok := r.Input.([]any); ok {
|
||||
for _, itemAny := range array {
|
||||
// Already parsed MediaInput
|
||||
if media, ok := itemAny.(MediaInput); ok {
|
||||
inputs = append(inputs, media)
|
||||
continue
|
||||
}
|
||||
// Generic map
|
||||
item, ok := itemAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
typeVal, ok := item["type"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch typeVal {
|
||||
case "input_text":
|
||||
text, _ := item["text"].(string)
|
||||
inputs = append(inputs, MediaInput{Type: "input_text", Text: text})
|
||||
case "input_image":
|
||||
// image_url may be string or object with url field
|
||||
var imageUrl string
|
||||
switch v := item["image_url"].(type) {
|
||||
case string:
|
||||
imageUrl = v
|
||||
case map[string]any:
|
||||
if url, ok := v["url"].(string); ok {
|
||||
imageUrl = url
|
||||
}
|
||||
}
|
||||
inputs = append(inputs, MediaInput{Type: "input_image", ImageUrl: imageUrl})
|
||||
case "input_file":
|
||||
// file_url may be string or object with url field
|
||||
var fileUrl string
|
||||
switch v := item["file_url"].(type) {
|
||||
case string:
|
||||
fileUrl = v
|
||||
case map[string]any:
|
||||
if url, ok := v["url"].(string); ok {
|
||||
fileUrl = url
|
||||
}
|
||||
}
|
||||
inputs = append(inputs, MediaInput{Type: "input_file", FileUrl: fileUrl})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inputs
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (c *ChatCompletionsStreamResponseChoiceDelta) GetReasoningContent() string
|
||||
|
||||
func (c *ChatCompletionsStreamResponseChoiceDelta) SetReasoningContent(s string) {
|
||||
c.ReasoningContent = &s
|
||||
c.Reasoning = &s
|
||||
//c.Reasoning = &s
|
||||
}
|
||||
|
||||
type ToolCallResponse struct {
|
||||
@@ -143,6 +143,13 @@ type ChatCompletionsStreamResponse struct {
|
||||
Usage *Usage `json:"usage"`
|
||||
}
|
||||
|
||||
func (c *ChatCompletionsStreamResponse) IsFinished() bool {
|
||||
if len(c.Choices) == 0 {
|
||||
return false
|
||||
}
|
||||
return c.Choices[0].FinishReason != nil && *c.Choices[0].FinishReason != ""
|
||||
}
|
||||
|
||||
func (c *ChatCompletionsStreamResponse) IsToolCall() bool {
|
||||
if len(c.Choices) == 0 {
|
||||
return false
|
||||
@@ -157,6 +164,19 @@ func (c *ChatCompletionsStreamResponse) GetFirstToolCall() *ToolCallResponse {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChatCompletionsStreamResponse) ClearToolCalls() {
|
||||
if !c.IsToolCall() {
|
||||
return
|
||||
}
|
||||
for choiceIdx := range c.Choices {
|
||||
for callIdx := range c.Choices[choiceIdx].Delta.ToolCalls {
|
||||
c.Choices[choiceIdx].Delta.ToolCalls[callIdx].ID = ""
|
||||
c.Choices[choiceIdx].Delta.ToolCalls[callIdx].Type = nil
|
||||
c.Choices[choiceIdx].Delta.ToolCalls[callIdx].Function.Name = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse {
|
||||
choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices))
|
||||
copy(choices, c.Choices)
|
||||
|
||||
@@ -2,6 +2,7 @@ package dto
|
||||
|
||||
import "one-api/constant"
|
||||
|
||||
// 这里不好动就不动了,本来想独立出来的(
|
||||
type OpenAIModels struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
@@ -9,3 +10,26 @@ type OpenAIModels struct {
|
||||
OwnedBy string `json:"owned_by"`
|
||||
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
|
||||
}
|
||||
|
||||
type AnthropicModel struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GeminiModel struct {
|
||||
Name interface{} `json:"name"`
|
||||
BaseModelId interface{} `json:"baseModelId"`
|
||||
Version interface{} `json:"version"`
|
||||
DisplayName interface{} `json:"displayName"`
|
||||
Description interface{} `json:"description"`
|
||||
InputTokenLimit interface{} `json:"inputTokenLimit"`
|
||||
OutputTokenLimit interface{} `json:"outputTokenLimit"`
|
||||
SupportedGenerationMethods []interface{} `json:"supportedGenerationMethods"`
|
||||
Thinking interface{} `json:"thinking"`
|
||||
Temperature interface{} `json:"temperature"`
|
||||
MaxTemperature interface{} `json:"maxTemperature"`
|
||||
TopP interface{} `json:"topP"`
|
||||
TopK interface{} `json:"topK"`
|
||||
}
|
||||
|
||||
25
dto/request_common.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/types"
|
||||
)
|
||||
|
||||
type Request interface {
|
||||
GetTokenCountMeta() *types.TokenCountMeta
|
||||
IsStream(c *gin.Context) bool
|
||||
SetModelName(modelName string)
|
||||
}
|
||||
|
||||
type BaseRequest struct {
|
||||
}
|
||||
|
||||
func (b *BaseRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
return &types.TokenCountMeta{
|
||||
TokenType: types.TokenTypeTokenizer,
|
||||
}
|
||||
}
|
||||
func (b *BaseRequest) IsStream(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
func (b *BaseRequest) SetModelName(modelName string) {}
|
||||
@@ -1,5 +1,12 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RerankRequest struct {
|
||||
Documents []any `json:"documents"`
|
||||
Query string `json:"query"`
|
||||
@@ -10,6 +17,32 @@ type RerankRequest struct {
|
||||
OverLapTokens int `json:"overlap_tokens,omitempty"`
|
||||
}
|
||||
|
||||
func (r *RerankRequest) IsStream(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RerankRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
var texts = make([]string, 0)
|
||||
|
||||
for _, document := range r.Documents {
|
||||
texts = append(texts, fmt.Sprintf("%v", document))
|
||||
}
|
||||
|
||||
if r.Query != "" {
|
||||
texts = append(texts, r.Query)
|
||||
}
|
||||
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: strings.Join(texts, "\n"),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RerankRequest) SetModelName(modelName string) {
|
||||
if modelName != "" {
|
||||
r.Model = modelName
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RerankRequest) GetReturnDocuments() bool {
|
||||
if r.ReturnDocuments == nil {
|
||||
return false
|
||||
|
||||
13
go.mod
@@ -31,6 +31,8 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stripe/stripe-go/v81 v81.4.0
|
||||
github.com/thanhpk/randstr v1.0.6
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/tiktoken-go/tokenizer v0.6.2
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/image v0.23.0
|
||||
@@ -42,7 +44,11 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 // indirect
|
||||
github.com/antlabs/pcopy v0.1.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect
|
||||
@@ -67,6 +73,8 @@ require (
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||
@@ -77,11 +85,16 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
||||
54
go.sum
@@ -1,11 +1,19 @@
|
||||
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
|
||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
|
||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
|
||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
|
||||
github.com/antlabs/pcopy v0.1.5 h1:5Fa1ExY9T6ar3ysAi4rzB5jiYg72Innm+/ESEIOSHvQ=
|
||||
github.com/antlabs/pcopy v0.1.5/go.mod h1:2FvdkPD3cFiM1CjGuXFCDQZqhKVcLI7IzeSJ2xUIOOI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
|
||||
@@ -102,6 +110,7 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
@@ -112,6 +121,10 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -150,8 +163,12 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -184,14 +201,19 @@ github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -204,6 +226,15 @@ github.com/stripe/stripe-go/v81 v81.4.0 h1:AuD9XzdAvl193qUCSaLocf8H+nRopOouXhxqJ
|
||||
github.com/stripe/stripe-go/v81 v81.4.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo=
|
||||
github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
|
||||
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tiktoken-go/tokenizer v0.6.2 h1:t0GN2DvcUZSFWT/62YOgoqb10y7gSXBGs0A+4VCQK+g=
|
||||
github.com/tiktoken-go/tokenizer v0.6.2/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
@@ -220,25 +251,36 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -247,18 +289,29 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
@@ -273,6 +326,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
package common
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"log"
|
||||
"one-api/common"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
loggerINFO = "INFO"
|
||||
loggerWarn = "WARN"
|
||||
loggerError = "ERR"
|
||||
loggerDebug = "DEBUG"
|
||||
)
|
||||
|
||||
const maxLogCount = 1000000
|
||||
@@ -27,7 +30,10 @@ var setupLogLock sync.Mutex
|
||||
var setupLogWorking bool
|
||||
|
||||
func SetupLogger() {
|
||||
if *LogDir != "" {
|
||||
defer func() {
|
||||
setupLogWorking = false
|
||||
}()
|
||||
if *common.LogDir != "" {
|
||||
ok := setupLogLock.TryLock()
|
||||
if !ok {
|
||||
log.Println("setup log is already working")
|
||||
@@ -35,9 +41,8 @@ func SetupLogger() {
|
||||
}
|
||||
defer func() {
|
||||
setupLogLock.Unlock()
|
||||
setupLogWorking = false
|
||||
}()
|
||||
logPath := filepath.Join(*LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102150405")))
|
||||
logPath := filepath.Join(*common.LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102150405")))
|
||||
fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatal("failed to open log file")
|
||||
@@ -47,16 +52,6 @@ func SetupLogger() {
|
||||
}
|
||||
}
|
||||
|
||||
func SysLog(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
}
|
||||
|
||||
func SysError(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
}
|
||||
|
||||
func LogInfo(ctx context.Context, msg string) {
|
||||
logHelper(ctx, loggerINFO, msg)
|
||||
}
|
||||
@@ -69,12 +64,18 @@ func LogError(ctx context.Context, msg string) {
|
||||
logHelper(ctx, loggerError, msg)
|
||||
}
|
||||
|
||||
func LogDebug(ctx context.Context, msg string) {
|
||||
if common.DebugEnabled {
|
||||
logHelper(ctx, loggerDebug, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func logHelper(ctx context.Context, level string, msg string) {
|
||||
writer := gin.DefaultErrorWriter
|
||||
if level == loggerINFO {
|
||||
writer = gin.DefaultWriter
|
||||
}
|
||||
id := ctx.Value(RequestIdKey)
|
||||
id := ctx.Value(common.RequestIdKey)
|
||||
if id == nil {
|
||||
id = "SYSTEM"
|
||||
}
|
||||
@@ -90,23 +91,17 @@ func logHelper(ctx context.Context, level string, msg string) {
|
||||
}
|
||||
}
|
||||
|
||||
func FatalLog(v ...any) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func LogQuota(quota int) string {
|
||||
if DisplayInCurrencyEnabled {
|
||||
return fmt.Sprintf("$%.6f 额度", float64(quota)/QuotaPerUnit)
|
||||
if common.DisplayInCurrencyEnabled {
|
||||
return fmt.Sprintf("$%.6f 额度", float64(quota)/common.QuotaPerUnit)
|
||||
} else {
|
||||
return fmt.Sprintf("%d 点额度", quota)
|
||||
}
|
||||
}
|
||||
|
||||
func FormatQuota(quota int) string {
|
||||
if DisplayInCurrencyEnabled {
|
||||
return fmt.Sprintf("$%.6f", float64(quota)/QuotaPerUnit)
|
||||
if common.DisplayInCurrencyEnabled {
|
||||
return fmt.Sprintf("$%.6f", float64(quota)/common.QuotaPerUnit)
|
||||
} else {
|
||||
return fmt.Sprintf("%d", quota)
|
||||
}
|
||||
9
main.go
@@ -8,6 +8,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/controller"
|
||||
"one-api/logger"
|
||||
"one-api/middleware"
|
||||
"one-api/model"
|
||||
"one-api/router"
|
||||
@@ -60,13 +61,13 @@ func main() {
|
||||
}
|
||||
if common.MemoryCacheEnabled {
|
||||
common.SysLog("memory cache enabled")
|
||||
common.SysError(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
||||
common.SysLog(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
||||
|
||||
// Add panic recovery and retry for InitChannelCache
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.SysError(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r))
|
||||
common.SysLog(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r))
|
||||
// Retry once
|
||||
_, _, fixErr := model.FixAbility()
|
||||
if fixErr != nil {
|
||||
@@ -125,7 +126,7 @@ func main() {
|
||||
// Initialize HTTP server
|
||||
server := gin.New()
|
||||
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
||||
common.SysError(fmt.Sprintf("panic detected: %v", err))
|
||||
common.SysLog(fmt.Sprintf("panic detected: %v", err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": gin.H{
|
||||
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
|
||||
@@ -171,7 +172,7 @@ func InitResources() error {
|
||||
// 加载环境变量
|
||||
common.InitEnv()
|
||||
|
||||
common.SetupLogger()
|
||||
logger.SetupLogger()
|
||||
|
||||
// Initialize model settings
|
||||
ratio_setting.InitRatioSettings()
|
||||
|
||||
@@ -194,14 +194,15 @@ func TokenAuth() func(c *gin.Context) {
|
||||
}
|
||||
// 检查path包含/v1/messages
|
||||
if strings.Contains(c.Request.URL.Path, "/v1/messages") {
|
||||
// 从x-api-key中获取key
|
||||
key := c.Request.Header.Get("x-api-key")
|
||||
if key != "" {
|
||||
c.Request.Header.Set("Authorization", "Bearer "+key)
|
||||
anthropicKey := c.Request.Header.Get("x-api-key")
|
||||
if anthropicKey != "" {
|
||||
c.Request.Header.Set("Authorization", "Bearer "+anthropicKey)
|
||||
}
|
||||
}
|
||||
// gemini api 从query中获取key
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") || strings.HasPrefix(c.Request.URL.Path, "/v1/models/") {
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/v1beta/openai/models") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/v1/models/") {
|
||||
skKey := c.Query("key")
|
||||
if skKey != "" {
|
||||
c.Request.Header.Set("Authorization", "Bearer "+skKey)
|
||||
|
||||
@@ -107,11 +107,11 @@ func Distribute() func(c *gin.Context) {
|
||||
// common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
|
||||
// message = "数据库一致性已被破坏,请联系管理员"
|
||||
//}
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, message)
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, message, string(types.ErrorCodeModelNotFound))
|
||||
return
|
||||
}
|
||||
if channel == nil {
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", userGroup, modelRequest.Model))
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", userGroup, modelRequest.Model), string(types.ErrorCodeModelNotFound))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,9 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
relayMode = relayconstant.RelayModeVideoFetchByID
|
||||
shouldSelectChannel = false
|
||||
}
|
||||
c.Set("relay_mode", relayMode)
|
||||
if _, ok := c.Get("relay_mode"); !ok {
|
||||
c.Set("relay_mode", relayMode)
|
||||
}
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") || strings.HasPrefix(c.Request.URL.Path, "/v1/models/") {
|
||||
// Gemini API 路径处理: /v1beta/models/gemini-2.0-flash:generateContent
|
||||
relayMode := relayconstant.RelayModeGemini
|
||||
@@ -244,6 +246,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
||||
common.SetContextKey(c, constant.ContextKeyChannelType, channel.Type)
|
||||
common.SetContextKey(c, constant.ContextKeyChannelCreateTime, channel.CreatedTime)
|
||||
common.SetContextKey(c, constant.ContextKeyChannelSetting, channel.GetSetting())
|
||||
common.SetContextKey(c, constant.ContextKeyChannelOtherSetting, channel.GetOtherSettings())
|
||||
common.SetContextKey(c, constant.ContextKeyChannelParamOverride, channel.GetParamOverride())
|
||||
if nil != channel.OpenAIOrganization && *channel.OpenAIOrganization != "" {
|
||||
common.SetContextKey(c, constant.ContextKeyChannelOrganization, *channel.OpenAIOrganization)
|
||||
@@ -267,6 +270,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
||||
common.SetContextKey(c, constant.ContextKeyChannelKey, key)
|
||||
common.SetContextKey(c, constant.ContextKeyChannelBaseUrl, channel.GetBaseURL())
|
||||
|
||||
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, false)
|
||||
|
||||
// TODO: api_version统一
|
||||
switch channel.Type {
|
||||
case constant.ChannelTypeAzure:
|
||||
|
||||
80
middleware/email-verification-rate-limit.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
EmailVerificationRateLimitMark = "EV"
|
||||
EmailVerificationMaxRequests = 2 // 30秒内最多2次
|
||||
EmailVerificationDuration = 30 // 30秒时间窗口
|
||||
)
|
||||
|
||||
func redisEmailVerificationRateLimiter(c *gin.Context) {
|
||||
ctx := context.Background()
|
||||
rdb := common.RDB
|
||||
key := "emailVerification:" + EmailVerificationRateLimitMark + ":" + c.ClientIP()
|
||||
|
||||
count, err := rdb.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
// fallback
|
||||
memoryEmailVerificationRateLimiter(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 第一次设置键时设置过期时间
|
||||
if count == 1 {
|
||||
_ = rdb.Expire(ctx, key, time.Duration(EmailVerificationDuration)*time.Second).Err()
|
||||
}
|
||||
|
||||
// 检查是否超出限制
|
||||
if count <= int64(EmailVerificationMaxRequests) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取剩余等待时间
|
||||
ttl, err := rdb.TTL(ctx, key).Result()
|
||||
waitSeconds := int64(EmailVerificationDuration)
|
||||
if err == nil && ttl > 0 {
|
||||
waitSeconds = int64(ttl.Seconds())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("发送过于频繁,请等待 %d 秒后再试", waitSeconds),
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func memoryEmailVerificationRateLimiter(c *gin.Context) {
|
||||
key := EmailVerificationRateLimitMark + ":" + c.ClientIP()
|
||||
|
||||
if !inMemoryRateLimiter.Request(key, EmailVerificationMaxRequests, EmailVerificationDuration) {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"success": false,
|
||||
"message": "发送过于频繁,请稍后再试",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func EmailVerificationRateLimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if common.RedisEnabled {
|
||||
redisEmailVerificationRateLimiter(c)
|
||||
} else {
|
||||
inMemoryRateLimiter.Init(common.RateLimitKeyExpirationDuration)
|
||||
memoryEmailVerificationRateLimiter(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
66
middleware/jimeng_adapter.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
relayconstant "one-api/relay/constant"
|
||||
)
|
||||
|
||||
func JimengRequestConvert() func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
action := c.Query("Action")
|
||||
if action == "" {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "Action query parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Jimeng official API request
|
||||
var originalReq map[string]interface{}
|
||||
if err := common.UnmarshalBodyReusable(c, &originalReq); err != nil {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
model, _ := originalReq["req_key"].(string)
|
||||
prompt, _ := originalReq["prompt"].(string)
|
||||
|
||||
unifiedReq := map[string]interface{}{
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"metadata": originalReq,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(unifiedReq)
|
||||
if err != nil {
|
||||
abortWithOpenAiMessage(c, http.StatusInternalServerError, "Failed to marshal request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Update request body
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData))
|
||||
c.Set(common.KeyRequestBody, jsonData)
|
||||
|
||||
if image, ok := originalReq["image"]; !ok || image == "" {
|
||||
c.Set("action", constant.TaskActionTextGenerate)
|
||||
}
|
||||
|
||||
c.Request.URL.Path = "/v1/video/generations"
|
||||
|
||||
if action == "CVSync2AsyncGetResult" {
|
||||
taskId, ok := originalReq["task_id"].(string)
|
||||
if !ok || taskId == "" {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "task_id is required for CVSync2AsyncGetResult")
|
||||
return
|
||||
}
|
||||
c.Request.URL.Path = "/v1/video/generations/" + taskId
|
||||
c.Request.Method = http.MethodGet
|
||||
c.Set("task_id", taskId)
|
||||
c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ func RelayPanicRecover() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
common.SysError(fmt.Sprintf("panic detected: %v", err))
|
||||
common.SysError(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
|
||||
common.SysLog(fmt.Sprintf("panic detected: %v", err))
|
||||
common.SysLog(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": gin.H{
|
||||
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
|
||||
|
||||
@@ -37,7 +37,7 @@ func TurnstileCheck() gin.HandlerFunc {
|
||||
"remoteip": {c.ClientIP()},
|
||||
})
|
||||
if err != nil {
|
||||
common.SysError(err.Error())
|
||||
common.SysLog(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
@@ -49,7 +49,7 @@ func TurnstileCheck() gin.HandlerFunc {
|
||||
var res turnstileCheckResponse
|
||||
err = json.NewDecoder(rawRes.Body).Decode(&res)
|
||||
if err != nil {
|
||||
common.SysError(err.Error())
|
||||
common.SysLog(err.Error())
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
|
||||
@@ -4,18 +4,24 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
)
|
||||
|
||||
func abortWithOpenAiMessage(c *gin.Context, statusCode int, message string) {
|
||||
func abortWithOpenAiMessage(c *gin.Context, statusCode int, message string, code ...string) {
|
||||
codeStr := ""
|
||||
if len(code) > 0 {
|
||||
codeStr = code[0]
|
||||
}
|
||||
userId := c.GetInt("id")
|
||||
c.JSON(statusCode, gin.H{
|
||||
"error": gin.H{
|
||||
"message": common.MessageWithRequestId(message, c.GetString(common.RequestIdKey)),
|
||||
"type": "new_api_error",
|
||||
"code": codeStr,
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
common.LogError(c.Request.Context(), fmt.Sprintf("user %d | %s", userId, message))
|
||||
logger.LogError(c.Request.Context(), fmt.Sprintf("user %d | %s", userId, message))
|
||||
}
|
||||
|
||||
func abortWithMidjourneyMessage(c *gin.Context, statusCode int, code int, description string) {
|
||||
@@ -25,5 +31,5 @@ func abortWithMidjourneyMessage(c *gin.Context, statusCode int, code int, descri
|
||||
"code": code,
|
||||
})
|
||||
c.Abort()
|
||||
common.LogError(c.Request.Context(), description)
|
||||
logger.LogError(c.Request.Context(), description)
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
|
||||
return &channel, err
|
||||
}
|
||||
|
||||
func (channel *Channel) AddAbilities() error {
|
||||
func (channel *Channel) AddAbilities(tx *gorm.DB) error {
|
||||
models_ := strings.Split(channel.Models, ",")
|
||||
groups_ := strings.Split(channel.Group, ",")
|
||||
abilitySet := make(map[string]struct{})
|
||||
@@ -169,8 +169,13 @@ func (channel *Channel) AddAbilities() error {
|
||||
if len(abilities) == 0 {
|
||||
return nil
|
||||
}
|
||||
// choose DB or provided tx
|
||||
useDB := DB
|
||||
if tx != nil {
|
||||
useDB = tx
|
||||
}
|
||||
for _, chunk := range lo.Chunk(abilities, 50) {
|
||||
err := DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&chunk).Error
|
||||
err := useDB.Clauses(clause.OnConflict{DoNothing: true}).Create(&chunk).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -289,13 +294,13 @@ func FixAbility() (int, int, error) {
|
||||
if common.UsingSQLite {
|
||||
err := DB.Exec("DELETE FROM abilities").Error
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Delete abilities failed: %s", err.Error()))
|
||||
common.SysLog(fmt.Sprintf("Delete abilities failed: %s", err.Error()))
|
||||
return 0, 0, err
|
||||
}
|
||||
} else {
|
||||
err := DB.Exec("TRUNCATE TABLE abilities").Error
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Truncate abilities failed: %s", err.Error()))
|
||||
common.SysLog(fmt.Sprintf("Truncate abilities failed: %s", err.Error()))
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
@@ -315,15 +320,15 @@ func FixAbility() (int, int, error) {
|
||||
// Delete all abilities of this channel
|
||||
err = DB.Where("channel_id IN ?", ids).Delete(&Ability{}).Error
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Delete abilities failed: %s", err.Error()))
|
||||
common.SysLog(fmt.Sprintf("Delete abilities failed: %s", err.Error()))
|
||||
failCount += len(chunk)
|
||||
continue
|
||||
}
|
||||
// Then add new abilities
|
||||
for _, channel := range chunk {
|
||||
err = channel.AddAbilities()
|
||||
err = channel.AddAbilities(nil)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Add abilities for channel %d failed: %s", channel.Id, err.Error()))
|
||||
common.SysLog(fmt.Sprintf("Add abilities for channel %d failed: %s", channel.Id, err.Error()))
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
|
||||
115
model/channel.go
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -41,7 +42,7 @@ type Channel struct {
|
||||
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
||||
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
||||
OtherInfo string `json:"other_info"`
|
||||
Settings string `json:"settings"`
|
||||
OtherSettings string `json:"settings" gorm:"column:settings"` // 其他设置
|
||||
Tag *string `json:"tag" gorm:"index"`
|
||||
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
|
||||
ParamOverride *string `json:"param_override" gorm:"type:text"`
|
||||
@@ -208,7 +209,7 @@ func (channel *Channel) GetOtherInfo() map[string]interface{} {
|
||||
if channel.OtherInfo != "" {
|
||||
err := common.Unmarshal([]byte(channel.OtherInfo), &otherInfo)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal other info: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to unmarshal other info: channel_id=%d, tag=%s, name=%s, error=%v", channel.Id, channel.GetTag(), channel.Name, err))
|
||||
}
|
||||
}
|
||||
return otherInfo
|
||||
@@ -217,7 +218,7 @@ func (channel *Channel) GetOtherInfo() map[string]interface{} {
|
||||
func (channel *Channel) SetOtherInfo(otherInfo map[string]interface{}) {
|
||||
otherInfoBytes, err := json.Marshal(otherInfo)
|
||||
if err != nil {
|
||||
common.SysError("failed to marshal other info: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to marshal other info: channel_id=%d, tag=%s, name=%s, error=%v", channel.Id, channel.GetTag(), channel.Name, err))
|
||||
return
|
||||
}
|
||||
channel.OtherInfo = string(otherInfoBytes)
|
||||
@@ -337,38 +338,54 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) {
|
||||
}
|
||||
|
||||
func BatchInsertChannels(channels []Channel) error {
|
||||
var err error
|
||||
err = DB.Create(&channels).Error
|
||||
if err != nil {
|
||||
return err
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, channel_ := range channels {
|
||||
err = channel_.AddAbilities()
|
||||
if err != nil {
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, chunk := range lo.Chunk(channels, 50) {
|
||||
if err := tx.Create(&chunk).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
for _, channel_ := range chunk {
|
||||
if err := channel_.AddAbilities(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func BatchDeleteChannels(ids []int) error {
|
||||
//使用事务 删除channel表和channel_ability表
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 使用事务 分批删除channel表和abilities表
|
||||
tx := DB.Begin()
|
||||
err := tx.Where("id in (?)", ids).Delete(&Channel{}).Error
|
||||
if err != nil {
|
||||
// 回滚事务
|
||||
tx.Rollback()
|
||||
return err
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
err = tx.Where("channel_id in (?)", ids).Delete(&Ability{}).Error
|
||||
if err != nil {
|
||||
// 回滚事务
|
||||
tx.Rollback()
|
||||
return err
|
||||
for _, chunk := range lo.Chunk(ids, 200) {
|
||||
if err := tx.Where("id in (?)", chunk).Delete(&Channel{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("channel_id in (?)", chunk).Delete(&Ability{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 提交事务
|
||||
tx.Commit()
|
||||
return err
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func (channel *Channel) GetPriority() int64 {
|
||||
@@ -389,7 +406,11 @@ func (channel *Channel) GetBaseURL() string {
|
||||
if channel.BaseURL == nil {
|
||||
return ""
|
||||
}
|
||||
return *channel.BaseURL
|
||||
url := *channel.BaseURL
|
||||
if url == "" {
|
||||
url = constant.ChannelBaseURLs[channel.Type]
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (channel *Channel) GetModelMapping() string {
|
||||
@@ -412,7 +433,7 @@ func (channel *Channel) Insert() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = channel.AddAbilities()
|
||||
err = channel.AddAbilities(nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -471,7 +492,7 @@ func (channel *Channel) UpdateResponseTime(responseTime int64) {
|
||||
ResponseTime: int(responseTime),
|
||||
}).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to update response time: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to update response time: channel_id=%d, error=%v", channel.Id, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +502,7 @@ func (channel *Channel) UpdateBalance(balance float64) {
|
||||
Balance: balance,
|
||||
}).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to update balance: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to update balance: channel_id=%d, error=%v", channel.Id, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +618,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
|
||||
if shouldUpdateAbilities {
|
||||
err := UpdateAbilityStatus(channelId, status == common.ChannelStatusEnabled)
|
||||
if err != nil {
|
||||
common.SysError("failed to update ability status: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to update ability status: channel_id=%d, error=%v", channelId, err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -625,7 +646,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
|
||||
}
|
||||
err = channel.Save()
|
||||
if err != nil {
|
||||
common.SysError("failed to update channel status: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to update channel status: channel_id=%d, status=%d, error=%v", channel.Id, status, err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -687,7 +708,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
||||
for _, channel := range channels {
|
||||
err = channel.UpdateAbilities(nil)
|
||||
if err != nil {
|
||||
common.SysError("failed to update abilities: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to update abilities: channel_id=%d, tag=%s, error=%v", channel.Id, channel.GetTag(), err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,7 +732,7 @@ func UpdateChannelUsedQuota(id int, quota int) {
|
||||
func updateChannelUsedQuota(id int, quota int) {
|
||||
err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to update channel used quota: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to update channel used quota: channel_id=%d, delta_quota=%d, error=%v", id, quota, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,7 +825,7 @@ func (channel *Channel) GetSetting() dto.ChannelSettings {
|
||||
if channel.Setting != nil && *channel.Setting != "" {
|
||||
err := common.Unmarshal([]byte(*channel.Setting), &setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal setting: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to unmarshal setting: channel_id=%d, error=%v", channel.Id, err))
|
||||
channel.Setting = nil // 清空设置以避免后续错误
|
||||
_ = channel.Save() // 保存修改
|
||||
}
|
||||
@@ -815,18 +836,40 @@ func (channel *Channel) GetSetting() dto.ChannelSettings {
|
||||
func (channel *Channel) SetSetting(setting dto.ChannelSettings) {
|
||||
settingBytes, err := common.Marshal(setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to marshal setting: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to marshal setting: channel_id=%d, error=%v", channel.Id, err))
|
||||
return
|
||||
}
|
||||
channel.Setting = common.GetPointer[string](string(settingBytes))
|
||||
}
|
||||
|
||||
func (channel *Channel) GetOtherSettings() dto.ChannelOtherSettings {
|
||||
setting := dto.ChannelOtherSettings{}
|
||||
if channel.OtherSettings != "" {
|
||||
err := common.UnmarshalJsonStr(channel.OtherSettings, &setting)
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("failed to unmarshal setting: channel_id=%d, error=%v", channel.Id, err))
|
||||
channel.OtherSettings = "{}" // 清空设置以避免后续错误
|
||||
_ = channel.Save() // 保存修改
|
||||
}
|
||||
}
|
||||
return setting
|
||||
}
|
||||
|
||||
func (channel *Channel) SetOtherSettings(setting dto.ChannelOtherSettings) {
|
||||
settingBytes, err := common.Marshal(setting)
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("failed to marshal setting: channel_id=%d, error=%v", channel.Id, err))
|
||||
return
|
||||
}
|
||||
channel.OtherSettings = string(settingBytes)
|
||||
}
|
||||
|
||||
func (channel *Channel) GetParamOverride() map[string]interface{} {
|
||||
paramOverride := make(map[string]interface{})
|
||||
if channel.ParamOverride != nil && *channel.ParamOverride != "" {
|
||||
err := common.Unmarshal([]byte(*channel.ParamOverride), ¶mOverride)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal param override: " + err.Error())
|
||||
common.SysLog(fmt.Sprintf("failed to unmarshal param override: channel_id=%d, error=%v", channel.Id, err))
|
||||
}
|
||||
}
|
||||
return paramOverride
|
||||
|
||||
27
model/log.go
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
"one-api/types"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -87,13 +89,13 @@ func RecordLog(userId int, logType int, content string) {
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to record log: " + err.Error())
|
||||
common.SysLog("failed to record log: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int,
|
||||
isStream bool, group string, other map[string]interface{}) {
|
||||
common.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
|
||||
logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
|
||||
username := c.GetString("username")
|
||||
otherStr := common.MapToJsonStr(other)
|
||||
// 判断是否需要记录 IP
|
||||
@@ -129,7 +131,7 @@ func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string,
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
common.LogError(c, "failed to record log: "+err.Error())
|
||||
logger.LogError(c, "failed to record log: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +144,6 @@ type RecordConsumeLogParams struct {
|
||||
Quota int `json:"quota"`
|
||||
Content string `json:"content"`
|
||||
TokenId int `json:"token_id"`
|
||||
UserQuota int `json:"user_quota"`
|
||||
UseTimeSeconds int `json:"use_time_seconds"`
|
||||
IsStream bool `json:"is_stream"`
|
||||
Group string `json:"group"`
|
||||
@@ -150,10 +151,10 @@ type RecordConsumeLogParams struct {
|
||||
}
|
||||
|
||||
func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) {
|
||||
common.LogInfo(c, fmt.Sprintf("record consume log: userId=%d, params=%s", userId, common.GetJsonString(params)))
|
||||
if !common.LogConsumeEnabled {
|
||||
return
|
||||
}
|
||||
logger.LogInfo(c, fmt.Sprintf("record consume log: userId=%d, params=%s", userId, common.GetJsonString(params)))
|
||||
username := c.GetString("username")
|
||||
otherStr := common.MapToJsonStr(params.Other)
|
||||
// 判断是否需要记录 IP
|
||||
@@ -189,7 +190,7 @@ func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams)
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
common.LogError(c, "failed to record log: "+err.Error())
|
||||
logger.LogError(c, "failed to record log: "+err.Error())
|
||||
}
|
||||
if common.DataExportEnabled {
|
||||
gopool.Go(func() {
|
||||
@@ -236,26 +237,22 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
channelIdsMap := make(map[int]struct{})
|
||||
channelMap := make(map[int]string)
|
||||
channelIds := types.NewSet[int]()
|
||||
for _, log := range logs {
|
||||
if log.ChannelId != 0 {
|
||||
channelIdsMap[log.ChannelId] = struct{}{}
|
||||
channelIds.Add(log.ChannelId)
|
||||
}
|
||||
}
|
||||
|
||||
channelIds := make([]int, 0, len(channelIdsMap))
|
||||
for channelId := range channelIdsMap {
|
||||
channelIds = append(channelIds, channelId)
|
||||
}
|
||||
if len(channelIds) > 0 {
|
||||
if channelIds.Len() > 0 {
|
||||
var channels []struct {
|
||||
Id int `gorm:"column:id"`
|
||||
Name string `gorm:"column:name"`
|
||||
}
|
||||
if err = DB.Table("channels").Select("id, name").Where("id IN ?", channelIds).Find(&channels).Error; err != nil {
|
||||
if err = DB.Table("channels").Select("id, name").Where("id IN ?", channelIds.Items()).Find(&channels).Error; err != nil {
|
||||
return logs, total, err
|
||||
}
|
||||
channelMap := make(map[int]string, len(channels))
|
||||
for _, channel := range channels {
|
||||
channelMap[channel.Id] = channel.Name
|
||||
}
|
||||
|
||||
143
model/main.go
@@ -64,6 +64,22 @@ var DB *gorm.DB
|
||||
|
||||
var LOG_DB *gorm.DB
|
||||
|
||||
// dropIndexIfExists drops a MySQL index only if it exists to avoid noisy 1091 errors
|
||||
func dropIndexIfExists(tableName string, indexName string) {
|
||||
if !common.UsingMySQL {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
// Check index existence via information_schema
|
||||
err := DB.Raw(
|
||||
"SELECT COUNT(1) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?",
|
||||
tableName, indexName,
|
||||
).Scan(&count).Error
|
||||
if err == nil && count > 0 {
|
||||
_ = DB.Exec("ALTER TABLE " + tableName + " DROP INDEX " + indexName + ";").Error
|
||||
}
|
||||
}
|
||||
|
||||
func createRootAccountIfNeed() error {
|
||||
var user User
|
||||
//if user.Status != common.UserStatusEnabled {
|
||||
@@ -180,6 +196,12 @@ func InitDB() (err error) {
|
||||
db = db.Debug()
|
||||
}
|
||||
DB = db
|
||||
// MySQL charset/collation startup check: ensure Chinese-capable charset
|
||||
if common.UsingMySQL {
|
||||
if err := checkMySQLChineseSupport(DB); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -214,6 +236,12 @@ func InitLogDB() (err error) {
|
||||
db = db.Debug()
|
||||
}
|
||||
LOG_DB = db
|
||||
// If log DB is MySQL, also ensure Chinese-capable charset
|
||||
if common.LogSqlType == common.DatabaseTypeMySQL {
|
||||
if err := checkMySQLChineseSupport(LOG_DB); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
sqlDB, err := LOG_DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,9 +263,16 @@ func InitLogDB() (err error) {
|
||||
}
|
||||
|
||||
func migrateDB() error {
|
||||
if !common.UsingPostgreSQL {
|
||||
return migrateDBFast()
|
||||
}
|
||||
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
|
||||
// 删除单列唯一索引(列级 UNIQUE)及早期命名方式,防止与新复合唯一索引 (model_name, deleted_at) 冲突
|
||||
dropIndexIfExists("models", "uk_model_name") // 新版复合索引名称(若已存在)
|
||||
dropIndexIfExists("models", "model_name") // 旧版列级唯一索引名称
|
||||
|
||||
dropIndexIfExists("vendors", "uk_vendor_name") // 新版复合索引名称(若已存在)
|
||||
dropIndexIfExists("vendors", "name") // 旧版列级唯一索引名称
|
||||
//if !common.UsingPostgreSQL {
|
||||
// return migrateDBFast()
|
||||
//}
|
||||
err := DB.AutoMigrate(
|
||||
&Channel{},
|
||||
&Token{},
|
||||
@@ -264,6 +299,14 @@ func migrateDB() error {
|
||||
}
|
||||
|
||||
func migrateDBFast() error {
|
||||
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
|
||||
// 删除单列唯一索引(列级 UNIQUE)及早期命名方式,防止与新复合唯一索引冲突
|
||||
dropIndexIfExists("models", "uk_model_name")
|
||||
dropIndexIfExists("models", "model_name")
|
||||
|
||||
dropIndexIfExists("vendors", "uk_vendor_name")
|
||||
dropIndexIfExists("vendors", "name")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
migrations := []struct {
|
||||
@@ -282,7 +325,7 @@ func migrateDBFast() error {
|
||||
{&QuotaData{}, "QuotaData"},
|
||||
{&Task{}, "Task"},
|
||||
{&Model{}, "Model"},
|
||||
{&Vendor{}, "Vendor"},
|
||||
{&Vendor{}, "Vendor"},
|
||||
{&PrefillGroup{}, "PrefillGroup"},
|
||||
{&Setup{}, "Setup"},
|
||||
{&TwoFA{}, "TwoFA"},
|
||||
@@ -342,6 +385,98 @@ func CloseDB() error {
|
||||
return closeDB(DB)
|
||||
}
|
||||
|
||||
// checkMySQLChineseSupport ensures the MySQL connection and current schema
|
||||
// default charset/collation can store Chinese characters. It allows common
|
||||
// Chinese-capable charsets (utf8mb4, utf8, gbk, big5, gb18030) and panics otherwise.
|
||||
func checkMySQLChineseSupport(db *gorm.DB) error {
|
||||
// 仅检测:当前库默认字符集/排序规则 + 各表的排序规则(隐含字符集)
|
||||
|
||||
// Read current schema defaults
|
||||
var schemaCharset, schemaCollation string
|
||||
err := db.Raw("SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()").Row().Scan(&schemaCharset, &schemaCollation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取当前库默认字符集/排序规则失败 / Failed to read schema default charset/collation: %v", err)
|
||||
}
|
||||
|
||||
toLower := func(s string) string { return strings.ToLower(s) }
|
||||
// Allowed charsets that can store Chinese text
|
||||
allowedCharsets := map[string]string{
|
||||
"utf8mb4": "utf8mb4_",
|
||||
"utf8": "utf8_",
|
||||
"gbk": "gbk_",
|
||||
"big5": "big5_",
|
||||
"gb18030": "gb18030_",
|
||||
}
|
||||
isChineseCapable := func(cs, cl string) bool {
|
||||
csLower := toLower(cs)
|
||||
clLower := toLower(cl)
|
||||
if prefix, ok := allowedCharsets[csLower]; ok {
|
||||
if clLower == "" {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(clLower, prefix)
|
||||
}
|
||||
// 如果仅提供了排序规则,尝试按排序规则前缀判断
|
||||
for _, prefix := range allowedCharsets {
|
||||
if strings.HasPrefix(clLower, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 1) 当前库默认值必须支持中文
|
||||
if !isChineseCapable(schemaCharset, schemaCollation) {
|
||||
return fmt.Errorf("当前库默认字符集/排序规则不支持中文:schema(%s/%s)。请将库设置为 utf8mb4/utf8/gbk/big5/gb18030 / Schema default charset/collation is not Chinese-capable: schema(%s/%s). Please set to utf8mb4/utf8/gbk/big5/gb18030",
|
||||
schemaCharset, schemaCollation, schemaCharset, schemaCollation)
|
||||
}
|
||||
|
||||
// 2) 所有物理表的排序规则(隐含字符集)必须支持中文
|
||||
type tableInfo struct {
|
||||
Name string
|
||||
Collation *string
|
||||
}
|
||||
var tables []tableInfo
|
||||
if err := db.Raw("SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'").Scan(&tables).Error; err != nil {
|
||||
return fmt.Errorf("读取表排序规则失败 / Failed to read table collations: %v", err)
|
||||
}
|
||||
|
||||
var badTables []string
|
||||
for _, t := range tables {
|
||||
// NULL 或空表示继承库默认设置,已在上面校验库默认,视为通过
|
||||
if t.Collation == nil || *t.Collation == "" {
|
||||
continue
|
||||
}
|
||||
cl := *t.Collation
|
||||
// 仅凭排序规则判断是否中文可用
|
||||
ok := false
|
||||
lower := strings.ToLower(cl)
|
||||
for _, prefix := range allowedCharsets {
|
||||
if strings.HasPrefix(lower, prefix) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
badTables = append(badTables, fmt.Sprintf("%s(%s)", t.Name, cl))
|
||||
}
|
||||
}
|
||||
|
||||
if len(badTables) > 0 {
|
||||
// 限制输出数量以避免日志过长
|
||||
maxShow := 20
|
||||
shown := badTables
|
||||
if len(shown) > maxShow {
|
||||
shown = shown[:maxShow]
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"存在不支持中文的表,请修复其排序规则/字符集。示例(最多展示 %d 项):%v / Found tables not Chinese-capable. Please fix their collation/charset. Examples (showing up to %d): %v",
|
||||
maxShow, shown, maxShow, shown,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
lastPingTime time.Time
|
||||
pingMutex sync.Mutex
|
||||
|
||||
@@ -2,29 +2,29 @@ package model
|
||||
|
||||
// GetMissingModels returns model names that are referenced in the system
|
||||
func GetMissingModels() ([]string, error) {
|
||||
// 1. 获取所有已启用模型(去重)
|
||||
models := GetEnabledModels()
|
||||
if len(models) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
// 1. 获取所有已启用模型(去重)
|
||||
models := GetEnabledModels()
|
||||
if len(models) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// 2. 查询已有的元数据模型名
|
||||
var existing []string
|
||||
if err := DB.Model(&Model{}).Where("model_name IN ?", models).Pluck("model_name", &existing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 2. 查询已有的元数据模型名
|
||||
var existing []string
|
||||
if err := DB.Model(&Model{}).Where("model_name IN ?", models).Pluck("model_name", &existing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingSet := make(map[string]struct{}, len(existing))
|
||||
for _, e := range existing {
|
||||
existingSet[e] = struct{}{}
|
||||
}
|
||||
existingSet := make(map[string]struct{}, len(existing))
|
||||
for _, e := range existing {
|
||||
existingSet[e] = struct{}{}
|
||||
}
|
||||
|
||||
// 3. 收集缺失模型
|
||||
var missing []string
|
||||
for _, name := range models {
|
||||
if _, ok := existingSet[name]; !ok {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
return missing, nil
|
||||
// 3. 收集缺失模型
|
||||
var missing []string
|
||||
for _, name := range models {
|
||||
if _, ok := existingSet[name]; !ok {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
return missing, nil
|
||||
}
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
package model
|
||||
|
||||
// GetModelEnableGroups 返回指定模型名称可用的用户分组列表。
|
||||
// 使用在 updatePricing() 中维护的缓存映射,O(1) 读取,适合高并发场景。
|
||||
func GetModelEnableGroups(modelName string) []string {
|
||||
// 确保缓存最新
|
||||
GetPricing()
|
||||
// 确保缓存最新
|
||||
GetPricing()
|
||||
|
||||
if modelName == "" {
|
||||
return make([]string, 0)
|
||||
}
|
||||
if modelName == "" {
|
||||
return make([]string, 0)
|
||||
}
|
||||
|
||||
modelEnableGroupsLock.RLock()
|
||||
groups, ok := modelEnableGroups[modelName]
|
||||
modelEnableGroupsLock.RUnlock()
|
||||
if !ok {
|
||||
return make([]string, 0)
|
||||
}
|
||||
return groups
|
||||
modelEnableGroupsLock.RLock()
|
||||
groups, ok := modelEnableGroups[modelName]
|
||||
modelEnableGroupsLock.RUnlock()
|
||||
if !ok {
|
||||
return make([]string, 0)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// GetModelQuotaType 返回指定模型的计费类型(quota_type)。
|
||||
// 同样使用缓存映射,避免每次遍历定价切片。
|
||||
func GetModelQuotaType(modelName string) int {
|
||||
GetPricing()
|
||||
// GetModelQuotaTypes 返回指定模型的计费类型集合(来自缓存)
|
||||
func GetModelQuotaTypes(modelName string) []int {
|
||||
GetPricing()
|
||||
|
||||
modelEnableGroupsLock.RLock()
|
||||
quota, ok := modelQuotaTypeMap[modelName]
|
||||
modelEnableGroupsLock.RUnlock()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return quota
|
||||
modelEnableGroupsLock.RLock()
|
||||
quota, ok := modelQuotaTypeMap[modelName]
|
||||
modelEnableGroupsLock.RUnlock()
|
||||
if !ok {
|
||||
return []int{}
|
||||
}
|
||||
return []int{quota}
|
||||
}
|
||||
|
||||
@@ -1,204 +1,146 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"one-api/common"
|
||||
"strconv"
|
||||
"strings"
|
||||
"one-api/common"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Model 用于存储模型的元数据,例如描述、标签等
|
||||
// ModelName 字段具有唯一性约束,确保每个模型只会出现一次
|
||||
// Tags 字段使用逗号分隔的字符串保存标签集合,后期可根据需要扩展为 JSON 类型
|
||||
// Status: 1 表示启用,0 表示禁用,保留以便后续功能扩展
|
||||
// CreatedTime 和 UpdatedTime 使用 Unix 时间戳(秒)保存方便跨数据库移植
|
||||
// DeletedAt 采用 GORM 的软删除特性,便于后续数据恢复
|
||||
//
|
||||
// 该表设计遵循第三范式(3NF):
|
||||
// 1. 每一列都与主键(Id 或 ModelName)直接相关
|
||||
// 2. 不存在部分依赖(ModelName 是唯一键)
|
||||
// 3. 不存在传递依赖(描述、标签等都依赖于 ModelName,而非依赖于其他非主键列)
|
||||
// 这样既保证了数据一致性,也方便后期扩展
|
||||
|
||||
// 模型名称匹配规则
|
||||
const (
|
||||
NameRuleExact = iota // 0 精确匹配
|
||||
NameRulePrefix // 1 前缀匹配
|
||||
NameRuleContains // 2 包含匹配
|
||||
NameRuleSuffix // 3 后缀匹配
|
||||
NameRuleExact = iota
|
||||
NameRulePrefix
|
||||
NameRuleContains
|
||||
NameRuleSuffix
|
||||
)
|
||||
|
||||
type BoundChannel struct {
|
||||
Name string `json:"name"`
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Type int `json:"type"`
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Id int `json:"id"`
|
||||
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,where:deleted_at IS NULL"`
|
||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
||||
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
|
||||
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
|
||||
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
Id int `json:"id"`
|
||||
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,priority:1"`
|
||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
||||
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
|
||||
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
|
||||
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
|
||||
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_model_name,priority:2"`
|
||||
|
||||
BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
|
||||
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
|
||||
QuotaType int `json:"quota_type" gorm:"-"`
|
||||
NameRule int `json:"name_rule" gorm:"default:0"`
|
||||
BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
|
||||
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
|
||||
QuotaTypes []int `json:"quota_types,omitempty" gorm:"-"`
|
||||
NameRule int `json:"name_rule" gorm:"default:0"`
|
||||
|
||||
MatchedModels []string `json:"matched_models,omitempty" gorm:"-"`
|
||||
MatchedCount int `json:"matched_count,omitempty" gorm:"-"`
|
||||
}
|
||||
|
||||
// Insert 创建新的模型元数据记录
|
||||
func (mi *Model) Insert() error {
|
||||
now := common.GetTimestamp()
|
||||
mi.CreatedTime = now
|
||||
mi.UpdatedTime = now
|
||||
return DB.Create(mi).Error
|
||||
now := common.GetTimestamp()
|
||||
mi.CreatedTime = now
|
||||
mi.UpdatedTime = now
|
||||
return DB.Create(mi).Error
|
||||
}
|
||||
|
||||
// IsModelNameDuplicated 检查模型名称是否重复(排除自身 ID)
|
||||
func IsModelNameDuplicated(id int, name string) (bool, error) {
|
||||
if name == "" {
|
||||
return false, nil
|
||||
}
|
||||
var cnt int64
|
||||
err := DB.Model(&Model{}).Where("model_name = ? AND id <> ?", name, id).Count(&cnt).Error
|
||||
return cnt > 0, err
|
||||
if name == "" {
|
||||
return false, nil
|
||||
}
|
||||
var cnt int64
|
||||
err := DB.Model(&Model{}).Where("model_name = ? AND id <> ?", name, id).Count(&cnt).Error
|
||||
return cnt > 0, err
|
||||
}
|
||||
|
||||
// Update 更新现有模型记录
|
||||
func (mi *Model) Update() error {
|
||||
// 仅更新需要变更的字段,避免覆盖 CreatedTime
|
||||
mi.UpdatedTime = common.GetTimestamp()
|
||||
|
||||
// 排除 created_time,其余字段自动更新,避免新增字段时需要维护列表
|
||||
return DB.Model(&Model{}).Where("id = ?", mi.Id).Omit("created_time").Updates(mi).Error
|
||||
mi.UpdatedTime = common.GetTimestamp()
|
||||
return DB.Session(&gorm.Session{AllowGlobalUpdate: false, FullSaveAssociations: false}).
|
||||
Model(&Model{}).
|
||||
Where("id = ?", mi.Id).
|
||||
Omit("created_time").
|
||||
Select("*").
|
||||
Updates(mi).Error
|
||||
}
|
||||
|
||||
// Delete 软删除模型记录
|
||||
func (mi *Model) Delete() error {
|
||||
return DB.Delete(mi).Error
|
||||
return DB.Delete(mi).Error
|
||||
}
|
||||
|
||||
// GetModelByName 根据模型名称查询元数据
|
||||
func GetModelByName(name string) (*Model, error) {
|
||||
var mi Model
|
||||
err := DB.Where("model_name = ?", name).First(&mi).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mi, nil
|
||||
}
|
||||
|
||||
// GetVendorModelCounts 统计每个供应商下模型数量(不受分页影响)
|
||||
func GetVendorModelCounts() (map[int64]int64, error) {
|
||||
var stats []struct {
|
||||
VendorID int64
|
||||
Count int64
|
||||
}
|
||||
if err := DB.Model(&Model{}).
|
||||
Select("vendor_id as vendor_id, count(*) as count").
|
||||
Group("vendor_id").
|
||||
Scan(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[int64]int64, len(stats))
|
||||
for _, s := range stats {
|
||||
m[s.VendorID] = s.Count
|
||||
}
|
||||
return m, nil
|
||||
var stats []struct {
|
||||
VendorID int64
|
||||
Count int64
|
||||
}
|
||||
if err := DB.Model(&Model{}).
|
||||
Select("vendor_id as vendor_id, count(*) as count").
|
||||
Group("vendor_id").
|
||||
Scan(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[int64]int64, len(stats))
|
||||
for _, s := range stats {
|
||||
m[s.VendorID] = s.Count
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// GetAllModels 分页获取所有模型元数据
|
||||
func GetAllModels(offset int, limit int) ([]*Model, error) {
|
||||
var models []*Model
|
||||
err := DB.Offset(offset).Limit(limit).Find(&models).Error
|
||||
return models, err
|
||||
var models []*Model
|
||||
err := DB.Order("id DESC").Offset(offset).Limit(limit).Find(&models).Error
|
||||
return models, err
|
||||
}
|
||||
|
||||
// GetBoundChannels 查询支持该模型的渠道(名称+类型)
|
||||
func GetBoundChannels(modelName string) ([]BoundChannel, error) {
|
||||
var channels []BoundChannel
|
||||
err := DB.Table("channels").
|
||||
Select("channels.name, channels.type").
|
||||
Joins("join abilities on abilities.channel_id = channels.id").
|
||||
Where("abilities.model = ? AND abilities.enabled = ?", modelName, true).
|
||||
Group("channels.id").
|
||||
Scan(&channels).Error
|
||||
return channels, err
|
||||
func GetBoundChannelsByModelsMap(modelNames []string) (map[string][]BoundChannel, error) {
|
||||
result := make(map[string][]BoundChannel)
|
||||
if len(modelNames) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
type row struct {
|
||||
Model string
|
||||
Name string
|
||||
Type int
|
||||
}
|
||||
var rows []row
|
||||
err := DB.Table("channels").
|
||||
Select("abilities.model as model, channels.name as name, channels.type as type").
|
||||
Joins("JOIN abilities ON abilities.channel_id = channels.id").
|
||||
Where("abilities.model IN ? AND abilities.enabled = ?", modelNames, true).
|
||||
Distinct().
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
result[r.Model] = append(result[r.Model], BoundChannel{Name: r.Name, Type: r.Type})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FindModelByNameWithRule 根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含
|
||||
func FindModelByNameWithRule(name string) (*Model, error) {
|
||||
// 1. 精确匹配
|
||||
if m, err := GetModelByName(name); err == nil {
|
||||
return m, nil
|
||||
}
|
||||
// 2. 规则匹配
|
||||
var models []*Model
|
||||
if err := DB.Where("name_rule <> ?", NameRuleExact).Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var prefixMatch, suffixMatch, containsMatch *Model
|
||||
for _, m := range models {
|
||||
switch m.NameRule {
|
||||
case NameRulePrefix:
|
||||
if strings.HasPrefix(name, m.ModelName) {
|
||||
if prefixMatch == nil || len(m.ModelName) > len(prefixMatch.ModelName) {
|
||||
prefixMatch = m
|
||||
}
|
||||
}
|
||||
case NameRuleSuffix:
|
||||
if strings.HasSuffix(name, m.ModelName) {
|
||||
if suffixMatch == nil || len(m.ModelName) > len(suffixMatch.ModelName) {
|
||||
suffixMatch = m
|
||||
}
|
||||
}
|
||||
case NameRuleContains:
|
||||
if strings.Contains(name, m.ModelName) {
|
||||
if containsMatch == nil || len(m.ModelName) > len(containsMatch.ModelName) {
|
||||
containsMatch = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if prefixMatch != nil {
|
||||
return prefixMatch, nil
|
||||
}
|
||||
if suffixMatch != nil {
|
||||
return suffixMatch, nil
|
||||
}
|
||||
if containsMatch != nil {
|
||||
return containsMatch, nil
|
||||
}
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// SearchModels 根据关键词和供应商搜索模型,支持分页
|
||||
func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Model, int64, error) {
|
||||
var models []*Model
|
||||
db := DB.Model(&Model{})
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
db = db.Where("model_name LIKE ? OR description LIKE ? OR tags LIKE ?", like, like, like)
|
||||
}
|
||||
if vendor != "" {
|
||||
// 如果是数字,按供应商 ID 精确匹配;否则按名称模糊匹配
|
||||
if vid, err := strconv.Atoi(vendor); err == nil {
|
||||
db = db.Where("models.vendor_id = ?", vid)
|
||||
} else {
|
||||
db = db.Joins("JOIN vendors ON vendors.id = models.vendor_id").Where("vendors.name LIKE ?", "%"+vendor+"%")
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = db.Offset(offset).Limit(limit).Order("models.id DESC").Find(&models).Error
|
||||
return models, total, err
|
||||
var models []*Model
|
||||
db := DB.Model(&Model{})
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
db = db.Where("model_name LIKE ? OR description LIKE ? OR tags LIKE ?", like, like, like)
|
||||
}
|
||||
if vendor != "" {
|
||||
if vid, err := strconv.Atoi(vendor); err == nil {
|
||||
db = db.Where("models.vendor_id = ?", vid)
|
||||
} else {
|
||||
db = db.Joins("JOIN vendors ON vendors.id = models.vendor_id").Where("vendors.name LIKE ?", "%"+vendor+"%")
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := db.Order("models.id DESC").Offset(offset).Limit(limit).Find(&models).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return models, total, nil
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func loadOptionsFromDatabase() {
|
||||
for _, option := range options {
|
||||
err := updateOptionMap(option.Key, option.Value)
|
||||
if err != nil {
|
||||
common.SysError("failed to update option map: " + err.Error())
|
||||
common.SysLog("failed to update option map: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
common.LinuxDOClientId = value
|
||||
case "LinuxDOClientSecret":
|
||||
common.LinuxDOClientSecret = value
|
||||
case "LinuxDOMinimumTrustLevel":
|
||||
common.LinuxDOMinimumTrustLevel, _ = strconv.Atoi(value)
|
||||
case "Footer":
|
||||
common.Footer = value
|
||||
case "SystemName":
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"database/sql/driver"
|
||||
"one-api/common"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PrefillGroup 用于存储可复用的“组”信息,例如模型组、标签组、端点组等。
|
||||
@@ -20,107 +20,107 @@ type JSONValue json.RawMessage
|
||||
|
||||
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||
func (j JSONValue) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(j), nil
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(j), nil
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner 接口,兼容不同驱动返回的类型
|
||||
func (j *JSONValue) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
*j = nil
|
||||
return nil
|
||||
case []byte:
|
||||
// 拷贝底层字节,避免保留底层缓冲区
|
||||
b := make([]byte, len(v))
|
||||
copy(b, v)
|
||||
*j = JSONValue(b)
|
||||
return nil
|
||||
case string:
|
||||
*j = JSONValue([]byte(v))
|
||||
return nil
|
||||
default:
|
||||
// 其他类型尝试序列化为 JSON
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*j = JSONValue(b)
|
||||
return nil
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
*j = nil
|
||||
return nil
|
||||
case []byte:
|
||||
// 拷贝底层字节,避免保留底层缓冲区
|
||||
b := make([]byte, len(v))
|
||||
copy(b, v)
|
||||
*j = JSONValue(b)
|
||||
return nil
|
||||
case string:
|
||||
*j = JSONValue([]byte(v))
|
||||
return nil
|
||||
default:
|
||||
// 其他类型尝试序列化为 JSON
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*j = JSONValue(b)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON 确保在对外编码时与 json.RawMessage 行为一致
|
||||
func (j JSONValue) MarshalJSON() ([]byte, error) {
|
||||
if j == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return j, nil
|
||||
if j == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON 确保在对外解码时与 json.RawMessage 行为一致
|
||||
func (j *JSONValue) UnmarshalJSON(data []byte) error {
|
||||
if data == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
b := make([]byte, len(data))
|
||||
copy(b, data)
|
||||
*j = JSONValue(b)
|
||||
return nil
|
||||
if data == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
b := make([]byte, len(data))
|
||||
copy(b, data)
|
||||
*j = JSONValue(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
type PrefillGroup struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name" gorm:"size:64;not null;uniqueIndex:uk_prefill_name,where:deleted_at IS NULL"`
|
||||
Type string `json:"type" gorm:"size:32;index;not null"`
|
||||
Items JSONValue `json:"items" gorm:"type:json"`
|
||||
Description string `json:"description,omitempty" gorm:"type:varchar(255)"`
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name" gorm:"size:64;not null;uniqueIndex:uk_prefill_name,where:deleted_at IS NULL"`
|
||||
Type string `json:"type" gorm:"size:32;index;not null"`
|
||||
Items JSONValue `json:"items" gorm:"type:json"`
|
||||
Description string `json:"description,omitempty" gorm:"type:varchar(255)"`
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
// Insert 新建组
|
||||
func (g *PrefillGroup) Insert() error {
|
||||
now := common.GetTimestamp()
|
||||
g.CreatedTime = now
|
||||
g.UpdatedTime = now
|
||||
return DB.Create(g).Error
|
||||
now := common.GetTimestamp()
|
||||
g.CreatedTime = now
|
||||
g.UpdatedTime = now
|
||||
return DB.Create(g).Error
|
||||
}
|
||||
|
||||
// IsPrefillGroupNameDuplicated 检查组名称是否重复(排除自身 ID)
|
||||
func IsPrefillGroupNameDuplicated(id int, name string) (bool, error) {
|
||||
if name == "" {
|
||||
return false, nil
|
||||
}
|
||||
var cnt int64
|
||||
err := DB.Model(&PrefillGroup{}).Where("name = ? AND id <> ?", name, id).Count(&cnt).Error
|
||||
return cnt > 0, err
|
||||
if name == "" {
|
||||
return false, nil
|
||||
}
|
||||
var cnt int64
|
||||
err := DB.Model(&PrefillGroup{}).Where("name = ? AND id <> ?", name, id).Count(&cnt).Error
|
||||
return cnt > 0, err
|
||||
}
|
||||
|
||||
// Update 更新组
|
||||
func (g *PrefillGroup) Update() error {
|
||||
g.UpdatedTime = common.GetTimestamp()
|
||||
return DB.Save(g).Error
|
||||
g.UpdatedTime = common.GetTimestamp()
|
||||
return DB.Save(g).Error
|
||||
}
|
||||
|
||||
// DeleteByID 根据 ID 删除组
|
||||
func DeletePrefillGroupByID(id int) error {
|
||||
return DB.Delete(&PrefillGroup{}, id).Error
|
||||
return DB.Delete(&PrefillGroup{}, id).Error
|
||||
}
|
||||
|
||||
// GetAllPrefillGroups 获取全部组,可按类型过滤(为空则返回全部)
|
||||
func GetAllPrefillGroups(groupType string) ([]*PrefillGroup, error) {
|
||||
var groups []*PrefillGroup
|
||||
query := DB.Model(&PrefillGroup{})
|
||||
if groupType != "" {
|
||||
query = query.Where("type = ?", groupType)
|
||||
}
|
||||
if err := query.Order("updated_time DESC").Find(&groups).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups, nil
|
||||
var groups []*PrefillGroup
|
||||
query := DB.Model(&PrefillGroup{})
|
||||
if groupType != "" {
|
||||
query = query.Where("type = ?", groupType)
|
||||
}
|
||||
if err := query.Order("updated_time DESC").Find(&groups).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
282
model/pricing.go
@@ -1,30 +1,31 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/setting/ratio_setting"
|
||||
"one-api/types"
|
||||
"sync"
|
||||
"time"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/setting/ratio_setting"
|
||||
"one-api/types"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Pricing struct {
|
||||
ModelName string `json:"model_name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
VendorID int `json:"vendor_id,omitempty"`
|
||||
QuotaType int `json:"quota_type"`
|
||||
ModelRatio float64 `json:"model_ratio"`
|
||||
ModelPrice float64 `json:"model_price"`
|
||||
OwnerBy string `json:"owner_by"`
|
||||
CompletionRatio float64 `json:"completion_ratio"`
|
||||
EnableGroup []string `json:"enable_groups"`
|
||||
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
|
||||
ModelName string `json:"model_name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
VendorID int `json:"vendor_id,omitempty"`
|
||||
QuotaType int `json:"quota_type"`
|
||||
ModelRatio float64 `json:"model_ratio"`
|
||||
ModelPrice float64 `json:"model_price"`
|
||||
OwnerBy string `json:"owner_by"`
|
||||
CompletionRatio float64 `json:"completion_ratio"`
|
||||
EnableGroup []string `json:"enable_groups"`
|
||||
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
|
||||
}
|
||||
|
||||
type PricingVendor struct {
|
||||
@@ -35,11 +36,11 @@ type PricingVendor struct {
|
||||
}
|
||||
|
||||
var (
|
||||
pricingMap []Pricing
|
||||
vendorsList []PricingVendor
|
||||
supportedEndpointMap map[string]common.EndpointInfo
|
||||
lastGetPricingTime time.Time
|
||||
updatePricingLock sync.Mutex
|
||||
pricingMap []Pricing
|
||||
vendorsList []PricingVendor
|
||||
supportedEndpointMap map[string]common.EndpointInfo
|
||||
lastGetPricingTime time.Time
|
||||
updatePricingLock sync.Mutex
|
||||
|
||||
// 缓存映射:模型名 -> 启用分组 / 计费类型
|
||||
modelEnableGroups = make(map[string][]string)
|
||||
@@ -91,7 +92,7 @@ func updatePricing() {
|
||||
//modelRatios := common.GetModelRatios()
|
||||
enableAbilities, err := GetAllEnableAbilityWithChannels()
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("GetAllEnableAbilityWithChannels error: %v", err))
|
||||
common.SysLog(fmt.Sprintf("GetAllEnableAbilityWithChannels error: %v", err))
|
||||
return
|
||||
}
|
||||
// 预加载模型元数据与供应商一次,避免循环查询
|
||||
@@ -121,19 +122,19 @@ func updatePricing() {
|
||||
for _, m := range prefixList {
|
||||
for _, pricingModel := range enableAbilities {
|
||||
if strings.HasPrefix(pricingModel.Model, m.ModelName) {
|
||||
if _, exists := metaMap[pricingModel.Model]; !exists {
|
||||
metaMap[pricingModel.Model] = m
|
||||
}
|
||||
}
|
||||
if _, exists := metaMap[pricingModel.Model]; !exists {
|
||||
metaMap[pricingModel.Model] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range suffixList {
|
||||
for _, pricingModel := range enableAbilities {
|
||||
if strings.HasSuffix(pricingModel.Model, m.ModelName) {
|
||||
if _, exists := metaMap[pricingModel.Model]; !exists {
|
||||
metaMap[pricingModel.Model] = m
|
||||
}
|
||||
}
|
||||
if _, exists := metaMap[pricingModel.Model]; !exists {
|
||||
metaMap[pricingModel.Model] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range containsList {
|
||||
@@ -179,34 +180,34 @@ func updatePricing() {
|
||||
//这里使用切片而不是Set,因为一个模型可能支持多个端点类型,并且第一个端点是优先使用端点
|
||||
modelSupportEndpointsStr := make(map[string][]string)
|
||||
|
||||
// 先根据已有能力填充原生端点
|
||||
for _, ability := range enableAbilities {
|
||||
endpoints := modelSupportEndpointsStr[ability.Model]
|
||||
channelTypes := common.GetEndpointTypesByChannelType(ability.ChannelType, ability.Model)
|
||||
for _, channelType := range channelTypes {
|
||||
if !common.StringsContains(endpoints, string(channelType)) {
|
||||
endpoints = append(endpoints, string(channelType))
|
||||
}
|
||||
}
|
||||
modelSupportEndpointsStr[ability.Model] = endpoints
|
||||
}
|
||||
// 先根据已有能力填充原生端点
|
||||
for _, ability := range enableAbilities {
|
||||
endpoints := modelSupportEndpointsStr[ability.Model]
|
||||
channelTypes := common.GetEndpointTypesByChannelType(ability.ChannelType, ability.Model)
|
||||
for _, channelType := range channelTypes {
|
||||
if !common.StringsContains(endpoints, string(channelType)) {
|
||||
endpoints = append(endpoints, string(channelType))
|
||||
}
|
||||
}
|
||||
modelSupportEndpointsStr[ability.Model] = endpoints
|
||||
}
|
||||
|
||||
// 再补充模型自定义端点
|
||||
for modelName, meta := range metaMap {
|
||||
if strings.TrimSpace(meta.Endpoints) == "" {
|
||||
continue
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(meta.Endpoints), &raw); err == nil {
|
||||
endpoints := modelSupportEndpointsStr[modelName]
|
||||
for k := range raw {
|
||||
if !common.StringsContains(endpoints, k) {
|
||||
endpoints = append(endpoints, k)
|
||||
}
|
||||
}
|
||||
modelSupportEndpointsStr[modelName] = endpoints
|
||||
}
|
||||
}
|
||||
// 再补充模型自定义端点
|
||||
for modelName, meta := range metaMap {
|
||||
if strings.TrimSpace(meta.Endpoints) == "" {
|
||||
continue
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(meta.Endpoints), &raw); err == nil {
|
||||
endpoints := modelSupportEndpointsStr[modelName]
|
||||
for k := range raw {
|
||||
if !common.StringsContains(endpoints, k) {
|
||||
endpoints = append(endpoints, k)
|
||||
}
|
||||
}
|
||||
modelSupportEndpointsStr[modelName] = endpoints
|
||||
}
|
||||
}
|
||||
|
||||
modelSupportEndpointTypes = make(map[string][]constant.EndpointType)
|
||||
for model, endpoints := range modelSupportEndpointsStr {
|
||||
@@ -216,92 +217,93 @@ func updatePricing() {
|
||||
supportedEndpoints = append(supportedEndpoints, endpointType)
|
||||
}
|
||||
modelSupportEndpointTypes[model] = supportedEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
// 构建全局 supportedEndpointMap(默认 + 自定义覆盖)
|
||||
supportedEndpointMap = make(map[string]common.EndpointInfo)
|
||||
// 1. 默认端点
|
||||
for _, endpoints := range modelSupportEndpointTypes {
|
||||
for _, et := range endpoints {
|
||||
if info, ok := common.GetDefaultEndpointInfo(et); ok {
|
||||
if _, exists := supportedEndpointMap[string(et)]; !exists {
|
||||
supportedEndpointMap[string(et)] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. 自定义端点(models 表)覆盖默认
|
||||
for _, meta := range metaMap {
|
||||
if strings.TrimSpace(meta.Endpoints) == "" {
|
||||
continue
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(meta.Endpoints), &raw); err == nil {
|
||||
for k, v := range raw {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
supportedEndpointMap[k] = common.EndpointInfo{Path: val, Method: "POST"}
|
||||
case map[string]interface{}:
|
||||
ep := common.EndpointInfo{Method: "POST"}
|
||||
if p, ok := val["path"].(string); ok {
|
||||
ep.Path = p
|
||||
}
|
||||
if m, ok := val["method"].(string); ok {
|
||||
ep.Method = strings.ToUpper(m)
|
||||
}
|
||||
supportedEndpointMap[k] = ep
|
||||
default:
|
||||
// ignore unsupported types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 构建全局 supportedEndpointMap(默认 + 自定义覆盖)
|
||||
supportedEndpointMap = make(map[string]common.EndpointInfo)
|
||||
// 1. 默认端点
|
||||
for _, endpoints := range modelSupportEndpointTypes {
|
||||
for _, et := range endpoints {
|
||||
if info, ok := common.GetDefaultEndpointInfo(et); ok {
|
||||
if _, exists := supportedEndpointMap[string(et)]; !exists {
|
||||
supportedEndpointMap[string(et)] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. 自定义端点(models 表)覆盖默认
|
||||
for _, meta := range metaMap {
|
||||
if strings.TrimSpace(meta.Endpoints) == "" {
|
||||
continue
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(meta.Endpoints), &raw); err == nil {
|
||||
for k, v := range raw {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
supportedEndpointMap[k] = common.EndpointInfo{Path: val, Method: "POST"}
|
||||
case map[string]interface{}:
|
||||
ep := common.EndpointInfo{Method: "POST"}
|
||||
if p, ok := val["path"].(string); ok {
|
||||
ep.Path = p
|
||||
}
|
||||
if m, ok := val["method"].(string); ok {
|
||||
ep.Method = strings.ToUpper(m)
|
||||
}
|
||||
supportedEndpointMap[k] = ep
|
||||
default:
|
||||
// ignore unsupported types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pricingMap = make([]Pricing, 0)
|
||||
for model, groups := range modelGroupsMap {
|
||||
pricing := Pricing{
|
||||
ModelName: model,
|
||||
EnableGroup: groups.Items(),
|
||||
SupportedEndpointTypes: modelSupportEndpointTypes[model],
|
||||
}
|
||||
pricingMap = make([]Pricing, 0)
|
||||
for model, groups := range modelGroupsMap {
|
||||
pricing := Pricing{
|
||||
ModelName: model,
|
||||
EnableGroup: groups.Items(),
|
||||
SupportedEndpointTypes: modelSupportEndpointTypes[model],
|
||||
}
|
||||
|
||||
// 补充模型元数据(描述、标签、供应商、状态)
|
||||
if meta, ok := metaMap[model]; ok {
|
||||
// 若模型被禁用(status!=1),则直接跳过,不返回给前端
|
||||
if meta.Status != 1 {
|
||||
continue
|
||||
}
|
||||
pricing.Description = meta.Description
|
||||
pricing.Tags = meta.Tags
|
||||
pricing.VendorID = meta.VendorID
|
||||
}
|
||||
modelPrice, findPrice := ratio_setting.GetModelPrice(model, false)
|
||||
if findPrice {
|
||||
pricing.ModelPrice = modelPrice
|
||||
pricing.QuotaType = 1
|
||||
} else {
|
||||
modelRatio, _, _ := ratio_setting.GetModelRatio(model)
|
||||
pricing.ModelRatio = modelRatio
|
||||
pricing.CompletionRatio = ratio_setting.GetCompletionRatio(model)
|
||||
pricing.QuotaType = 0
|
||||
}
|
||||
pricingMap = append(pricingMap, pricing)
|
||||
}
|
||||
// 补充模型元数据(描述、标签、供应商、状态)
|
||||
if meta, ok := metaMap[model]; ok {
|
||||
// 若模型被禁用(status!=1),则直接跳过,不返回给前端
|
||||
if meta.Status != 1 {
|
||||
continue
|
||||
}
|
||||
pricing.Description = meta.Description
|
||||
pricing.Icon = meta.Icon
|
||||
pricing.Tags = meta.Tags
|
||||
pricing.VendorID = meta.VendorID
|
||||
}
|
||||
modelPrice, findPrice := ratio_setting.GetModelPrice(model, false)
|
||||
if findPrice {
|
||||
pricing.ModelPrice = modelPrice
|
||||
pricing.QuotaType = 1
|
||||
} else {
|
||||
modelRatio, _, _ := ratio_setting.GetModelRatio(model)
|
||||
pricing.ModelRatio = modelRatio
|
||||
pricing.CompletionRatio = ratio_setting.GetCompletionRatio(model)
|
||||
pricing.QuotaType = 0
|
||||
}
|
||||
pricingMap = append(pricingMap, pricing)
|
||||
}
|
||||
|
||||
// 刷新缓存映射,供高并发快速查询
|
||||
modelEnableGroupsLock.Lock()
|
||||
modelEnableGroups = make(map[string][]string)
|
||||
modelQuotaTypeMap = make(map[string]int)
|
||||
for _, p := range pricingMap {
|
||||
modelEnableGroups[p.ModelName] = p.EnableGroup
|
||||
modelQuotaTypeMap[p.ModelName] = p.QuotaType
|
||||
}
|
||||
modelEnableGroupsLock.Unlock()
|
||||
// 刷新缓存映射,供高并发快速查询
|
||||
modelEnableGroupsLock.Lock()
|
||||
modelEnableGroups = make(map[string][]string)
|
||||
modelQuotaTypeMap = make(map[string]int)
|
||||
for _, p := range pricingMap {
|
||||
modelEnableGroups[p.ModelName] = p.EnableGroup
|
||||
modelQuotaTypeMap[p.ModelName] = p.QuotaType
|
||||
}
|
||||
modelEnableGroupsLock.Unlock()
|
||||
|
||||
lastGetPricingTime = time.Now()
|
||||
lastGetPricingTime = time.Now()
|
||||
}
|
||||
|
||||
// GetSupportedEndpointMap 返回全局端点到路径的映射
|
||||
func GetSupportedEndpointMap() map[string]common.EndpointInfo {
|
||||
return supportedEndpointMap
|
||||
return supportedEndpointMap
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ package model
|
||||
// 该方法用于需要最新数据的内部管理 API,
|
||||
// 因此会绕过默认的 1 分钟延迟刷新。
|
||||
func RefreshPricing() {
|
||||
updatePricingLock.Lock()
|
||||
defer updatePricingLock.Unlock()
|
||||
updatePricingLock.Lock()
|
||||
defer updatePricingLock.Unlock()
|
||||
|
||||
modelSupportEndpointsLock.Lock()
|
||||
defer modelSupportEndpointsLock.Unlock()
|
||||
modelSupportEndpointsLock.Lock()
|
||||
defer modelSupportEndpointsLock.Unlock()
|
||||
|
||||
updatePricing()
|
||||
updatePricing()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -148,7 +149,7 @@ func Redeem(key string, userId int) (quota int, err error) {
|
||||
if err != nil {
|
||||
return 0, errors.New("兑换失败," + err.Error())
|
||||
}
|
||||
RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s,兑换码ID %d", common.LogQuota(redemption.Quota), redemption.Id))
|
||||
RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s,兑换码ID %d", logger.LogQuota(redemption.Quota), redemption.Id))
|
||||
return redemption.Quota, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
||||
token.Status = common.TokenStatusExpired
|
||||
err := token.SelectUpdate()
|
||||
if err != nil {
|
||||
common.SysError("failed to update token status" + err.Error())
|
||||
common.SysLog("failed to update token status" + err.Error())
|
||||
}
|
||||
}
|
||||
return token, errors.New("该令牌已过期")
|
||||
@@ -102,7 +102,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
||||
token.Status = common.TokenStatusExhausted
|
||||
err := token.SelectUpdate()
|
||||
if err != nil {
|
||||
common.SysError("failed to update token status" + err.Error())
|
||||
common.SysLog("failed to update token status" + err.Error())
|
||||
}
|
||||
}
|
||||
keyPrefix := key[:3]
|
||||
@@ -134,7 +134,7 @@ func GetTokenById(id int) (*Token, error) {
|
||||
if shouldUpdateRedis(true, err) {
|
||||
gopool.Go(func() {
|
||||
if err := cacheSetToken(token); err != nil {
|
||||
common.SysError("failed to update user status cache: " + err.Error())
|
||||
common.SysLog("failed to update user status cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func GetTokenByKey(key string, fromDB bool) (token *Token, err error) {
|
||||
if shouldUpdateRedis(fromDB, err) && token != nil {
|
||||
gopool.Go(func() {
|
||||
if err := cacheSetToken(*token); err != nil {
|
||||
common.SysError("failed to update user status cache: " + err.Error())
|
||||
common.SysLog("failed to update user status cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func (token *Token) Update() (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheSetToken(*token)
|
||||
if err != nil {
|
||||
common.SysError("failed to update token cache: " + err.Error())
|
||||
common.SysLog("failed to update token cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func (token *Token) SelectUpdate() (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheSetToken(*token)
|
||||
if err != nil {
|
||||
common.SysError("failed to update token cache: " + err.Error())
|
||||
common.SysLog("failed to update token cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func (token *Token) Delete() (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheDeleteToken(token.Key)
|
||||
if err != nil {
|
||||
common.SysError("failed to delete token cache: " + err.Error())
|
||||
common.SysLog("failed to delete token cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func IncreaseTokenQuota(id int, key string, quota int) (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheIncrTokenQuota(key, int64(quota))
|
||||
if err != nil {
|
||||
common.SysError("failed to increase token quota: " + err.Error())
|
||||
common.SysLog("failed to increase token quota: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -299,7 +299,7 @@ func DecreaseTokenQuota(id int, key string, quota int) (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheDecrTokenQuota(key, int64(quota))
|
||||
if err != nil {
|
||||
common.SysError("failed to decrease token quota: " + err.Error())
|
||||
common.SysLog("failed to decrease token quota: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -94,7 +95,7 @@ func Recharge(referenceId string, customerId string) (err error) {
|
||||
return errors.New("充值失败," + err.Error())
|
||||
}
|
||||
|
||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", common.FormatQuota(int(quota)), topUp.Amount))
|
||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ func (t *TwoFA) ValidateTOTPAndUpdateUsage(code string) (bool, error) {
|
||||
if !common.ValidateTOTPCode(t.Secret, code) {
|
||||
// 增加失败次数
|
||||
if err := t.IncrementFailedAttempts(); err != nil {
|
||||
common.SysError("更新2FA失败次数失败: " + err.Error())
|
||||
common.SysLog("更新2FA失败次数失败: " + err.Error())
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func (t *TwoFA) ValidateTOTPAndUpdateUsage(code string) (bool, error) {
|
||||
t.LastUsedAt = &now
|
||||
|
||||
if err := t.Update(); err != nil {
|
||||
common.SysError("更新2FA使用记录失败: " + err.Error())
|
||||
common.SysLog("更新2FA使用记录失败: " + err.Error())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -277,7 +277,7 @@ func (t *TwoFA) ValidateBackupCodeAndUpdateUsage(code string) (bool, error) {
|
||||
if !valid {
|
||||
// 增加失败次数
|
||||
if err := t.IncrementFailedAttempts(); err != nil {
|
||||
common.SysError("更新2FA失败次数失败: " + err.Error())
|
||||
common.SysLog("更新2FA失败次数失败: " + err.Error())
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func (t *TwoFA) ValidateBackupCodeAndUpdateUsage(code string) (bool, error) {
|
||||
t.LastUsedAt = &now
|
||||
|
||||
if err := t.Update(); err != nil {
|
||||
common.SysError("更新2FA使用记录失败: " + err.Error())
|
||||
common.SysLog("更新2FA使用记录失败: " + err.Error())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -21,12 +21,6 @@ type QuotaData struct {
|
||||
}
|
||||
|
||||
func UpdateQuotaData() {
|
||||
// recover
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.SysLog(fmt.Sprintf("UpdateQuotaData panic: %s", r))
|
||||
}
|
||||
}()
|
||||
for {
|
||||
if common.DataExportEnabled {
|
||||
common.SysLog("正在更新数据看板数据...")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -75,7 +76,7 @@ func (user *User) GetSetting() dto.UserSetting {
|
||||
if user.Setting != "" {
|
||||
err := json.Unmarshal([]byte(user.Setting), &setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal setting: " + err.Error())
|
||||
common.SysLog("failed to unmarshal setting: " + err.Error())
|
||||
}
|
||||
}
|
||||
return setting
|
||||
@@ -84,7 +85,7 @@ func (user *User) GetSetting() dto.UserSetting {
|
||||
func (user *User) SetSetting(setting dto.UserSetting) {
|
||||
settingBytes, err := json.Marshal(setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to marshal setting: " + err.Error())
|
||||
common.SysLog("failed to marshal setting: " + err.Error())
|
||||
return
|
||||
}
|
||||
user.Setting = string(settingBytes)
|
||||
@@ -274,7 +275,7 @@ func inviteUser(inviterId int) (err error) {
|
||||
func (user *User) TransferAffQuotaToQuota(quota int) error {
|
||||
// 检查quota是否小于最小额度
|
||||
if float64(quota) < common.QuotaPerUnit {
|
||||
return fmt.Errorf("转移额度最小为%s!", common.LogQuota(int(common.QuotaPerUnit)))
|
||||
return fmt.Errorf("转移额度最小为%s!", logger.LogQuota(int(common.QuotaPerUnit)))
|
||||
}
|
||||
|
||||
// 开始数据库事务
|
||||
@@ -324,16 +325,16 @@ func (user *User) Insert(inviterId int) error {
|
||||
return result.Error
|
||||
}
|
||||
if common.QuotaForNewUser > 0 {
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(common.QuotaForNewUser)))
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser)))
|
||||
}
|
||||
if inviterId != 0 {
|
||||
if common.QuotaForInvitee > 0 {
|
||||
_ = IncreaseUserQuota(user.Id, common.QuotaForInvitee, true)
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(common.QuotaForInvitee)))
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", logger.LogQuota(common.QuotaForInvitee)))
|
||||
}
|
||||
if common.QuotaForInviter > 0 {
|
||||
//_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
|
||||
RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(common.QuotaForInviter)))
|
||||
RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
|
||||
_ = inviteUser(inviterId)
|
||||
}
|
||||
}
|
||||
@@ -517,7 +518,7 @@ func IsAdmin(userId int) bool {
|
||||
var user User
|
||||
err := DB.Where("id = ?", userId).Select("role").Find(&user).Error
|
||||
if err != nil {
|
||||
common.SysError("no such user " + err.Error())
|
||||
common.SysLog("no such user " + err.Error())
|
||||
return false
|
||||
}
|
||||
return user.Role >= common.RoleAdminUser
|
||||
@@ -572,7 +573,7 @@ func GetUserQuota(id int, fromDB bool) (quota int, err error) {
|
||||
if shouldUpdateRedis(fromDB, err) {
|
||||
gopool.Go(func() {
|
||||
if err := updateUserQuotaCache(id, quota); err != nil {
|
||||
common.SysError("failed to update user quota cache: " + err.Error())
|
||||
common.SysLog("failed to update user quota cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -610,7 +611,7 @@ func GetUserGroup(id int, fromDB bool) (group string, err error) {
|
||||
if shouldUpdateRedis(fromDB, err) {
|
||||
gopool.Go(func() {
|
||||
if err := updateUserGroupCache(id, group); err != nil {
|
||||
common.SysError("failed to update user group cache: " + err.Error())
|
||||
common.SysLog("failed to update user group cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -639,7 +640,7 @@ func GetUserSetting(id int, fromDB bool) (settingMap dto.UserSetting, err error)
|
||||
if shouldUpdateRedis(fromDB, err) {
|
||||
gopool.Go(func() {
|
||||
if err := updateUserSettingCache(id, setting); err != nil {
|
||||
common.SysError("failed to update user setting cache: " + err.Error())
|
||||
common.SysLog("failed to update user setting cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -669,7 +670,7 @@ func IncreaseUserQuota(id int, quota int, db bool) (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheIncrUserQuota(id, int64(quota))
|
||||
if err != nil {
|
||||
common.SysError("failed to increase user quota: " + err.Error())
|
||||
common.SysLog("failed to increase user quota: " + err.Error())
|
||||
}
|
||||
})
|
||||
if !db && common.BatchUpdateEnabled {
|
||||
@@ -694,7 +695,7 @@ func DecreaseUserQuota(id int, quota int) (err error) {
|
||||
gopool.Go(func() {
|
||||
err := cacheDecrUserQuota(id, int64(quota))
|
||||
if err != nil {
|
||||
common.SysError("failed to decrease user quota: " + err.Error())
|
||||
common.SysLog("failed to decrease user quota: " + err.Error())
|
||||
}
|
||||
})
|
||||
if common.BatchUpdateEnabled {
|
||||
@@ -750,7 +751,7 @@ func updateUserUsedQuotaAndRequestCount(id int, quota int, count int) {
|
||||
},
|
||||
).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to update user used quota and request count: " + err.Error())
|
||||
common.SysLog("failed to update user used quota and request count: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -767,14 +768,14 @@ func updateUserUsedQuota(id int, quota int) {
|
||||
},
|
||||
).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to update user used quota: " + err.Error())
|
||||
common.SysLog("failed to update user used quota: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func updateUserRequestCount(id int, count int) {
|
||||
err := DB.Model(&User{}).Where("id = ?", id).Update("request_count", gorm.Expr("request_count + ?", count)).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to update user request count: " + err.Error())
|
||||
common.SysLog("failed to update user request count: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,7 +786,7 @@ func GetUsernameById(id int, fromDB bool) (username string, err error) {
|
||||
if shouldUpdateRedis(fromDB, err) {
|
||||
gopool.Go(func() {
|
||||
if err := updateUserNameCache(id, username); err != nil {
|
||||
common.SysError("failed to update user name cache: " + err.Error())
|
||||
common.SysLog("failed to update user name cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (user *UserBase) GetSetting() dto.UserSetting {
|
||||
if user.Setting != "" {
|
||||
err := common.Unmarshal([]byte(user.Setting), &setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal setting: " + err.Error())
|
||||
common.SysLog("failed to unmarshal setting: " + err.Error())
|
||||
}
|
||||
}
|
||||
return setting
|
||||
@@ -78,7 +78,7 @@ func GetUserCache(userId int) (userCache *UserBase, err error) {
|
||||
if shouldUpdateRedis(fromDB, err) && user != nil {
|
||||
gopool.Go(func() {
|
||||
if err := updateUserCache(*user); err != nil {
|
||||
common.SysError("failed to update user status cache: " + err.Error())
|
||||
common.SysLog("failed to update user status cache: " + err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,12 +77,12 @@ func batchUpdate() {
|
||||
case BatchUpdateTypeUserQuota:
|
||||
err := increaseUserQuota(key, value)
|
||||
if err != nil {
|
||||
common.SysError("failed to batch update user quota: " + err.Error())
|
||||
common.SysLog("failed to batch update user quota: " + err.Error())
|
||||
}
|
||||
case BatchUpdateTypeTokenQuota:
|
||||
err := increaseTokenQuota(key, value)
|
||||
if err != nil {
|
||||
common.SysError("failed to batch update token quota: " + err.Error())
|
||||
common.SysLog("failed to batch update token quota: " + err.Error())
|
||||
}
|
||||
case BatchUpdateTypeUsedQuota:
|
||||
updateUserUsedQuota(key, value)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"one-api/common"
|
||||
"one-api/common"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Vendor 用于存储供应商信息,供模型引用
|
||||
@@ -13,76 +13,76 @@ import (
|
||||
// 本表同样遵循 3NF 设计范式
|
||||
|
||||
type Vendor struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,where:deleted_at IS NULL"`
|
||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
||||
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,priority:1"`
|
||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
||||
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_vendor_name,priority:2"`
|
||||
}
|
||||
|
||||
// Insert 创建新的供应商记录
|
||||
func (v *Vendor) Insert() error {
|
||||
now := common.GetTimestamp()
|
||||
v.CreatedTime = now
|
||||
v.UpdatedTime = now
|
||||
return DB.Create(v).Error
|
||||
now := common.GetTimestamp()
|
||||
v.CreatedTime = now
|
||||
v.UpdatedTime = now
|
||||
return DB.Create(v).Error
|
||||
}
|
||||
|
||||
// IsVendorNameDuplicated 检查供应商名称是否重复(排除自身 ID)
|
||||
func IsVendorNameDuplicated(id int, name string) (bool, error) {
|
||||
if name == "" {
|
||||
return false, nil
|
||||
}
|
||||
var cnt int64
|
||||
err := DB.Model(&Vendor{}).Where("name = ? AND id <> ?", name, id).Count(&cnt).Error
|
||||
return cnt > 0, err
|
||||
if name == "" {
|
||||
return false, nil
|
||||
}
|
||||
var cnt int64
|
||||
err := DB.Model(&Vendor{}).Where("name = ? AND id <> ?", name, id).Count(&cnt).Error
|
||||
return cnt > 0, err
|
||||
}
|
||||
|
||||
// Update 更新供应商记录
|
||||
func (v *Vendor) Update() error {
|
||||
v.UpdatedTime = common.GetTimestamp()
|
||||
return DB.Save(v).Error
|
||||
v.UpdatedTime = common.GetTimestamp()
|
||||
return DB.Save(v).Error
|
||||
}
|
||||
|
||||
// Delete 软删除供应商
|
||||
func (v *Vendor) Delete() error {
|
||||
return DB.Delete(v).Error
|
||||
return DB.Delete(v).Error
|
||||
}
|
||||
|
||||
// GetVendorByID 根据 ID 获取供应商
|
||||
func GetVendorByID(id int) (*Vendor, error) {
|
||||
var v Vendor
|
||||
err := DB.First(&v, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
var v Vendor
|
||||
err := DB.First(&v, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
// GetAllVendors 获取全部供应商(分页)
|
||||
func GetAllVendors(offset int, limit int) ([]*Vendor, error) {
|
||||
var vendors []*Vendor
|
||||
err := DB.Offset(offset).Limit(limit).Find(&vendors).Error
|
||||
return vendors, err
|
||||
var vendors []*Vendor
|
||||
err := DB.Offset(offset).Limit(limit).Find(&vendors).Error
|
||||
return vendors, err
|
||||
}
|
||||
|
||||
// SearchVendors 按关键字搜索供应商
|
||||
func SearchVendors(keyword string, offset int, limit int) ([]*Vendor, int64, error) {
|
||||
db := DB.Model(&Vendor{})
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
db = db.Where("name LIKE ? OR description LIKE ?", like, like)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var vendors []*Vendor
|
||||
if err := db.Offset(offset).Limit(limit).Order("id DESC").Find(&vendors).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return vendors, total, nil
|
||||
}
|
||||
db := DB.Model(&Vendor{})
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
db = db.Where("name LIKE ? OR description LIKE ?", like, like)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var vendors []*Vendor
|
||||
if err := db.Offset(offset).Limit(limit).Order("id DESC").Find(&vendors).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return vendors, total, nil
|
||||
}
|
||||
|
||||
@@ -7,104 +7,43 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.AudioRequest, error) {
|
||||
audioRequest := &dto.AudioRequest{}
|
||||
err := common.UnmarshalBodyReusable(c, audioRequest)
|
||||
func AudioHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) {
|
||||
info.InitChannelMeta(c)
|
||||
|
||||
audioReq, ok := info.Request.(*dto.AudioRequest)
|
||||
if !ok {
|
||||
return types.NewError(errors.New("invalid request type"), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
request, err := common.DeepCopy(audioReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch info.RelayMode {
|
||||
case relayconstant.RelayModeAudioSpeech:
|
||||
if audioRequest.Model == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
if setting.ShouldCheckPromptSensitive() {
|
||||
words, err := service.CheckSensitiveInput(audioRequest.Input)
|
||||
if err != nil {
|
||||
common.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(words, ",")))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = c.Request.ParseForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formData := c.Request.PostForm
|
||||
if audioRequest.Model == "" {
|
||||
audioRequest.Model = formData.Get("model")
|
||||
}
|
||||
|
||||
if audioRequest.Model == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
audioRequest.ResponseFormat = formData.Get("response_format")
|
||||
if audioRequest.ResponseFormat == "" {
|
||||
audioRequest.ResponseFormat = "json"
|
||||
}
|
||||
}
|
||||
return audioRequest, nil
|
||||
}
|
||||
|
||||
func AudioHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
relayInfo := relaycommon.GenRelayInfoOpenAIAudio(c)
|
||||
audioRequest, err := getAndValidAudioRequest(c, relayInfo)
|
||||
|
||||
if err != nil {
|
||||
common.LogError(c, fmt.Sprintf("getAndValidAudioRequest failed: %s", err.Error()))
|
||||
return types.NewError(err, types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
|
||||
return types.NewError(fmt.Errorf("failed to copy request to AudioRequest: %w", err), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
promptTokens := 0
|
||||
preConsumedTokens := common.PreConsumedQuota
|
||||
if relayInfo.RelayMode == relayconstant.RelayModeAudioSpeech {
|
||||
promptTokens = service.CountTTSToken(audioRequest.Input, audioRequest.Model)
|
||||
preConsumedTokens = promptTokens
|
||||
relayInfo.PromptTokens = promptTokens
|
||||
}
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, relayInfo, preConsumedTokens, 0)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeModelPriceError, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
|
||||
if openaiErr != nil {
|
||||
return openaiErr
|
||||
}
|
||||
defer func() {
|
||||
if openaiErr != nil {
|
||||
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||
}
|
||||
}()
|
||||
|
||||
err = helper.ModelMappedHelper(c, relayInfo, audioRequest)
|
||||
err = helper.ModelMappedHelper(c, info, request)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeChannelModelMappedError, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
adaptor := GetAdaptor(relayInfo.ApiType)
|
||||
adaptor := GetAdaptor(info.ApiType)
|
||||
if adaptor == nil {
|
||||
return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
|
||||
return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
adaptor.Init(relayInfo)
|
||||
adaptor.Init(info)
|
||||
|
||||
ioReader, err := adaptor.ConvertAudioRequest(c, relayInfo, *audioRequest)
|
||||
ioReader, err := adaptor.ConvertAudioRequest(c, info, *request)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, ioReader)
|
||||
resp, err := adaptor.DoRequest(c, info, ioReader)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
}
|
||||
@@ -121,14 +60,14 @@ func AudioHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
}
|
||||
}
|
||||
|
||||
usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
|
||||
usage, newAPIError := adaptor.DoResponse(c, httpResp, info)
|
||||
if newAPIError != nil {
|
||||
// reset status code 重置状态码
|
||||
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
|
||||
return newAPIError
|
||||
}
|
||||
|
||||
postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "")
|
||||
postConsumeQuota(c, info, usage.(*dto.Usage), "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,20 +34,20 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
var fullRequestURL string
|
||||
switch info.RelayFormat {
|
||||
case relaycommon.RelayFormatClaude:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v2/apps/claude-code-proxy/v1/messages", info.BaseUrl)
|
||||
case types.RelayFormatClaude:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v2/apps/claude-code-proxy/v1/messages", info.ChannelBaseUrl)
|
||||
default:
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeEmbeddings:
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/embeddings", info.BaseUrl)
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/embeddings", info.ChannelBaseUrl)
|
||||
case constant.RelayModeRerank:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.BaseUrl)
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.ChannelBaseUrl)
|
||||
case constant.RelayModeImagesGenerations:
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", info.BaseUrl)
|
||||
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", info.ChannelBaseUrl)
|
||||
case constant.RelayModeCompletions:
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/completions", info.BaseUrl)
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/completions", info.ChannelBaseUrl)
|
||||
default:
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/chat/completions", info.BaseUrl)
|
||||
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/chat/completions", info.ChannelBaseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,29 +118,16 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
switch info.RelayFormat {
|
||||
case relaycommon.RelayFormatClaude:
|
||||
case types.RelayFormatClaude:
|
||||
if info.IsStream {
|
||||
err, usage = claude.ClaudeStreamHandler(c, resp, info, claude.RequestModeMessage)
|
||||
return claude.ClaudeStreamHandler(c, resp, info, claude.RequestModeMessage)
|
||||
} else {
|
||||
err, usage = claude.ClaudeHandler(c, resp, info, claude.RequestModeMessage)
|
||||
return claude.ClaudeHandler(c, resp, info, claude.RequestModeMessage)
|
||||
}
|
||||
default:
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeImagesGenerations:
|
||||
err, usage = aliImageHandler(c, resp, info)
|
||||
case constant.RelayModeEmbeddings:
|
||||
err, usage = aliEmbeddingHandler(c, resp)
|
||||
case constant.RelayModeRerank:
|
||||
err, usage = RerankHandler(c, resp, info)
|
||||
default:
|
||||
if info.IsStream {
|
||||
usage, err = openai.OaiStreamHandler(c, info, resp)
|
||||
} else {
|
||||
usage, err = openai.OpenaiHandler(c, info, resp)
|
||||
}
|
||||
}
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.DoResponse(c, resp, info)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetModelList() []string {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/logger"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/types"
|
||||
@@ -22,14 +23,14 @@ func oaiImage2Ali(request dto.ImageRequest) *AliImageRequest {
|
||||
imageRequest.Input.Prompt = request.Prompt
|
||||
imageRequest.Model = request.Model
|
||||
imageRequest.Parameters.Size = strings.Replace(request.Size, "x", "*", -1)
|
||||
imageRequest.Parameters.N = request.N
|
||||
imageRequest.Parameters.N = int(request.N)
|
||||
imageRequest.ResponseFormat = request.ResponseFormat
|
||||
|
||||
return &imageRequest
|
||||
}
|
||||
|
||||
func updateTask(info *relaycommon.RelayInfo, taskID string) (*AliResponse, error, []byte) {
|
||||
url := fmt.Sprintf("%s/api/v1/tasks/%s", info.BaseUrl, taskID)
|
||||
url := fmt.Sprintf("%s/api/v1/tasks/%s", info.ChannelBaseUrl, taskID)
|
||||
|
||||
var aliResponse AliResponse
|
||||
|
||||
@@ -43,7 +44,7 @@ func updateTask(info *relaycommon.RelayInfo, taskID string) (*AliResponse, error
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
common.SysError("updateTask client.Do err: " + err.Error())
|
||||
common.SysLog("updateTask client.Do err: " + err.Error())
|
||||
return &aliResponse, err, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -53,7 +54,7 @@ func updateTask(info *relaycommon.RelayInfo, taskID string) (*AliResponse, error
|
||||
var response AliResponse
|
||||
err = json.Unmarshal(responseBody, &response)
|
||||
if err != nil {
|
||||
common.SysError("updateTask NewDecoder err: " + err.Error())
|
||||
common.SysLog("updateTask NewDecoder err: " + err.Error())
|
||||
return &aliResponse, err, nil
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ func responseAli2OpenAIImage(c *gin.Context, response *AliResponse, info *relayc
|
||||
if responseFormat == "b64_json" {
|
||||
_, b64, err := service.GetImageFromUrl(data.Url)
|
||||
if err != nil {
|
||||
common.LogError(c, "get_image_data_failed: "+err.Error())
|
||||
logger.LogError(c, "get_image_data_failed: "+err.Error())
|
||||
continue
|
||||
}
|
||||
b64Json = b64
|
||||
@@ -134,14 +135,14 @@ func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError), nil
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
err = json.Unmarshal(responseBody, &aliTaskResponse)
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
if aliTaskResponse.Message != "" {
|
||||
common.LogError(c, "ali_async_task_failed: "+aliTaskResponse.Message)
|
||||
logger.LogError(c, "ali_async_task_failed: "+aliTaskResponse.Message)
|
||||
return types.NewError(errors.New(aliTaskResponse.Message), types.ErrorCodeBadResponse), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -36,7 +36,7 @@ func RerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError), nil
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
|
||||
var aliResponse AliRerankResponse
|
||||
err = json.Unmarshal(responseBody, &aliResponse)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"strings"
|
||||
|
||||
"one-api/types"
|
||||
@@ -46,7 +47,7 @@ func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*types.NewAPIErro
|
||||
return types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
|
||||
model := c.GetString("model")
|
||||
if model == "" {
|
||||
@@ -148,7 +149,7 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError,
|
||||
var aliResponse AliResponse
|
||||
err := json.Unmarshal([]byte(data), &aliResponse)
|
||||
if err != nil {
|
||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||
common.SysLog("error unmarshalling stream response: " + err.Error())
|
||||
return true
|
||||
}
|
||||
if aliResponse.Usage.OutputTokens != 0 {
|
||||
@@ -161,7 +162,7 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError,
|
||||
lastResponseText = aliResponse.Output.Text
|
||||
jsonResponse, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
common.SysError("error marshalling stream response: " + err.Error())
|
||||
common.SysLog("error marshalling stream response: " + err.Error())
|
||||
return true
|
||||
}
|
||||
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||
@@ -171,7 +172,7 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError,
|
||||
return false
|
||||
}
|
||||
})
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
return nil, &usage
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ func aliHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.U
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError), nil
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
err = json.Unmarshal(responseBody, &aliResponse)
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
common2 "one-api/common"
|
||||
"one-api/logger"
|
||||
"one-api/relay/common"
|
||||
"one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
@@ -181,7 +182,7 @@ func sendPingData(c *gin.Context, mutex *sync.Mutex) error {
|
||||
|
||||
err := helper.PingData(c)
|
||||
if err != nil {
|
||||
common2.LogError(c, "SSE ping error: "+err.Error())
|
||||
logger.LogError(c, "SSE ping error: "+err.Error())
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
|
||||
var claudeReq *dto.ClaudeRequest
|
||||
var err error
|
||||
claudeReq, err = claude.RequestOpenAI2ClaudeMessage(*request)
|
||||
claudeReq, err = claude.RequestOpenAI2ClaudeMessage(c, *request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
@@ -113,14 +112,14 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
|
||||
}
|
||||
claudeReq := claudeReq_.(*dto.ClaudeRequest)
|
||||
awsClaudeReq := copyRequest(claudeReq)
|
||||
awsReq.Body, err = json.Marshal(awsClaudeReq)
|
||||
awsReq.Body, err = common.Marshal(awsClaudeReq)
|
||||
if err != nil {
|
||||
return types.NewError(errors.Wrap(err, "marshal request"), types.ErrorCodeBadResponseBody), nil
|
||||
}
|
||||
|
||||
awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq)
|
||||
if err != nil {
|
||||
return types.NewError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeChannelAwsClientError), nil
|
||||
return types.NewOpenAIError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
claudeInfo := &claude.ClaudeResponseInfo{
|
||||
@@ -165,14 +164,14 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
claudeReq := claudeReq_.(*dto.ClaudeRequest)
|
||||
|
||||
awsClaudeReq := copyRequest(claudeReq)
|
||||
awsReq.Body, err = json.Marshal(awsClaudeReq)
|
||||
awsReq.Body, err = common.Marshal(awsClaudeReq)
|
||||
if err != nil {
|
||||
return types.NewError(errors.Wrap(err, "marshal request"), types.ErrorCodeBadResponseBody), nil
|
||||
}
|
||||
|
||||
awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq)
|
||||
if err != nil {
|
||||
return types.NewError(errors.Wrap(err, "InvokeModelWithResponseStream"), types.ErrorCodeChannelAwsClientError), nil
|
||||
return types.NewOpenAIError(errors.Wrap(err, "InvokeModelWithResponseStream"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil
|
||||
}
|
||||
stream := awsResp.GetStream()
|
||||
defer stream.Close()
|
||||
|
||||
@@ -101,7 +101,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
default:
|
||||
suffix += strings.ToLower(info.UpstreamModelName)
|
||||
}
|
||||
fullRequestURL := fmt.Sprintf("%s/rpc/2.0/ai_custom/v1/wenxinworkshop/%s", info.BaseUrl, suffix)
|
||||
fullRequestURL := fmt.Sprintf("%s/rpc/2.0/ai_custom/v1/wenxinworkshop/%s", info.ChannelBaseUrl, suffix)
|
||||
var accessToken string
|
||||
var err error
|
||||
if accessToken, err = getBaiduAccessToken(info.ApiKey); err != nil {
|
||||
|
||||
@@ -118,7 +118,7 @@ func baiduStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
|
||||
var baiduResponse BaiduChatStreamResponse
|
||||
err := common.Unmarshal([]byte(data), &baiduResponse)
|
||||
if err != nil {
|
||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||
common.SysLog("error unmarshalling stream response: " + err.Error())
|
||||
return true
|
||||
}
|
||||
if baiduResponse.Usage.TotalTokens != 0 {
|
||||
@@ -129,11 +129,11 @@ func baiduStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
|
||||
response := streamResponseBaidu2OpenAI(&baiduResponse)
|
||||
err = helper.ObjectData(c, response)
|
||||
if err != nil {
|
||||
common.SysError("error sending stream response: " + err.Error())
|
||||
common.SysLog("error sending stream response: " + err.Error())
|
||||
}
|
||||
return true
|
||||
})
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
return nil, usage
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func baiduHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respon
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
err = json.Unmarshal(responseBody, &baiduResponse)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
@@ -168,7 +168,7 @@ func baiduEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *ht
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
err = json.Unmarshal(responseBody, &baiduResponse)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
|
||||
@@ -45,15 +45,15 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeChatCompletions:
|
||||
return fmt.Sprintf("%s/v2/chat/completions", info.BaseUrl), nil
|
||||
return fmt.Sprintf("%s/v2/chat/completions", info.ChannelBaseUrl), nil
|
||||
case constant.RelayModeEmbeddings:
|
||||
return fmt.Sprintf("%s/v2/embeddings", info.BaseUrl), nil
|
||||
return fmt.Sprintf("%s/v2/embeddings", info.ChannelBaseUrl), nil
|
||||
case constant.RelayModeImagesGenerations:
|
||||
return fmt.Sprintf("%s/v2/images/generations", info.BaseUrl), nil
|
||||
return fmt.Sprintf("%s/v2/images/generations", info.ChannelBaseUrl), nil
|
||||
case constant.RelayModeImagesEdits:
|
||||
return fmt.Sprintf("%s/v2/images/edits", info.BaseUrl), nil
|
||||
return fmt.Sprintf("%s/v2/images/edits", info.ChannelBaseUrl), nil
|
||||
case constant.RelayModeRerank:
|
||||
return fmt.Sprintf("%s/v2/rerank", info.BaseUrl), nil
|
||||
return fmt.Sprintf("%s/v2/rerank", info.ChannelBaseUrl), nil
|
||||
default:
|
||||
}
|
||||
return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode)
|
||||
|
||||