Compare commits

...

10 Commits

Author SHA1 Message Date
JustSong
28fb4d76af fix: disable redis on master node 2023-06-22 20:12:43 +08:00
mrhaoji
ca779e4ffa fix: fix time_test.sh (#191)
* Update time_test.sh to fix the params

修复测试脚本入参问题

* Update time_test.sh to fix the params
2023-06-22 19:53:28 +08:00
JustSong
f51c982437 fix: update time_test.sh 2023-06-22 19:36:54 +08:00
JustSong
36e681e878 chore: update time_test.sh 2023-06-22 19:25:27 +08:00
JustSong
75cd522c2c chore: add time_test.sh 2023-06-22 19:14:45 +08:00
mrhaoji
c893d04667 chore: update docker-compose.yml (#189)
去除 Redis 服务的 ports 配置,只允许 Docker Compose 启动的服务才可以访问Redis,不会暴露到宿主机上也不会和宿主机产生端口冲突;同时也提升安全性。
2023-06-22 14:49:33 +08:00
JustSong
c6717307d0 chore: update one-api.service 2023-06-22 11:37:44 +08:00
JustSong
97cdb616cd docs: update README 2023-06-22 11:17:42 +08:00
JustSong
76a3913115 chore: update docker-compose.yml 2023-06-22 11:15:01 +08:00
JustSong
00151a0124 chore: format logs 2023-06-22 10:59:01 +08:00
17 changed files with 104 additions and 46 deletions

View File

@@ -86,7 +86,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
## 部署 ## 部署
### 基于 Docker 进行部署 ### 基于 Docker 进行部署
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api` 部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
@@ -266,8 +266,8 @@ https://openai.justsong.cn
## 常见问题 ## 常见问题
1. 额度是什么怎么计算的One API 的额度计算有问题? 1. 额度是什么怎么计算的One API 的额度计算有问题?
+ 额度 = token * 倍率 + 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率
+ 倍率包括分组的倍率,以及补全倍率。 + 其中补全倍率对于 GPT3.5 固定为 1.33GPT4 为 2与官方保持一致
+ 如果是非流模式,官方接口会返回消耗的总 token但是你要注意提示和补全的消耗倍率不一样。 + 如果是非流模式,官方接口会返回消耗的总 token但是你要注意提示和补全的消耗倍率不一样。
2. 账户额度足够为什么提示额度不足? 2. 账户额度足够为什么提示额度不足?
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。 + 请检查你的令牌额度是否足够,这个和账户额度是分开的。

36
bin/time_test.sh Normal file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
if [ $# -ne 3 ]; then
echo "Usage: time_test.sh <domain> <key> <count>"
exit 1
fi
domain=$1
key=$2
count=$3
total_time=0
times=()
for ((i=1; i<=count; i++)); do
result=$(curl -o /dev/null -s -w %{time_total}\\n \
https://"$domain"/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $key" \
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}')
echo "$result"
total_time=$(bc <<< "$total_time + $result")
times+=("$result")
done
average_time=$(echo "scale=4; $total_time / $count" | bc)
sum_of_squares=0
for time in "${times[@]}"; do
difference=$(echo "scale=4; $time - $average_time" | bc)
square=$(echo "scale=4; $difference * $difference" | bc)
sum_of_squares=$(echo "scale=4; $sum_of_squares + $square" | bc)
done
standard_deviation=$(echo "scale=4; sqrt($sum_of_squares / $count)" | bc)
echo "Average time: $average_time±$standard_deviation"

View File

@@ -11,7 +11,7 @@ var GroupRatio = map[string]float64{
func GroupRatio2JSONString() string { func GroupRatio2JSONString() string {
jsonBytes, err := json.Marshal(GroupRatio) jsonBytes, err := json.Marshal(GroupRatio)
if err != nil { if err != nil {
SysError("Error marshalling model ratio: " + err.Error()) SysError("error marshalling model ratio: " + err.Error())
} }
return string(jsonBytes) return string(jsonBytes)
} }
@@ -24,7 +24,7 @@ func UpdateGroupRatioByJSONString(jsonStr string) error {
func GetGroupRatio(name string) float64 { func GetGroupRatio(name string) float64 {
ratio, ok := GroupRatio[name] ratio, ok := GroupRatio[name]
if !ok { if !ok {
SysError("Group ratio not found: " + name) SysError("group ratio not found: " + name)
return 1 return 1
} }
return ratio return ratio

View File

@@ -40,7 +40,7 @@ var ModelRatio = map[string]float64{
func ModelRatio2JSONString() string { func ModelRatio2JSONString() string {
jsonBytes, err := json.Marshal(ModelRatio) jsonBytes, err := json.Marshal(ModelRatio)
if err != nil { if err != nil {
SysError("Error marshalling model ratio: " + err.Error()) SysError("error marshalling model ratio: " + err.Error())
} }
return string(jsonBytes) return string(jsonBytes)
} }
@@ -53,7 +53,7 @@ func UpdateModelRatioByJSONString(jsonStr string) error {
func GetModelRatio(name string) float64 { func GetModelRatio(name string) float64 {
ratio, ok := ModelRatio[name] ratio, ok := ModelRatio[name]
if !ok { if !ok {
SysError("Model ratio not found: " + name) SysError("model ratio not found: " + name)
return 30 return 30
} }
return ratio return ratio

View File

@@ -17,10 +17,15 @@ func InitRedisClient() (err error) {
SysLog("REDIS_CONN_STRING not set, Redis is not enabled") SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
return nil return nil
} }
if IsMasterNode {
SysLog("Redis is disabled on master node")
RedisEnabled = false
return nil
}
SysLog("Redis is enabled") SysLog("Redis is enabled")
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
if err != nil { if err != nil {
FatalLog(err) FatalLog("failed to parse Redis connection string: " + err.Error())
} }
RDB = redis.NewClient(opt) RDB = redis.NewClient(opt)
@@ -28,13 +33,16 @@ func InitRedisClient() (err error) {
defer cancel() defer cancel()
_, err = RDB.Ping(ctx).Result() _, err = RDB.Ping(ctx).Result()
if err != nil {
FatalLog("Redis ping test failed: " + err.Error())
}
return err return err
} }
func ParseRedisOption() *redis.Options { func ParseRedisOption() *redis.Options {
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
if err != nil { if err != nil {
panic(err) FatalLog("failed to parse Redis connection string: " + err.Error())
} }
return opt return opt
} }

View File

@@ -129,7 +129,7 @@ func disableChannel(channelId int, channelName string, reason string) {
content := fmt.Sprintf("通道「%s」#%d已被禁用原因%s", channelName, channelId, reason) content := fmt.Sprintf("通道「%s」#%d已被禁用原因%s", channelName, channelId, reason)
err := common.SendEmail(subject, common.RootUserEmail, content) err := common.SendEmail(subject, common.RootUserEmail, content)
if err != nil { if err != nil {
common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
} }
} }
@@ -176,7 +176,7 @@ func testAllChannels(c *gin.Context) error {
} }
err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
if err != nil { if err != nil {
common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
} }
testAllChannelsLock.Lock() testAllChannelsLock.Lock()
testAllChannelsRunning = false testAllChannelsRunning = false

View File

@@ -140,7 +140,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
quotaDelta := quota - preConsumedQuota quotaDelta := quota - preConsumedQuota
err := model.PostConsumeTokenQuota(tokenId, quotaDelta) err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
if err != nil { if err != nil {
common.SysError("Error consuming token remain quota: " + err.Error()) common.SysError("error consuming token remain quota: " + err.Error())
} }
tokenName := c.GetString("token_name") tokenName := c.GetString("token_name")
model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f", tokenName, textRequest.Model, common.LogQuota(quota), modelRatio, groupRatio)) model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f", tokenName, textRequest.Model, common.LogQuota(quota), modelRatio, groupRatio))
@@ -173,7 +173,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
for scanner.Scan() { for scanner.Scan() {
data := scanner.Text() data := scanner.Text()
if len(data) < 6 { // must be something wrong! if len(data) < 6 { // must be something wrong!
common.SysError("Invalid stream response: " + data) common.SysError("invalid stream response: " + data)
continue continue
} }
dataChan <- data dataChan <- data
@@ -184,7 +184,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
var streamResponse ChatCompletionsStreamResponse var streamResponse ChatCompletionsStreamResponse
err = json.Unmarshal([]byte(data), &streamResponse) err = json.Unmarshal([]byte(data), &streamResponse)
if err != nil { if err != nil {
common.SysError("Error unmarshalling stream response: " + err.Error()) common.SysError("error unmarshalling stream response: " + err.Error())
return return
} }
for _, choice := range streamResponse.Choices { for _, choice := range streamResponse.Choices {
@@ -194,7 +194,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
var streamResponse CompletionsStreamResponse var streamResponse CompletionsStreamResponse
err = json.Unmarshal([]byte(data), &streamResponse) err = json.Unmarshal([]byte(data), &streamResponse)
if err != nil { if err != nil {
common.SysError("Error unmarshalling stream response: " + err.Error()) common.SysError("error unmarshalling stream response: " + err.Error())
return return
} }
for _, choice := range streamResponse.Choices { for _, choice := range streamResponse.Choices {

View File

@@ -118,7 +118,7 @@ func Relay(c *gin.Context) {
"error": err.OpenAIError, "error": err.OpenAIError,
}) })
channelId := c.GetInt("channel_id") channelId := c.GetInt("channel_id")
common.SysError(fmt.Sprintf("Relay error (channel #%d): %s", channelId, err.Message)) common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
// https://platform.openai.com/docs/guides/error-codes/api-errors // https://platform.openai.com/docs/guides/error-codes/api-errors
if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") { if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") {
channelId := c.GetInt("channel_id") channelId := c.GetInt("channel_id")

View File

@@ -2,7 +2,7 @@ version: '3.4'
services: services:
one-api: one-api:
image: ghcr.io/songquanpeng/one-api:latest image: justsong/one-api:latest
container_name: one-api container_name: one-api
restart: always restart: always
command: --log-dir /app/logs command: --log-dir /app/logs
@@ -11,12 +11,22 @@ services:
volumes: volumes:
- ./data:/data - ./data:/data
- ./logs:/app/logs - ./logs:/app/logs
# environment: environment:
# REDIS_CONN_STRING: redis://default:redispw@localhost:49153 - SQL_DSN=root:123456@tcp(host.docker.internal:3306)/one-api # 修改此行,或注释掉以使用 SQLite 作为数据库
# SESSION_SECRET: random_string - REDIS_CONN_STRING=redis://redis
# SQL_DSN: root:123456@tcp(localhost:3306)/one-api - SESSION_SECRET=random_string # 修改为随机字符串
- TZ=Asia/Shanghai
# - SYNC_FREQUENCY=60 # 多机部署时从节点取消注释该行
# - FRONTEND_BASE_URL=https://openai.justsong.cn # 多机部署时从节点取消注释该行
depends_on:
- redis
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk '{print $2}' | grep 'true'"] test: [ "CMD-SHELL", "curl -s http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk '{print $2}' | grep 'true'" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
redis:
image: redis:latest
container_name: redis
restart: always

11
main.go
View File

@@ -6,7 +6,6 @@ import (
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/sessions/redis" "github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log"
"one-api/common" "one-api/common"
"one-api/middleware" "one-api/middleware"
"one-api/model" "one-api/model"
@@ -30,19 +29,19 @@ func main() {
// Initialize SQL Database // Initialize SQL Database
err := model.InitDB() err := model.InitDB()
if err != nil { if err != nil {
common.FatalLog(err) common.FatalLog("failed to initialize database: " + err.Error())
} }
defer func() { defer func() {
err := model.CloseDB() err := model.CloseDB()
if err != nil { if err != nil {
common.FatalLog(err) common.FatalLog("failed to close database: " + err.Error())
} }
}() }()
// Initialize Redis // Initialize Redis
err = common.InitRedisClient() err = common.InitRedisClient()
if err != nil { if err != nil {
common.FatalLog(err) common.FatalLog("failed to initialize Redis: " + err.Error())
} }
// Initialize options // Initialize options
@@ -53,7 +52,7 @@ func main() {
if os.Getenv("SYNC_FREQUENCY") != "" { if os.Getenv("SYNC_FREQUENCY") != "" {
frequency, err := strconv.Atoi(os.Getenv("SYNC_FREQUENCY")) frequency, err := strconv.Atoi(os.Getenv("SYNC_FREQUENCY"))
if err != nil { if err != nil {
common.FatalLog(err) common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
} }
go model.SyncOptions(frequency) go model.SyncOptions(frequency)
if common.RedisEnabled { if common.RedisEnabled {
@@ -84,6 +83,6 @@ func main() {
} }
err = server.Run(":" + port) err = server.Run(":" + port)
if err != nil { if err != nil {
log.Println(err) common.FatalLog("failed to start HTTP server: " + err.Error())
} }
} }

View File

@@ -137,13 +137,13 @@ func InitChannelCache() {
channelSyncLock.Lock() channelSyncLock.Lock()
group2model2channels = newGroup2model2channels group2model2channels = newGroup2model2channels
channelSyncLock.Unlock() channelSyncLock.Unlock()
common.SysLog("Channels synced from database") common.SysLog("channels synced from database")
} }
func SyncChannelCache(frequency int) { func SyncChannelCache(frequency int) {
for { for {
time.Sleep(time.Duration(frequency) * time.Second) time.Sleep(time.Duration(frequency) * time.Second)
common.SysLog("Syncing channels from database") common.SysLog("syncing channels from database")
InitChannelCache() InitChannelCache()
} }
} }

View File

@@ -54,7 +54,7 @@ func InitDB() (err error) {
PrepareStmt: true, // precompile SQL PrepareStmt: true, // precompile SQL
}) })
} }
common.SysLog("Database connected") common.SysLog("database connected")
if err == nil { if err == nil {
DB = db DB = db
if !common.IsMasterNode { if !common.IsMasterNode {
@@ -88,7 +88,7 @@ func InitDB() (err error) {
if err != nil { if err != nil {
return err return err
} }
common.SysLog("Database migrated") common.SysLog("database migrated")
err = createRootAccountIfNeed() err = createRootAccountIfNeed()
return err return err
} else { } else {

View File

@@ -75,7 +75,7 @@ func loadOptionsFromDatabase() {
for _, option := range options { for _, option := range options {
err := updateOptionMap(option.Key, option.Value) err := updateOptionMap(option.Key, option.Value)
if err != nil { if err != nil {
common.SysError("Failed to update option map: " + err.Error()) common.SysError("failed to update option map: " + err.Error())
} }
} }
} }
@@ -83,7 +83,7 @@ func loadOptionsFromDatabase() {
func SyncOptions(frequency int) { func SyncOptions(frequency int) {
for { for {
time.Sleep(time.Duration(frequency) * time.Second) time.Sleep(time.Duration(frequency) * time.Second)
common.SysLog("Syncing options from database") common.SysLog("syncing options from database")
loadOptionsFromDatabase() loadOptionsFromDatabase()
} }
} }

View File

@@ -64,7 +64,7 @@ func Redeem(key string, userId int) (quota int, err error) {
redemption.Status = common.RedemptionCodeStatusUsed redemption.Status = common.RedemptionCodeStatusUsed
err := redemption.SelectUpdate() err := redemption.SelectUpdate()
if err != nil { if err != nil {
common.SysError("更新兑换码状态失败:" + err.Error()) common.SysError("failed to update redemption status: " + err.Error())
} }
RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota))) RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
}() }()

View File

@@ -45,7 +45,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
token.Status = common.TokenStatusExpired token.Status = common.TokenStatusExpired
err := token.SelectUpdate() err := token.SelectUpdate()
if err != nil { if err != nil {
common.SysError("更新令牌状态失败:" + err.Error()) common.SysError("failed to update token status" + err.Error())
} }
return nil, errors.New("该令牌已过期") return nil, errors.New("该令牌已过期")
} }
@@ -53,7 +53,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
token.Status = common.TokenStatusExhausted token.Status = common.TokenStatusExhausted
err := token.SelectUpdate() err := token.SelectUpdate()
if err != nil { if err != nil {
common.SysError("更新令牌状态失败:" + err.Error()) common.SysError("failed to update token status" + err.Error())
} }
return nil, errors.New("该令牌额度已用尽") return nil, errors.New("该令牌额度已用尽")
} }
@@ -61,7 +61,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
token.AccessedTime = common.GetTimestamp() token.AccessedTime = common.GetTimestamp()
err := token.SelectUpdate() err := token.SelectUpdate()
if err != nil { if err != nil {
common.SysError("更新令牌失败:" + err.Error()) common.SysError("failed to update token" + err.Error())
} }
}() }()
return token, nil return token, nil
@@ -166,7 +166,7 @@ func PreConsumeTokenQuota(tokenId int, quota int) (err error) {
go func() { go func() {
email, err := GetUserEmail(token.UserId) email, err := GetUserEmail(token.UserId)
if err != nil { if err != nil {
common.SysError("获取用户邮箱失败:" + err.Error()) common.SysError("failed to fetch user email: " + err.Error())
} }
prompt := "您的额度即将用尽" prompt := "您的额度即将用尽"
if noMoreQuota { if noMoreQuota {
@@ -177,7 +177,7 @@ func PreConsumeTokenQuota(tokenId int, quota int) (err error) {
err = common.SendEmail(prompt, email, err = common.SendEmail(prompt, email,
fmt.Sprintf("%s当前剩余额度为 %d为了不影响您的使用请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink)) fmt.Sprintf("%s当前剩余额度为 %d为了不影响您的使用请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
if err != nil { if err != nil {
common.SysError("发送邮件失败:" + err.Error()) common.SysError("failed to send email" + err.Error())
} }
} }
}() }()

View File

@@ -220,7 +220,7 @@ func IsAdmin(userId int) bool {
var user User var user User
err := DB.Where("id = ?", userId).Select("role").Find(&user).Error err := DB.Where("id = ?", userId).Select("role").Find(&user).Error
if err != nil { if err != nil {
common.SysError("No such user " + err.Error()) common.SysError("no such user " + err.Error())
return false return false
} }
return user.Role >= common.RoleAdminUser return user.Role >= common.RoleAdminUser
@@ -233,7 +233,7 @@ func IsUserEnabled(userId int) bool {
var user User var user User
err := DB.Where("id = ?", userId).Select("status").Find(&user).Error err := DB.Where("id = ?", userId).Select("status").Find(&user).Error
if err != nil { if err != nil {
common.SysError("No such user " + err.Error()) common.SysError("no such user " + err.Error())
return false return false
} }
return user.Status == common.UserStatusEnabled return user.Status == common.UserStatusEnabled
@@ -300,6 +300,6 @@ func UpdateUserUsedQuotaAndRequestCount(id int, quota int) {
}, },
).Error ).Error
if err != nil { if err != nil {
common.SysError("Failed to update user used quota and request count: " + err.Error()) common.SysError("failed to update user used quota and request count: " + err.Error())
} }
} }

View File

@@ -1,11 +1,16 @@
# File path: /etc/systemd/system/one-api.service
# sudo systemctl daemon-reload
# sudo systemctl start one-api
# sudo systemctl enable one-api
# sudo systemctl status one-api
[Unit] [Unit]
Description=One API Service Description=One API Service
After=network.target After=network.target
[Service] [Service]
User=yourusername # 守护进程用户名 User=ubuntu # 注意修改用户名
WorkingDirectory=/path/to/One-API # One API运行路径 WorkingDirectory=/path/to/one-api # 注意修改路径
ExecStart=/path/to/One-API/one-api --port 3000 --log-dir /path/to/One-API/logs # 端口 ExecStart=/path/to/one-api/one-api --port 3000 --log-dir /path/to/one-api/logs # 注意修改路径和端口
Restart=always Restart=always
RestartSec=5 RestartSec=5