Upload 134 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +33 -0
- LICENSE +21 -0
- VERSION +0 -0
- bin/migration_v0.2-v0.3.sql +6 -0
- bin/migration_v0.3-v0.4.sql +17 -0
- bin/time_test.sh +40 -0
- common/constants.go +202 -0
- common/crypto.go +14 -0
- common/custom-event.go +82 -0
- common/email.go +67 -0
- common/embed-file-system.go +32 -0
- common/gin.go +26 -0
- common/group-ratio.go +31 -0
- common/init.go +57 -0
- common/logger.go +52 -0
- common/model-ratio.go +99 -0
- common/rate-limit.go +70 -0
- common/redis.go +68 -0
- common/utils.go +192 -0
- common/validate.go +9 -0
- common/verification.go +77 -0
- controller/billing.go +91 -0
- controller/channel-billing.go +345 -0
- controller/channel-test.go +223 -0
- controller/channel.go +154 -0
- controller/github.go +207 -0
- controller/group.go +19 -0
- controller/log.go +133 -0
- controller/misc.go +204 -0
- controller/model.go +446 -0
- controller/option.go +91 -0
- controller/redemption.go +192 -0
- controller/relay-ali.go +241 -0
- controller/relay-audio.go +147 -0
- controller/relay-baidu.go +370 -0
- controller/relay-claude.go +220 -0
- controller/relay-image.go +180 -0
- controller/relay-openai.go +144 -0
- controller/relay-palm.go +205 -0
- controller/relay-text.go +522 -0
- controller/relay-utils.go +169 -0
- controller/relay-xunfei.go +290 -0
- controller/relay-zhipu.go +301 -0
- controller/relay.go +228 -0
- controller/token.go +228 -0
- controller/user.go +743 -0
- controller/wechat.go +164 -0
- docker-compose.yml +34 -0
- go.mod +61 -0
- go.sum +205 -0
Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:16 as builder
|
| 2 |
+
|
| 3 |
+
WORKDIR /build
|
| 4 |
+
COPY web/package.json .
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY ./web .
|
| 7 |
+
COPY ./VERSION .
|
| 8 |
+
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
|
| 9 |
+
|
| 10 |
+
FROM golang AS builder2
|
| 11 |
+
|
| 12 |
+
ENV GO111MODULE=on \
|
| 13 |
+
CGO_ENABLED=1 \
|
| 14 |
+
GOOS=linux
|
| 15 |
+
|
| 16 |
+
WORKDIR /build
|
| 17 |
+
ADD go.mod go.sum ./
|
| 18 |
+
RUN go mod download
|
| 19 |
+
COPY . .
|
| 20 |
+
COPY --from=builder /build/build ./web/build
|
| 21 |
+
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
| 22 |
+
|
| 23 |
+
FROM alpine
|
| 24 |
+
|
| 25 |
+
RUN apk update \
|
| 26 |
+
&& apk upgrade \
|
| 27 |
+
&& apk add --no-cache ca-certificates tzdata \
|
| 28 |
+
&& update-ca-certificates 2>/dev/null || true
|
| 29 |
+
|
| 30 |
+
COPY --from=builder2 /build/one-api /
|
| 31 |
+
EXPOSE 3000
|
| 32 |
+
WORKDIR /data
|
| 33 |
+
ENTRYPOINT ["/one-api"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023 JustSong
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
VERSION
ADDED
|
File without changes
|
bin/migration_v0.2-v0.3.sql
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
UPDATE users
|
| 2 |
+
SET quota = quota + (
|
| 3 |
+
SELECT SUM(remain_quota)
|
| 4 |
+
FROM tokens
|
| 5 |
+
WHERE tokens.user_id = users.id
|
| 6 |
+
)
|
bin/migration_v0.3-v0.4.sql
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INSERT INTO abilities (`group`, model, channel_id, enabled)
|
| 2 |
+
SELECT c.`group`, m.model, c.id, 1
|
| 3 |
+
FROM channels c
|
| 4 |
+
CROSS JOIN (
|
| 5 |
+
SELECT 'gpt-3.5-turbo' AS model UNION ALL
|
| 6 |
+
SELECT 'gpt-3.5-turbo-0301' AS model UNION ALL
|
| 7 |
+
SELECT 'gpt-4' AS model UNION ALL
|
| 8 |
+
SELECT 'gpt-4-0314' AS model
|
| 9 |
+
) AS m
|
| 10 |
+
WHERE c.status = 1
|
| 11 |
+
AND NOT EXISTS (
|
| 12 |
+
SELECT 1
|
| 13 |
+
FROM abilities a
|
| 14 |
+
WHERE a.`group` = c.`group`
|
| 15 |
+
AND a.model = m.model
|
| 16 |
+
AND a.channel_id = c.id
|
| 17 |
+
);
|
bin/time_test.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
if [ $# -lt 3 ]; then
|
| 4 |
+
echo "Usage: time_test.sh <domain> <key> <count> [<model>]"
|
| 5 |
+
exit 1
|
| 6 |
+
fi
|
| 7 |
+
|
| 8 |
+
domain=$1
|
| 9 |
+
key=$2
|
| 10 |
+
count=$3
|
| 11 |
+
model=${4:-"gpt-3.5-turbo"} # 设置默认模型为 gpt-3.5-turbo
|
| 12 |
+
|
| 13 |
+
total_time=0
|
| 14 |
+
times=()
|
| 15 |
+
|
| 16 |
+
for ((i=1; i<=count; i++)); do
|
| 17 |
+
result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \
|
| 18 |
+
https://"$domain"/v1/chat/completions \
|
| 19 |
+
-H "Content-Type: application/json" \
|
| 20 |
+
-H "Authorization: Bearer $key" \
|
| 21 |
+
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}')
|
| 22 |
+
http_code=$(echo "$result" | awk '{print $1}')
|
| 23 |
+
time=$(echo "$result" | awk '{print $2}')
|
| 24 |
+
echo "HTTP status code: $http_code, Time taken: $time"
|
| 25 |
+
total_time=$(bc <<< "$total_time + $time")
|
| 26 |
+
times+=("$time")
|
| 27 |
+
done
|
| 28 |
+
|
| 29 |
+
average_time=$(echo "scale=4; $total_time / $count" | bc)
|
| 30 |
+
|
| 31 |
+
sum_of_squares=0
|
| 32 |
+
for time in "${times[@]}"; do
|
| 33 |
+
difference=$(echo "scale=4; $time - $average_time" | bc)
|
| 34 |
+
square=$(echo "scale=4; $difference * $difference" | bc)
|
| 35 |
+
sum_of_squares=$(echo "scale=4; $sum_of_squares + $square" | bc)
|
| 36 |
+
done
|
| 37 |
+
|
| 38 |
+
standard_deviation=$(echo "scale=4; sqrt($sum_of_squares / $count)" | bc)
|
| 39 |
+
|
| 40 |
+
echo "Average time: $average_time±$standard_deviation"
|
common/constants.go
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"os"
|
| 5 |
+
"strconv"
|
| 6 |
+
"sync"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
var StartTime = time.Now().Unix() // unit: second
|
| 13 |
+
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
|
| 14 |
+
var SystemName = "One API"
|
| 15 |
+
var ServerAddress = "http://localhost:3000"
|
| 16 |
+
var Footer = ""
|
| 17 |
+
var Logo = ""
|
| 18 |
+
var TopUpLink = ""
|
| 19 |
+
var ChatLink = ""
|
| 20 |
+
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
| 21 |
+
var DisplayInCurrencyEnabled = true
|
| 22 |
+
var DisplayTokenStatEnabled = true
|
| 23 |
+
|
| 24 |
+
var UsingSQLite = false
|
| 25 |
+
|
| 26 |
+
// Any options with "Secret", "Token" in its key won't be return by GetOptions
|
| 27 |
+
|
| 28 |
+
var SessionSecret = uuid.New().String()
|
| 29 |
+
var SQLitePath = "one-api.db"
|
| 30 |
+
|
| 31 |
+
var OptionMap map[string]string
|
| 32 |
+
var OptionMapRWMutex sync.RWMutex
|
| 33 |
+
|
| 34 |
+
var ItemsPerPage = 10
|
| 35 |
+
var MaxRecentItems = 100
|
| 36 |
+
|
| 37 |
+
var PasswordLoginEnabled = true
|
| 38 |
+
var PasswordRegisterEnabled = true
|
| 39 |
+
var EmailVerificationEnabled = false
|
| 40 |
+
var GitHubOAuthEnabled = false
|
| 41 |
+
var WeChatAuthEnabled = false
|
| 42 |
+
var TurnstileCheckEnabled = false
|
| 43 |
+
var RegisterEnabled = true
|
| 44 |
+
|
| 45 |
+
var EmailDomainRestrictionEnabled = false
|
| 46 |
+
var EmailDomainWhitelist = []string{
|
| 47 |
+
"gmail.com",
|
| 48 |
+
"163.com",
|
| 49 |
+
"126.com",
|
| 50 |
+
"qq.com",
|
| 51 |
+
"outlook.com",
|
| 52 |
+
"hotmail.com",
|
| 53 |
+
"icloud.com",
|
| 54 |
+
"yahoo.com",
|
| 55 |
+
"foxmail.com",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
var DebugEnabled = os.Getenv("DEBUG") == "true"
|
| 59 |
+
|
| 60 |
+
var LogConsumeEnabled = true
|
| 61 |
+
|
| 62 |
+
var SMTPServer = ""
|
| 63 |
+
var SMTPPort = 587
|
| 64 |
+
var SMTPAccount = ""
|
| 65 |
+
var SMTPFrom = ""
|
| 66 |
+
var SMTPToken = ""
|
| 67 |
+
|
| 68 |
+
var GitHubClientId = ""
|
| 69 |
+
var GitHubClientSecret = ""
|
| 70 |
+
|
| 71 |
+
var WeChatServerAddress = ""
|
| 72 |
+
var WeChatServerToken = ""
|
| 73 |
+
var WeChatAccountQRCodeImageURL = ""
|
| 74 |
+
|
| 75 |
+
var TurnstileSiteKey = ""
|
| 76 |
+
var TurnstileSecretKey = ""
|
| 77 |
+
|
| 78 |
+
var QuotaForNewUser = 0
|
| 79 |
+
var QuotaForInviter = 0
|
| 80 |
+
var QuotaForInvitee = 0
|
| 81 |
+
var ChannelDisableThreshold = 5.0
|
| 82 |
+
var AutomaticDisableChannelEnabled = false
|
| 83 |
+
var QuotaRemindThreshold = 1000
|
| 84 |
+
var PreConsumedQuota = 500
|
| 85 |
+
var ApproximateTokenEnabled = false
|
| 86 |
+
var RetryTimes = 0
|
| 87 |
+
|
| 88 |
+
var RootUserEmail = ""
|
| 89 |
+
|
| 90 |
+
var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
| 91 |
+
|
| 92 |
+
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
| 93 |
+
var RequestInterval = time.Duration(requestInterval) * time.Second
|
| 94 |
+
|
| 95 |
+
var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
|
| 96 |
+
|
| 97 |
+
const (
|
| 98 |
+
RoleGuestUser = 0
|
| 99 |
+
RoleCommonUser = 1
|
| 100 |
+
RoleAdminUser = 10
|
| 101 |
+
RoleRootUser = 100
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
var (
|
| 105 |
+
FileUploadPermission = RoleGuestUser
|
| 106 |
+
FileDownloadPermission = RoleGuestUser
|
| 107 |
+
ImageUploadPermission = RoleGuestUser
|
| 108 |
+
ImageDownloadPermission = RoleGuestUser
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
// All duration's unit is seconds
|
| 112 |
+
// Shouldn't larger then RateLimitKeyExpirationDuration
|
| 113 |
+
var (
|
| 114 |
+
GlobalApiRateLimitNum = 180
|
| 115 |
+
GlobalApiRateLimitDuration int64 = 3 * 60
|
| 116 |
+
|
| 117 |
+
GlobalWebRateLimitNum = 60
|
| 118 |
+
GlobalWebRateLimitDuration int64 = 3 * 60
|
| 119 |
+
|
| 120 |
+
UploadRateLimitNum = 10
|
| 121 |
+
UploadRateLimitDuration int64 = 60
|
| 122 |
+
|
| 123 |
+
DownloadRateLimitNum = 10
|
| 124 |
+
DownloadRateLimitDuration int64 = 60
|
| 125 |
+
|
| 126 |
+
CriticalRateLimitNum = 20
|
| 127 |
+
CriticalRateLimitDuration int64 = 20 * 60
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
var RateLimitKeyExpirationDuration = 20 * time.Minute
|
| 131 |
+
|
| 132 |
+
const (
|
| 133 |
+
UserStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 134 |
+
UserStatusDisabled = 2 // also don't use 0
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
const (
|
| 138 |
+
TokenStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 139 |
+
TokenStatusDisabled = 2 // also don't use 0
|
| 140 |
+
TokenStatusExpired = 3
|
| 141 |
+
TokenStatusExhausted = 4
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
const (
|
| 145 |
+
RedemptionCodeStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 146 |
+
RedemptionCodeStatusDisabled = 2 // also don't use 0
|
| 147 |
+
RedemptionCodeStatusUsed = 3 // also don't use 0
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
const (
|
| 151 |
+
ChannelStatusUnknown = 0
|
| 152 |
+
ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 153 |
+
ChannelStatusDisabled = 2 // also don't use 0
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
const (
|
| 157 |
+
ChannelTypeUnknown = 0
|
| 158 |
+
ChannelTypeOpenAI = 1
|
| 159 |
+
ChannelTypeAPI2D = 2
|
| 160 |
+
ChannelTypeAzure = 3
|
| 161 |
+
ChannelTypeCloseAI = 4
|
| 162 |
+
ChannelTypeOpenAISB = 5
|
| 163 |
+
ChannelTypeOpenAIMax = 6
|
| 164 |
+
ChannelTypeOhMyGPT = 7
|
| 165 |
+
ChannelTypeCustom = 8
|
| 166 |
+
ChannelTypeAILS = 9
|
| 167 |
+
ChannelTypeAIProxy = 10
|
| 168 |
+
ChannelTypePaLM = 11
|
| 169 |
+
ChannelTypeAPI2GPT = 12
|
| 170 |
+
ChannelTypeAIGC2D = 13
|
| 171 |
+
ChannelTypeAnthropic = 14
|
| 172 |
+
ChannelTypeBaidu = 15
|
| 173 |
+
ChannelTypeZhipu = 16
|
| 174 |
+
ChannelTypeAli = 17
|
| 175 |
+
ChannelTypeXunfei = 18
|
| 176 |
+
ChannelType360 = 19
|
| 177 |
+
ChannelTypeOpenRouter = 20
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
var ChannelBaseURLs = []string{
|
| 181 |
+
"", // 0
|
| 182 |
+
"https://api.openai.com", // 1
|
| 183 |
+
"https://oa.api2d.net", // 2
|
| 184 |
+
"", // 3
|
| 185 |
+
"https://api.closeai-proxy.xyz", // 4
|
| 186 |
+
"https://api.openai-sb.com", // 5
|
| 187 |
+
"https://api.openaimax.com", // 6
|
| 188 |
+
"https://api.ohmygpt.com", // 7
|
| 189 |
+
"", // 8
|
| 190 |
+
"https://api.caipacity.com", // 9
|
| 191 |
+
"https://api.aiproxy.io", // 10
|
| 192 |
+
"", // 11
|
| 193 |
+
"https://api.api2gpt.com", // 12
|
| 194 |
+
"https://api.aigc2d.com", // 13
|
| 195 |
+
"https://api.anthropic.com", // 14
|
| 196 |
+
"https://aip.baidubce.com", // 15
|
| 197 |
+
"https://open.bigmodel.cn", // 16
|
| 198 |
+
"https://dashscope.aliyuncs.com", // 17
|
| 199 |
+
"", // 18
|
| 200 |
+
"https://ai.360.cn", // 19
|
| 201 |
+
"https://openrouter.ai/api", // 20
|
| 202 |
+
}
|
common/crypto.go
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import "golang.org/x/crypto/bcrypt"
|
| 4 |
+
|
| 5 |
+
func Password2Hash(password string) (string, error) {
|
| 6 |
+
passwordBytes := []byte(password)
|
| 7 |
+
hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
| 8 |
+
return string(hashedPassword), err
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
func ValidatePasswordAndHash(password string, hash string) bool {
|
| 12 |
+
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
| 13 |
+
return err == nil
|
| 14 |
+
}
|
common/custom-event.go
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
| 2 |
+
// Use of this source code is governed by a MIT style
|
| 3 |
+
// license that can be found in the LICENSE file.
|
| 4 |
+
|
| 5 |
+
package common
|
| 6 |
+
|
| 7 |
+
import (
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"net/http"
|
| 11 |
+
"strings"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type stringWriter interface {
|
| 15 |
+
io.Writer
|
| 16 |
+
writeString(string) (int, error)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type stringWrapper struct {
|
| 20 |
+
io.Writer
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func (w stringWrapper) writeString(str string) (int, error) {
|
| 24 |
+
return w.Writer.Write([]byte(str))
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func checkWriter(writer io.Writer) stringWriter {
|
| 28 |
+
if w, ok := writer.(stringWriter); ok {
|
| 29 |
+
return w
|
| 30 |
+
} else {
|
| 31 |
+
return stringWrapper{writer}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Server-Sent Events
|
| 36 |
+
// W3C Working Draft 29 October 2009
|
| 37 |
+
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
|
| 38 |
+
|
| 39 |
+
var contentType = []string{"text/event-stream"}
|
| 40 |
+
var noCache = []string{"no-cache"}
|
| 41 |
+
|
| 42 |
+
var fieldReplacer = strings.NewReplacer(
|
| 43 |
+
"\n", "\\n",
|
| 44 |
+
"\r", "\\r")
|
| 45 |
+
|
| 46 |
+
var dataReplacer = strings.NewReplacer(
|
| 47 |
+
"\n", "\ndata:",
|
| 48 |
+
"\r", "\\r")
|
| 49 |
+
|
| 50 |
+
type CustomEvent struct {
|
| 51 |
+
Event string
|
| 52 |
+
Id string
|
| 53 |
+
Retry uint
|
| 54 |
+
Data interface{}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func encode(writer io.Writer, event CustomEvent) error {
|
| 58 |
+
w := checkWriter(writer)
|
| 59 |
+
return writeData(w, event.Data)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func writeData(w stringWriter, data interface{}) error {
|
| 63 |
+
dataReplacer.WriteString(w, fmt.Sprint(data))
|
| 64 |
+
if strings.HasPrefix(data.(string), "data") {
|
| 65 |
+
w.writeString("\n\n")
|
| 66 |
+
}
|
| 67 |
+
return nil
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
func (r CustomEvent) Render(w http.ResponseWriter) error {
|
| 71 |
+
r.WriteContentType(w)
|
| 72 |
+
return encode(w, r)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
func (r CustomEvent) WriteContentType(w http.ResponseWriter) {
|
| 76 |
+
header := w.Header()
|
| 77 |
+
header["Content-Type"] = contentType
|
| 78 |
+
|
| 79 |
+
if _, exist := header["Cache-Control"]; !exist {
|
| 80 |
+
header["Cache-Control"] = noCache
|
| 81 |
+
}
|
| 82 |
+
}
|
common/email.go
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"crypto/tls"
|
| 5 |
+
"encoding/base64"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net/smtp"
|
| 8 |
+
"strings"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func SendEmail(subject string, receiver string, content string) error {
|
| 12 |
+
if SMTPFrom == "" { // for compatibility
|
| 13 |
+
SMTPFrom = SMTPAccount
|
| 14 |
+
}
|
| 15 |
+
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
| 16 |
+
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
| 17 |
+
"From: %s<%s>\r\n"+
|
| 18 |
+
"Subject: %s\r\n"+
|
| 19 |
+
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
| 20 |
+
receiver, SystemName, SMTPFrom, encodedSubject, content))
|
| 21 |
+
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
| 22 |
+
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
| 23 |
+
to := strings.Split(receiver, ";")
|
| 24 |
+
var err error
|
| 25 |
+
if SMTPPort == 465 {
|
| 26 |
+
tlsConfig := &tls.Config{
|
| 27 |
+
InsecureSkipVerify: true,
|
| 28 |
+
ServerName: SMTPServer,
|
| 29 |
+
}
|
| 30 |
+
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
|
| 31 |
+
if err != nil {
|
| 32 |
+
return err
|
| 33 |
+
}
|
| 34 |
+
client, err := smtp.NewClient(conn, SMTPServer)
|
| 35 |
+
if err != nil {
|
| 36 |
+
return err
|
| 37 |
+
}
|
| 38 |
+
defer client.Close()
|
| 39 |
+
if err = client.Auth(auth); err != nil {
|
| 40 |
+
return err
|
| 41 |
+
}
|
| 42 |
+
if err = client.Mail(SMTPFrom); err != nil {
|
| 43 |
+
return err
|
| 44 |
+
}
|
| 45 |
+
receiverEmails := strings.Split(receiver, ";")
|
| 46 |
+
for _, receiver := range receiverEmails {
|
| 47 |
+
if err = client.Rcpt(receiver); err != nil {
|
| 48 |
+
return err
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
w, err := client.Data()
|
| 52 |
+
if err != nil {
|
| 53 |
+
return err
|
| 54 |
+
}
|
| 55 |
+
_, err = w.Write(mail)
|
| 56 |
+
if err != nil {
|
| 57 |
+
return err
|
| 58 |
+
}
|
| 59 |
+
err = w.Close()
|
| 60 |
+
if err != nil {
|
| 61 |
+
return err
|
| 62 |
+
}
|
| 63 |
+
} else {
|
| 64 |
+
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
|
| 65 |
+
}
|
| 66 |
+
return err
|
| 67 |
+
}
|
common/embed-file-system.go
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"embed"
|
| 5 |
+
"github.com/gin-contrib/static"
|
| 6 |
+
"io/fs"
|
| 7 |
+
"net/http"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
// Credit: https://github.com/gin-contrib/static/issues/19
|
| 11 |
+
|
| 12 |
+
type embedFileSystem struct {
|
| 13 |
+
http.FileSystem
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
| 17 |
+
_, err := e.Open(path)
|
| 18 |
+
if err != nil {
|
| 19 |
+
return false
|
| 20 |
+
}
|
| 21 |
+
return true
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
| 25 |
+
efs, err := fs.Sub(fsEmbed, targetPath)
|
| 26 |
+
if err != nil {
|
| 27 |
+
panic(err)
|
| 28 |
+
}
|
| 29 |
+
return embedFileSystem{
|
| 30 |
+
FileSystem: http.FS(efs),
|
| 31 |
+
}
|
| 32 |
+
}
|
common/gin.go
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
"io"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
| 11 |
+
requestBody, err := io.ReadAll(c.Request.Body)
|
| 12 |
+
if err != nil {
|
| 13 |
+
return err
|
| 14 |
+
}
|
| 15 |
+
err = c.Request.Body.Close()
|
| 16 |
+
if err != nil {
|
| 17 |
+
return err
|
| 18 |
+
}
|
| 19 |
+
err = json.Unmarshal(requestBody, &v)
|
| 20 |
+
if err != nil {
|
| 21 |
+
return err
|
| 22 |
+
}
|
| 23 |
+
// Reset request body
|
| 24 |
+
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
| 25 |
+
return nil
|
| 26 |
+
}
|
common/group-ratio.go
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import "encoding/json"
|
| 4 |
+
|
| 5 |
+
var GroupRatio = map[string]float64{
|
| 6 |
+
"default": 1,
|
| 7 |
+
"vip": 1,
|
| 8 |
+
"svip": 1,
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
func GroupRatio2JSONString() string {
|
| 12 |
+
jsonBytes, err := json.Marshal(GroupRatio)
|
| 13 |
+
if err != nil {
|
| 14 |
+
SysError("error marshalling model ratio: " + err.Error())
|
| 15 |
+
}
|
| 16 |
+
return string(jsonBytes)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
func UpdateGroupRatioByJSONString(jsonStr string) error {
|
| 20 |
+
GroupRatio = make(map[string]float64)
|
| 21 |
+
return json.Unmarshal([]byte(jsonStr), &GroupRatio)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func GetGroupRatio(name string) float64 {
|
| 25 |
+
ratio, ok := GroupRatio[name]
|
| 26 |
+
if !ok {
|
| 27 |
+
SysError("group ratio not found: " + name)
|
| 28 |
+
return 1
|
| 29 |
+
}
|
| 30 |
+
return ratio
|
| 31 |
+
}
|
common/init.go
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"flag"
|
| 5 |
+
"fmt"
|
| 6 |
+
"log"
|
| 7 |
+
"os"
|
| 8 |
+
"path/filepath"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
var (
|
| 12 |
+
Port = flag.Int("port", 3000, "the listening port")
|
| 13 |
+
PrintVersion = flag.Bool("version", false, "print version and exit")
|
| 14 |
+
PrintHelp = flag.Bool("help", false, "print help and exit")
|
| 15 |
+
LogDir = flag.String("log-dir", "", "specify the log directory")
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
func printHelp() {
|
| 19 |
+
fmt.Println("One API " + Version + " - All in one API service for OpenAI API.")
|
| 20 |
+
fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
|
| 21 |
+
fmt.Println("GitHub: https://github.com/songquanpeng/one-api")
|
| 22 |
+
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func init() {
|
| 26 |
+
flag.Parse()
|
| 27 |
+
|
| 28 |
+
if *PrintVersion {
|
| 29 |
+
fmt.Println(Version)
|
| 30 |
+
os.Exit(0)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if *PrintHelp {
|
| 34 |
+
printHelp()
|
| 35 |
+
os.Exit(0)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
if os.Getenv("SESSION_SECRET") != "" {
|
| 39 |
+
SessionSecret = os.Getenv("SESSION_SECRET")
|
| 40 |
+
}
|
| 41 |
+
if os.Getenv("SQLITE_PATH") != "" {
|
| 42 |
+
SQLitePath = os.Getenv("SQLITE_PATH")
|
| 43 |
+
}
|
| 44 |
+
if *LogDir != "" {
|
| 45 |
+
var err error
|
| 46 |
+
*LogDir, err = filepath.Abs(*LogDir)
|
| 47 |
+
if err != nil {
|
| 48 |
+
log.Fatal(err)
|
| 49 |
+
}
|
| 50 |
+
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
|
| 51 |
+
err = os.Mkdir(*LogDir, 0777)
|
| 52 |
+
if err != nil {
|
| 53 |
+
log.Fatal(err)
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
common/logger.go
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"github.com/gin-gonic/gin"
|
| 6 |
+
"io"
|
| 7 |
+
"log"
|
| 8 |
+
"os"
|
| 9 |
+
"path/filepath"
|
| 10 |
+
"time"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func SetupGinLog() {
|
| 14 |
+
if *LogDir != "" {
|
| 15 |
+
commonLogPath := filepath.Join(*LogDir, "common.log")
|
| 16 |
+
errorLogPath := filepath.Join(*LogDir, "error.log")
|
| 17 |
+
commonFd, err := os.OpenFile(commonLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
| 18 |
+
if err != nil {
|
| 19 |
+
log.Fatal("failed to open log file")
|
| 20 |
+
}
|
| 21 |
+
errorFd, err := os.OpenFile(errorLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
| 22 |
+
if err != nil {
|
| 23 |
+
log.Fatal("failed to open log file")
|
| 24 |
+
}
|
| 25 |
+
gin.DefaultWriter = io.MultiWriter(os.Stdout, commonFd)
|
| 26 |
+
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, errorFd)
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func SysLog(s string) {
|
| 31 |
+
t := time.Now()
|
| 32 |
+
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
func SysError(s string) {
|
| 36 |
+
t := time.Now()
|
| 37 |
+
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
func FatalLog(v ...any) {
|
| 41 |
+
t := time.Now()
|
| 42 |
+
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
| 43 |
+
os.Exit(1)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func LogQuota(quota int) string {
|
| 47 |
+
if DisplayInCurrencyEnabled {
|
| 48 |
+
return fmt.Sprintf("$%.6f 额度", float64(quota)/QuotaPerUnit)
|
| 49 |
+
} else {
|
| 50 |
+
return fmt.Sprintf("%d 点额度", quota)
|
| 51 |
+
}
|
| 52 |
+
}
|
common/model-ratio.go
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"strings"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
// ModelRatio
|
| 9 |
+
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
| 10 |
+
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
|
| 11 |
+
// https://openai.com/pricing
|
| 12 |
+
// TODO: when a new api is enabled, check the pricing here
|
| 13 |
+
// 1 === $0.002 / 1K tokens
|
| 14 |
+
// 1 === ¥0.014 / 1k tokens
|
| 15 |
+
var ModelRatio = map[string]float64{
|
| 16 |
+
"gpt-4": 15,
|
| 17 |
+
"gpt-4-0314": 15,
|
| 18 |
+
"gpt-4-0613": 15,
|
| 19 |
+
"gpt-4-32k": 30,
|
| 20 |
+
"gpt-4-32k-0314": 30,
|
| 21 |
+
"gpt-4-32k-0613": 30,
|
| 22 |
+
"gpt-3.5-turbo": 0.75, // $0.0015 / 1K tokens
|
| 23 |
+
"gpt-3.5-turbo-0301": 0.75,
|
| 24 |
+
"gpt-3.5-turbo-0613": 0.75,
|
| 25 |
+
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
|
| 26 |
+
"gpt-3.5-turbo-16k-0613": 1.5,
|
| 27 |
+
"text-ada-001": 0.2,
|
| 28 |
+
"text-babbage-001": 0.25,
|
| 29 |
+
"text-curie-001": 1,
|
| 30 |
+
"text-davinci-002": 10,
|
| 31 |
+
"text-davinci-003": 10,
|
| 32 |
+
"text-davinci-edit-001": 10,
|
| 33 |
+
"code-davinci-edit-001": 10,
|
| 34 |
+
"whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens
|
| 35 |
+
"davinci": 10,
|
| 36 |
+
"curie": 10,
|
| 37 |
+
"babbage": 10,
|
| 38 |
+
"ada": 10,
|
| 39 |
+
"text-embedding-ada-002": 0.05,
|
| 40 |
+
"text-search-ada-doc-001": 10,
|
| 41 |
+
"text-moderation-stable": 0.1,
|
| 42 |
+
"text-moderation-latest": 0.1,
|
| 43 |
+
"dall-e": 8,
|
| 44 |
+
"claude-instant-1": 0.815, // $1.63 / 1M tokens
|
| 45 |
+
"claude-2": 5.51, // $11.02 / 1M tokens
|
| 46 |
+
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens
|
| 47 |
+
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens
|
| 48 |
+
"Embedding-V1": 0.1429, // ¥0.002 / 1k tokens
|
| 49 |
+
"PaLM-2": 1,
|
| 50 |
+
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
| 51 |
+
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
| 52 |
+
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
|
| 53 |
+
"qwen-v1": 0.8572, // TBD: https://help.aliyun.com/document_detail/2399482.html?spm=a2c4g.2399482.0.0.1ad347feilAgag
|
| 54 |
+
"qwen-plus-v1": 0.5715, // Same as above
|
| 55 |
+
"SparkDesk": 0.8572, // TBD
|
| 56 |
+
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
|
| 57 |
+
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
| 58 |
+
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
| 59 |
+
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
| 60 |
+
"360GPT_S2_V9.4": 0.8572, // ¥0.012 / 1k tokens
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
func ModelRatio2JSONString() string {
|
| 64 |
+
jsonBytes, err := json.Marshal(ModelRatio)
|
| 65 |
+
if err != nil {
|
| 66 |
+
SysError("error marshalling model ratio: " + err.Error())
|
| 67 |
+
}
|
| 68 |
+
return string(jsonBytes)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
func UpdateModelRatioByJSONString(jsonStr string) error {
|
| 72 |
+
ModelRatio = make(map[string]float64)
|
| 73 |
+
return json.Unmarshal([]byte(jsonStr), &ModelRatio)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
func GetModelRatio(name string) float64 {
|
| 77 |
+
ratio, ok := ModelRatio[name]
|
| 78 |
+
if !ok {
|
| 79 |
+
SysError("model ratio not found: " + name)
|
| 80 |
+
return 30
|
| 81 |
+
}
|
| 82 |
+
return ratio
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func GetCompletionRatio(name string) float64 {
|
| 86 |
+
if strings.HasPrefix(name, "gpt-3.5") {
|
| 87 |
+
return 1.333333
|
| 88 |
+
}
|
| 89 |
+
if strings.HasPrefix(name, "gpt-4") {
|
| 90 |
+
return 2
|
| 91 |
+
}
|
| 92 |
+
if strings.HasPrefix(name, "claude-instant-1") {
|
| 93 |
+
return 3.38
|
| 94 |
+
}
|
| 95 |
+
if strings.HasPrefix(name, "claude-2") {
|
| 96 |
+
return 2.965517
|
| 97 |
+
}
|
| 98 |
+
return 1
|
| 99 |
+
}
|
common/rate-limit.go
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"sync"
|
| 5 |
+
"time"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type InMemoryRateLimiter struct {
|
| 9 |
+
store map[string]*[]int64
|
| 10 |
+
mutex sync.Mutex
|
| 11 |
+
expirationDuration time.Duration
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) {
|
| 15 |
+
if l.store == nil {
|
| 16 |
+
l.mutex.Lock()
|
| 17 |
+
if l.store == nil {
|
| 18 |
+
l.store = make(map[string]*[]int64)
|
| 19 |
+
l.expirationDuration = expirationDuration
|
| 20 |
+
if expirationDuration > 0 {
|
| 21 |
+
go l.clearExpiredItems()
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
l.mutex.Unlock()
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func (l *InMemoryRateLimiter) clearExpiredItems() {
|
| 29 |
+
for {
|
| 30 |
+
time.Sleep(l.expirationDuration)
|
| 31 |
+
l.mutex.Lock()
|
| 32 |
+
now := time.Now().Unix()
|
| 33 |
+
for key := range l.store {
|
| 34 |
+
queue := l.store[key]
|
| 35 |
+
size := len(*queue)
|
| 36 |
+
if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) {
|
| 37 |
+
delete(l.store, key)
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
l.mutex.Unlock()
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Request parameter duration's unit is seconds
|
| 45 |
+
func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool {
|
| 46 |
+
l.mutex.Lock()
|
| 47 |
+
defer l.mutex.Unlock()
|
| 48 |
+
// [old <-- new]
|
| 49 |
+
queue, ok := l.store[key]
|
| 50 |
+
now := time.Now().Unix()
|
| 51 |
+
if ok {
|
| 52 |
+
if len(*queue) < maxRequestNum {
|
| 53 |
+
*queue = append(*queue, now)
|
| 54 |
+
return true
|
| 55 |
+
} else {
|
| 56 |
+
if now-(*queue)[0] >= duration {
|
| 57 |
+
*queue = (*queue)[1:]
|
| 58 |
+
*queue = append(*queue, now)
|
| 59 |
+
return true
|
| 60 |
+
} else {
|
| 61 |
+
return false
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
} else {
|
| 65 |
+
s := make([]int64, 0, maxRequestNum)
|
| 66 |
+
l.store[key] = &s
|
| 67 |
+
*(l.store[key]) = append(*(l.store[key]), now)
|
| 68 |
+
}
|
| 69 |
+
return true
|
| 70 |
+
}
|
common/redis.go
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"github.com/go-redis/redis/v8"
|
| 6 |
+
"os"
|
| 7 |
+
"time"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
var RDB *redis.Client
|
| 11 |
+
var RedisEnabled = true
|
| 12 |
+
|
| 13 |
+
// InitRedisClient This function is called after init()
|
| 14 |
+
func InitRedisClient() (err error) {
|
| 15 |
+
if os.Getenv("REDIS_CONN_STRING") == "" {
|
| 16 |
+
RedisEnabled = false
|
| 17 |
+
SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
|
| 18 |
+
return nil
|
| 19 |
+
}
|
| 20 |
+
if os.Getenv("SYNC_FREQUENCY") == "" {
|
| 21 |
+
RedisEnabled = false
|
| 22 |
+
SysLog("SYNC_FREQUENCY not set, Redis is disabled")
|
| 23 |
+
return nil
|
| 24 |
+
}
|
| 25 |
+
SysLog("Redis is enabled")
|
| 26 |
+
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
| 27 |
+
if err != nil {
|
| 28 |
+
FatalLog("failed to parse Redis connection string: " + err.Error())
|
| 29 |
+
}
|
| 30 |
+
RDB = redis.NewClient(opt)
|
| 31 |
+
|
| 32 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 33 |
+
defer cancel()
|
| 34 |
+
|
| 35 |
+
_, err = RDB.Ping(ctx).Result()
|
| 36 |
+
if err != nil {
|
| 37 |
+
FatalLog("Redis ping test failed: " + err.Error())
|
| 38 |
+
}
|
| 39 |
+
return err
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func ParseRedisOption() *redis.Options {
|
| 43 |
+
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
| 44 |
+
if err != nil {
|
| 45 |
+
FatalLog("failed to parse Redis connection string: " + err.Error())
|
| 46 |
+
}
|
| 47 |
+
return opt
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func RedisSet(key string, value string, expiration time.Duration) error {
|
| 51 |
+
ctx := context.Background()
|
| 52 |
+
return RDB.Set(ctx, key, value, expiration).Err()
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func RedisGet(key string) (string, error) {
|
| 56 |
+
ctx := context.Background()
|
| 57 |
+
return RDB.Get(ctx, key).Result()
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
func RedisDel(key string) error {
|
| 61 |
+
ctx := context.Background()
|
| 62 |
+
return RDB.Del(ctx, key).Err()
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
func RedisDecrease(key string, value int64) error {
|
| 66 |
+
ctx := context.Background()
|
| 67 |
+
return RDB.DecrBy(ctx, key, value).Err()
|
| 68 |
+
}
|
common/utils.go
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
"html/template"
|
| 7 |
+
"log"
|
| 8 |
+
"math/rand"
|
| 9 |
+
"net"
|
| 10 |
+
"os"
|
| 11 |
+
"os/exec"
|
| 12 |
+
"runtime"
|
| 13 |
+
"strconv"
|
| 14 |
+
"strings"
|
| 15 |
+
"time"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
func OpenBrowser(url string) {
|
| 19 |
+
var err error
|
| 20 |
+
|
| 21 |
+
switch runtime.GOOS {
|
| 22 |
+
case "linux":
|
| 23 |
+
err = exec.Command("xdg-open", url).Start()
|
| 24 |
+
case "windows":
|
| 25 |
+
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
| 26 |
+
case "darwin":
|
| 27 |
+
err = exec.Command("open", url).Start()
|
| 28 |
+
}
|
| 29 |
+
if err != nil {
|
| 30 |
+
log.Println(err)
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func GetIp() (ip string) {
|
| 35 |
+
ips, err := net.InterfaceAddrs()
|
| 36 |
+
if err != nil {
|
| 37 |
+
log.Println(err)
|
| 38 |
+
return ip
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
for _, a := range ips {
|
| 42 |
+
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
| 43 |
+
if ipNet.IP.To4() != nil {
|
| 44 |
+
ip = ipNet.IP.String()
|
| 45 |
+
if strings.HasPrefix(ip, "10") {
|
| 46 |
+
return
|
| 47 |
+
}
|
| 48 |
+
if strings.HasPrefix(ip, "172") {
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
if strings.HasPrefix(ip, "192.168") {
|
| 52 |
+
return
|
| 53 |
+
}
|
| 54 |
+
ip = ""
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
var sizeKB = 1024
|
| 62 |
+
var sizeMB = sizeKB * 1024
|
| 63 |
+
var sizeGB = sizeMB * 1024
|
| 64 |
+
|
| 65 |
+
func Bytes2Size(num int64) string {
|
| 66 |
+
numStr := ""
|
| 67 |
+
unit := "B"
|
| 68 |
+
if num/int64(sizeGB) > 1 {
|
| 69 |
+
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
|
| 70 |
+
unit = "GB"
|
| 71 |
+
} else if num/int64(sizeMB) > 1 {
|
| 72 |
+
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
|
| 73 |
+
unit = "MB"
|
| 74 |
+
} else if num/int64(sizeKB) > 1 {
|
| 75 |
+
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
|
| 76 |
+
unit = "KB"
|
| 77 |
+
} else {
|
| 78 |
+
numStr = fmt.Sprintf("%d", num)
|
| 79 |
+
}
|
| 80 |
+
return numStr + " " + unit
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
func Seconds2Time(num int) (time string) {
|
| 84 |
+
if num/31104000 > 0 {
|
| 85 |
+
time += strconv.Itoa(num/31104000) + " 年 "
|
| 86 |
+
num %= 31104000
|
| 87 |
+
}
|
| 88 |
+
if num/2592000 > 0 {
|
| 89 |
+
time += strconv.Itoa(num/2592000) + " 个月 "
|
| 90 |
+
num %= 2592000
|
| 91 |
+
}
|
| 92 |
+
if num/86400 > 0 {
|
| 93 |
+
time += strconv.Itoa(num/86400) + " 天 "
|
| 94 |
+
num %= 86400
|
| 95 |
+
}
|
| 96 |
+
if num/3600 > 0 {
|
| 97 |
+
time += strconv.Itoa(num/3600) + " 小时 "
|
| 98 |
+
num %= 3600
|
| 99 |
+
}
|
| 100 |
+
if num/60 > 0 {
|
| 101 |
+
time += strconv.Itoa(num/60) + " 分钟 "
|
| 102 |
+
num %= 60
|
| 103 |
+
}
|
| 104 |
+
time += strconv.Itoa(num) + " 秒"
|
| 105 |
+
return
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
func Interface2String(inter interface{}) string {
|
| 109 |
+
switch inter.(type) {
|
| 110 |
+
case string:
|
| 111 |
+
return inter.(string)
|
| 112 |
+
case int:
|
| 113 |
+
return fmt.Sprintf("%d", inter.(int))
|
| 114 |
+
case float64:
|
| 115 |
+
return fmt.Sprintf("%f", inter.(float64))
|
| 116 |
+
}
|
| 117 |
+
return "Not Implemented"
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
func UnescapeHTML(x string) interface{} {
|
| 121 |
+
return template.HTML(x)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
func IntMax(a int, b int) int {
|
| 125 |
+
if a >= b {
|
| 126 |
+
return a
|
| 127 |
+
} else {
|
| 128 |
+
return b
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
func GetUUID() string {
|
| 133 |
+
code := uuid.New().String()
|
| 134 |
+
code = strings.Replace(code, "-", "", -1)
|
| 135 |
+
return code
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
| 139 |
+
|
| 140 |
+
func init() {
|
| 141 |
+
rand.Seed(time.Now().UnixNano())
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
func GenerateKey() string {
|
| 145 |
+
rand.Seed(time.Now().UnixNano())
|
| 146 |
+
key := make([]byte, 48)
|
| 147 |
+
for i := 0; i < 16; i++ {
|
| 148 |
+
key[i] = keyChars[rand.Intn(len(keyChars))]
|
| 149 |
+
}
|
| 150 |
+
uuid_ := GetUUID()
|
| 151 |
+
for i := 0; i < 32; i++ {
|
| 152 |
+
c := uuid_[i]
|
| 153 |
+
if i%2 == 0 && c >= 'a' && c <= 'z' {
|
| 154 |
+
c = c - 'a' + 'A'
|
| 155 |
+
}
|
| 156 |
+
key[i+16] = c
|
| 157 |
+
}
|
| 158 |
+
return string(key)
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
func GetRandomString(length int) string {
|
| 162 |
+
rand.Seed(time.Now().UnixNano())
|
| 163 |
+
key := make([]byte, length)
|
| 164 |
+
for i := 0; i < length; i++ {
|
| 165 |
+
key[i] = keyChars[rand.Intn(len(keyChars))]
|
| 166 |
+
}
|
| 167 |
+
return string(key)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
func GetTimestamp() int64 {
|
| 171 |
+
return time.Now().Unix()
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
func Max(a int, b int) int {
|
| 175 |
+
if a >= b {
|
| 176 |
+
return a
|
| 177 |
+
} else {
|
| 178 |
+
return b
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
func GetOrDefault(env string, defaultValue int) int {
|
| 183 |
+
if env == "" || os.Getenv(env) == "" {
|
| 184 |
+
return defaultValue
|
| 185 |
+
}
|
| 186 |
+
num, err := strconv.Atoi(os.Getenv(env))
|
| 187 |
+
if err != nil {
|
| 188 |
+
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
|
| 189 |
+
return defaultValue
|
| 190 |
+
}
|
| 191 |
+
return num
|
| 192 |
+
}
|
common/validate.go
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import "github.com/go-playground/validator/v10"
|
| 4 |
+
|
| 5 |
+
var Validate *validator.Validate
|
| 6 |
+
|
| 7 |
+
func init() {
|
| 8 |
+
Validate = validator.New()
|
| 9 |
+
}
|
common/verification.go
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package common
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/google/uuid"
|
| 5 |
+
"strings"
|
| 6 |
+
"sync"
|
| 7 |
+
"time"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type verificationValue struct {
|
| 11 |
+
code string
|
| 12 |
+
time time.Time
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const (
|
| 16 |
+
EmailVerificationPurpose = "v"
|
| 17 |
+
PasswordResetPurpose = "r"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
var verificationMutex sync.Mutex
|
| 21 |
+
var verificationMap map[string]verificationValue
|
| 22 |
+
var verificationMapMaxSize = 10
|
| 23 |
+
var VerificationValidMinutes = 10
|
| 24 |
+
|
| 25 |
+
func GenerateVerificationCode(length int) string {
|
| 26 |
+
code := uuid.New().String()
|
| 27 |
+
code = strings.Replace(code, "-", "", -1)
|
| 28 |
+
if length == 0 {
|
| 29 |
+
return code
|
| 30 |
+
}
|
| 31 |
+
return code[:length]
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func RegisterVerificationCodeWithKey(key string, code string, purpose string) {
|
| 35 |
+
verificationMutex.Lock()
|
| 36 |
+
defer verificationMutex.Unlock()
|
| 37 |
+
verificationMap[purpose+key] = verificationValue{
|
| 38 |
+
code: code,
|
| 39 |
+
time: time.Now(),
|
| 40 |
+
}
|
| 41 |
+
if len(verificationMap) > verificationMapMaxSize {
|
| 42 |
+
removeExpiredPairs()
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func VerifyCodeWithKey(key string, code string, purpose string) bool {
|
| 47 |
+
verificationMutex.Lock()
|
| 48 |
+
defer verificationMutex.Unlock()
|
| 49 |
+
value, okay := verificationMap[purpose+key]
|
| 50 |
+
now := time.Now()
|
| 51 |
+
if !okay || int(now.Sub(value.time).Seconds()) >= VerificationValidMinutes*60 {
|
| 52 |
+
return false
|
| 53 |
+
}
|
| 54 |
+
return code == value.code
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func DeleteKey(key string, purpose string) {
|
| 58 |
+
verificationMutex.Lock()
|
| 59 |
+
defer verificationMutex.Unlock()
|
| 60 |
+
delete(verificationMap, purpose+key)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// no lock inside, so the caller must lock the verificationMap before calling!
|
| 64 |
+
func removeExpiredPairs() {
|
| 65 |
+
now := time.Now()
|
| 66 |
+
for key := range verificationMap {
|
| 67 |
+
if int(now.Sub(verificationMap[key].time).Seconds()) >= VerificationValidMinutes*60 {
|
| 68 |
+
delete(verificationMap, key)
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
func init() {
|
| 74 |
+
verificationMutex.Lock()
|
| 75 |
+
defer verificationMutex.Unlock()
|
| 76 |
+
verificationMap = make(map[string]verificationValue)
|
| 77 |
+
}
|
controller/billing.go
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"one-api/common"
|
| 6 |
+
"one-api/model"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func GetSubscription(c *gin.Context) {
|
| 10 |
+
var remainQuota int
|
| 11 |
+
var usedQuota int
|
| 12 |
+
var err error
|
| 13 |
+
var token *model.Token
|
| 14 |
+
var expiredTime int64
|
| 15 |
+
if common.DisplayTokenStatEnabled {
|
| 16 |
+
tokenId := c.GetInt("token_id")
|
| 17 |
+
token, err = model.GetTokenById(tokenId)
|
| 18 |
+
expiredTime = token.ExpiredTime
|
| 19 |
+
remainQuota = token.RemainQuota
|
| 20 |
+
usedQuota = token.UsedQuota
|
| 21 |
+
} else {
|
| 22 |
+
userId := c.GetInt("id")
|
| 23 |
+
remainQuota, err = model.GetUserQuota(userId)
|
| 24 |
+
usedQuota, err = model.GetUserUsedQuota(userId)
|
| 25 |
+
}
|
| 26 |
+
if expiredTime <= 0 {
|
| 27 |
+
expiredTime = 0
|
| 28 |
+
}
|
| 29 |
+
if err != nil {
|
| 30 |
+
openAIError := OpenAIError{
|
| 31 |
+
Message: err.Error(),
|
| 32 |
+
Type: "one_api_error",
|
| 33 |
+
}
|
| 34 |
+
c.JSON(200, gin.H{
|
| 35 |
+
"error": openAIError,
|
| 36 |
+
})
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
quota := remainQuota + usedQuota
|
| 40 |
+
amount := float64(quota)
|
| 41 |
+
if common.DisplayInCurrencyEnabled {
|
| 42 |
+
amount /= common.QuotaPerUnit
|
| 43 |
+
}
|
| 44 |
+
if token != nil && token.UnlimitedQuota {
|
| 45 |
+
amount = 100000000
|
| 46 |
+
}
|
| 47 |
+
subscription := OpenAISubscriptionResponse{
|
| 48 |
+
Object: "billing_subscription",
|
| 49 |
+
HasPaymentMethod: true,
|
| 50 |
+
SoftLimitUSD: amount,
|
| 51 |
+
HardLimitUSD: amount,
|
| 52 |
+
SystemHardLimitUSD: amount,
|
| 53 |
+
AccessUntil: expiredTime,
|
| 54 |
+
}
|
| 55 |
+
c.JSON(200, subscription)
|
| 56 |
+
return
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func GetUsage(c *gin.Context) {
|
| 60 |
+
var quota int
|
| 61 |
+
var err error
|
| 62 |
+
var token *model.Token
|
| 63 |
+
if common.DisplayTokenStatEnabled {
|
| 64 |
+
tokenId := c.GetInt("token_id")
|
| 65 |
+
token, err = model.GetTokenById(tokenId)
|
| 66 |
+
quota = token.UsedQuota
|
| 67 |
+
} else {
|
| 68 |
+
userId := c.GetInt("id")
|
| 69 |
+
quota, err = model.GetUserUsedQuota(userId)
|
| 70 |
+
}
|
| 71 |
+
if err != nil {
|
| 72 |
+
openAIError := OpenAIError{
|
| 73 |
+
Message: err.Error(),
|
| 74 |
+
Type: "one_api_error",
|
| 75 |
+
}
|
| 76 |
+
c.JSON(200, gin.H{
|
| 77 |
+
"error": openAIError,
|
| 78 |
+
})
|
| 79 |
+
return
|
| 80 |
+
}
|
| 81 |
+
amount := float64(quota)
|
| 82 |
+
if common.DisplayInCurrencyEnabled {
|
| 83 |
+
amount /= common.QuotaPerUnit
|
| 84 |
+
}
|
| 85 |
+
usage := OpenAIUsageResponse{
|
| 86 |
+
Object: "list",
|
| 87 |
+
TotalUsage: amount * 100,
|
| 88 |
+
}
|
| 89 |
+
c.JSON(200, usage)
|
| 90 |
+
return
|
| 91 |
+
}
|
controller/channel-billing.go
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"one-api/common"
|
| 10 |
+
"one-api/model"
|
| 11 |
+
"strconv"
|
| 12 |
+
"time"
|
| 13 |
+
|
| 14 |
+
"github.com/gin-gonic/gin"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
// https://github.com/songquanpeng/one-api/issues/79
|
| 18 |
+
|
| 19 |
+
type OpenAISubscriptionResponse struct {
|
| 20 |
+
Object string `json:"object"`
|
| 21 |
+
HasPaymentMethod bool `json:"has_payment_method"`
|
| 22 |
+
SoftLimitUSD float64 `json:"soft_limit_usd"`
|
| 23 |
+
HardLimitUSD float64 `json:"hard_limit_usd"`
|
| 24 |
+
SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
|
| 25 |
+
AccessUntil int64 `json:"access_until"`
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
type OpenAIUsageDailyCost struct {
|
| 29 |
+
Timestamp float64 `json:"timestamp"`
|
| 30 |
+
LineItems []struct {
|
| 31 |
+
Name string `json:"name"`
|
| 32 |
+
Cost float64 `json:"cost"`
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
type OpenAICreditGrants struct {
|
| 37 |
+
Object string `json:"object"`
|
| 38 |
+
TotalGranted float64 `json:"total_granted"`
|
| 39 |
+
TotalUsed float64 `json:"total_used"`
|
| 40 |
+
TotalAvailable float64 `json:"total_available"`
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
type OpenAIUsageResponse struct {
|
| 44 |
+
Object string `json:"object"`
|
| 45 |
+
//DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"`
|
| 46 |
+
TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
type OpenAISBUsageResponse struct {
|
| 50 |
+
Msg string `json:"msg"`
|
| 51 |
+
Data *struct {
|
| 52 |
+
Credit string `json:"credit"`
|
| 53 |
+
} `json:"data"`
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
type AIProxyUserOverviewResponse struct {
|
| 57 |
+
Success bool `json:"success"`
|
| 58 |
+
Message string `json:"message"`
|
| 59 |
+
ErrorCode int `json:"error_code"`
|
| 60 |
+
Data struct {
|
| 61 |
+
TotalPoints float64 `json:"totalPoints"`
|
| 62 |
+
} `json:"data"`
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
type API2GPTUsageResponse struct {
|
| 66 |
+
Object string `json:"object"`
|
| 67 |
+
TotalGranted float64 `json:"total_granted"`
|
| 68 |
+
TotalUsed float64 `json:"total_used"`
|
| 69 |
+
TotalRemaining float64 `json:"total_remaining"`
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
type APGC2DGPTUsageResponse struct {
|
| 73 |
+
//Grants interface{} `json:"grants"`
|
| 74 |
+
Object string `json:"object"`
|
| 75 |
+
TotalAvailable float64 `json:"total_available"`
|
| 76 |
+
TotalGranted float64 `json:"total_granted"`
|
| 77 |
+
TotalUsed float64 `json:"total_used"`
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// GetAuthHeader get auth header
|
| 81 |
+
func GetAuthHeader(token string) http.Header {
|
| 82 |
+
h := http.Header{}
|
| 83 |
+
h.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
| 84 |
+
return h
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
| 88 |
+
req, err := http.NewRequest(method, url, nil)
|
| 89 |
+
if err != nil {
|
| 90 |
+
return nil, err
|
| 91 |
+
}
|
| 92 |
+
for k := range headers {
|
| 93 |
+
req.Header.Add(k, headers.Get(k))
|
| 94 |
+
}
|
| 95 |
+
res, err := httpClient.Do(req)
|
| 96 |
+
if err != nil {
|
| 97 |
+
return nil, err
|
| 98 |
+
}
|
| 99 |
+
if res.StatusCode != http.StatusOK {
|
| 100 |
+
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
| 101 |
+
}
|
| 102 |
+
body, err := io.ReadAll(res.Body)
|
| 103 |
+
if err != nil {
|
| 104 |
+
return nil, err
|
| 105 |
+
}
|
| 106 |
+
err = res.Body.Close()
|
| 107 |
+
if err != nil {
|
| 108 |
+
return nil, err
|
| 109 |
+
}
|
| 110 |
+
return body, nil
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
func updateChannelCloseAIBalance(channel *model.Channel) (float64, error) {
|
| 114 |
+
url := fmt.Sprintf("%s/dashboard/billing/credit_grants", channel.BaseURL)
|
| 115 |
+
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 116 |
+
|
| 117 |
+
if err != nil {
|
| 118 |
+
return 0, err
|
| 119 |
+
}
|
| 120 |
+
response := OpenAICreditGrants{}
|
| 121 |
+
err = json.Unmarshal(body, &response)
|
| 122 |
+
if err != nil {
|
| 123 |
+
return 0, err
|
| 124 |
+
}
|
| 125 |
+
channel.UpdateBalance(response.TotalAvailable)
|
| 126 |
+
return response.TotalAvailable, nil
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) {
|
| 130 |
+
url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key)
|
| 131 |
+
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 132 |
+
if err != nil {
|
| 133 |
+
return 0, err
|
| 134 |
+
}
|
| 135 |
+
response := OpenAISBUsageResponse{}
|
| 136 |
+
err = json.Unmarshal(body, &response)
|
| 137 |
+
if err != nil {
|
| 138 |
+
return 0, err
|
| 139 |
+
}
|
| 140 |
+
if response.Data == nil {
|
| 141 |
+
return 0, errors.New(response.Msg)
|
| 142 |
+
}
|
| 143 |
+
balance, err := strconv.ParseFloat(response.Data.Credit, 64)
|
| 144 |
+
if err != nil {
|
| 145 |
+
return 0, err
|
| 146 |
+
}
|
| 147 |
+
channel.UpdateBalance(balance)
|
| 148 |
+
return balance, nil
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
func updateChannelAIProxyBalance(channel *model.Channel) (float64, error) {
|
| 152 |
+
url := "https://aiproxy.io/api/report/getUserOverview"
|
| 153 |
+
headers := http.Header{}
|
| 154 |
+
headers.Add("Api-Key", channel.Key)
|
| 155 |
+
body, err := GetResponseBody("GET", url, channel, headers)
|
| 156 |
+
if err != nil {
|
| 157 |
+
return 0, err
|
| 158 |
+
}
|
| 159 |
+
response := AIProxyUserOverviewResponse{}
|
| 160 |
+
err = json.Unmarshal(body, &response)
|
| 161 |
+
if err != nil {
|
| 162 |
+
return 0, err
|
| 163 |
+
}
|
| 164 |
+
if !response.Success {
|
| 165 |
+
return 0, fmt.Errorf("code: %d, message: %s", response.ErrorCode, response.Message)
|
| 166 |
+
}
|
| 167 |
+
channel.UpdateBalance(response.Data.TotalPoints)
|
| 168 |
+
return response.Data.TotalPoints, nil
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) {
|
| 172 |
+
url := "https://api.api2gpt.com/dashboard/billing/credit_grants"
|
| 173 |
+
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 174 |
+
|
| 175 |
+
if err != nil {
|
| 176 |
+
return 0, err
|
| 177 |
+
}
|
| 178 |
+
response := API2GPTUsageResponse{}
|
| 179 |
+
err = json.Unmarshal(body, &response)
|
| 180 |
+
if err != nil {
|
| 181 |
+
return 0, err
|
| 182 |
+
}
|
| 183 |
+
channel.UpdateBalance(response.TotalRemaining)
|
| 184 |
+
return response.TotalRemaining, nil
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) {
|
| 188 |
+
url := "https://api.aigc2d.com/dashboard/billing/credit_grants"
|
| 189 |
+
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 190 |
+
if err != nil {
|
| 191 |
+
return 0, err
|
| 192 |
+
}
|
| 193 |
+
response := APGC2DGPTUsageResponse{}
|
| 194 |
+
err = json.Unmarshal(body, &response)
|
| 195 |
+
if err != nil {
|
| 196 |
+
return 0, err
|
| 197 |
+
}
|
| 198 |
+
channel.UpdateBalance(response.TotalAvailable)
|
| 199 |
+
return response.TotalAvailable, nil
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
| 203 |
+
baseURL := common.ChannelBaseURLs[channel.Type]
|
| 204 |
+
if channel.BaseURL == "" {
|
| 205 |
+
channel.BaseURL = baseURL
|
| 206 |
+
}
|
| 207 |
+
switch channel.Type {
|
| 208 |
+
case common.ChannelTypeOpenAI:
|
| 209 |
+
if channel.BaseURL != "" {
|
| 210 |
+
baseURL = channel.BaseURL
|
| 211 |
+
}
|
| 212 |
+
case common.ChannelTypeAzure:
|
| 213 |
+
return 0, errors.New("尚未实现")
|
| 214 |
+
case common.ChannelTypeCustom:
|
| 215 |
+
baseURL = channel.BaseURL
|
| 216 |
+
case common.ChannelTypeCloseAI:
|
| 217 |
+
return updateChannelCloseAIBalance(channel)
|
| 218 |
+
case common.ChannelTypeOpenAISB:
|
| 219 |
+
return updateChannelOpenAISBBalance(channel)
|
| 220 |
+
case common.ChannelTypeAIProxy:
|
| 221 |
+
return updateChannelAIProxyBalance(channel)
|
| 222 |
+
case common.ChannelTypeAPI2GPT:
|
| 223 |
+
return updateChannelAPI2GPTBalance(channel)
|
| 224 |
+
case common.ChannelTypeAIGC2D:
|
| 225 |
+
return updateChannelAIGC2DBalance(channel)
|
| 226 |
+
default:
|
| 227 |
+
return 0, errors.New("尚未实现")
|
| 228 |
+
}
|
| 229 |
+
url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL)
|
| 230 |
+
|
| 231 |
+
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 232 |
+
if err != nil {
|
| 233 |
+
return 0, err
|
| 234 |
+
}
|
| 235 |
+
subscription := OpenAISubscriptionResponse{}
|
| 236 |
+
err = json.Unmarshal(body, &subscription)
|
| 237 |
+
if err != nil {
|
| 238 |
+
return 0, err
|
| 239 |
+
}
|
| 240 |
+
now := time.Now()
|
| 241 |
+
startDate := fmt.Sprintf("%s-01", now.Format("2006-01"))
|
| 242 |
+
endDate := now.Format("2006-01-02")
|
| 243 |
+
if !subscription.HasPaymentMethod {
|
| 244 |
+
startDate = now.AddDate(0, 0, -100).Format("2006-01-02")
|
| 245 |
+
}
|
| 246 |
+
url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate)
|
| 247 |
+
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
| 248 |
+
if err != nil {
|
| 249 |
+
return 0, err
|
| 250 |
+
}
|
| 251 |
+
usage := OpenAIUsageResponse{}
|
| 252 |
+
err = json.Unmarshal(body, &usage)
|
| 253 |
+
if err != nil {
|
| 254 |
+
return 0, err
|
| 255 |
+
}
|
| 256 |
+
balance := subscription.HardLimitUSD - usage.TotalUsage/100
|
| 257 |
+
channel.UpdateBalance(balance)
|
| 258 |
+
return balance, nil
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
func UpdateChannelBalance(c *gin.Context) {
|
| 262 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 263 |
+
if err != nil {
|
| 264 |
+
c.JSON(http.StatusOK, gin.H{
|
| 265 |
+
"success": false,
|
| 266 |
+
"message": err.Error(),
|
| 267 |
+
})
|
| 268 |
+
return
|
| 269 |
+
}
|
| 270 |
+
channel, err := model.GetChannelById(id, true)
|
| 271 |
+
if err != nil {
|
| 272 |
+
c.JSON(http.StatusOK, gin.H{
|
| 273 |
+
"success": false,
|
| 274 |
+
"message": err.Error(),
|
| 275 |
+
})
|
| 276 |
+
return
|
| 277 |
+
}
|
| 278 |
+
balance, err := updateChannelBalance(channel)
|
| 279 |
+
if err != nil {
|
| 280 |
+
c.JSON(http.StatusOK, gin.H{
|
| 281 |
+
"success": false,
|
| 282 |
+
"message": err.Error(),
|
| 283 |
+
})
|
| 284 |
+
return
|
| 285 |
+
}
|
| 286 |
+
c.JSON(http.StatusOK, gin.H{
|
| 287 |
+
"success": true,
|
| 288 |
+
"message": "",
|
| 289 |
+
"balance": balance,
|
| 290 |
+
})
|
| 291 |
+
return
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
func updateAllChannelsBalance() error {
|
| 295 |
+
channels, err := model.GetAllChannels(0, 0, true)
|
| 296 |
+
if err != nil {
|
| 297 |
+
return err
|
| 298 |
+
}
|
| 299 |
+
for _, channel := range channels {
|
| 300 |
+
if channel.Status != common.ChannelStatusEnabled {
|
| 301 |
+
continue
|
| 302 |
+
}
|
| 303 |
+
// TODO: support Azure
|
| 304 |
+
if channel.Type != common.ChannelTypeOpenAI && channel.Type != common.ChannelTypeCustom {
|
| 305 |
+
continue
|
| 306 |
+
}
|
| 307 |
+
balance, err := updateChannelBalance(channel)
|
| 308 |
+
if err != nil {
|
| 309 |
+
continue
|
| 310 |
+
} else {
|
| 311 |
+
// err is nil & balance <= 0 means quota is used up
|
| 312 |
+
if balance <= 0 {
|
| 313 |
+
disableChannel(channel.Id, channel.Name, "余额不足")
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
time.Sleep(common.RequestInterval)
|
| 317 |
+
}
|
| 318 |
+
return nil
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
func UpdateAllChannelsBalance(c *gin.Context) {
|
| 322 |
+
// TODO: make it async
|
| 323 |
+
err := updateAllChannelsBalance()
|
| 324 |
+
if err != nil {
|
| 325 |
+
c.JSON(http.StatusOK, gin.H{
|
| 326 |
+
"success": false,
|
| 327 |
+
"message": err.Error(),
|
| 328 |
+
})
|
| 329 |
+
return
|
| 330 |
+
}
|
| 331 |
+
c.JSON(http.StatusOK, gin.H{
|
| 332 |
+
"success": true,
|
| 333 |
+
"message": "",
|
| 334 |
+
})
|
| 335 |
+
return
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
func AutomaticallyUpdateChannels(frequency int) {
|
| 339 |
+
for {
|
| 340 |
+
time.Sleep(time.Duration(frequency) * time.Minute)
|
| 341 |
+
common.SysLog("updating all channels")
|
| 342 |
+
_ = updateAllChannelsBalance()
|
| 343 |
+
common.SysLog("channels update done")
|
| 344 |
+
}
|
| 345 |
+
}
|
controller/channel-test.go
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
"net/http"
|
| 10 |
+
"one-api/common"
|
| 11 |
+
"one-api/model"
|
| 12 |
+
"strconv"
|
| 13 |
+
"sync"
|
| 14 |
+
"time"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIError) {
|
| 18 |
+
switch channel.Type {
|
| 19 |
+
case common.ChannelTypePaLM:
|
| 20 |
+
fallthrough
|
| 21 |
+
case common.ChannelTypeAnthropic:
|
| 22 |
+
fallthrough
|
| 23 |
+
case common.ChannelTypeBaidu:
|
| 24 |
+
fallthrough
|
| 25 |
+
case common.ChannelTypeZhipu:
|
| 26 |
+
fallthrough
|
| 27 |
+
case common.ChannelTypeAli:
|
| 28 |
+
fallthrough
|
| 29 |
+
case common.ChannelType360:
|
| 30 |
+
fallthrough
|
| 31 |
+
case common.ChannelTypeXunfei:
|
| 32 |
+
return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil
|
| 33 |
+
case common.ChannelTypeAzure:
|
| 34 |
+
request.Model = "gpt-35-turbo"
|
| 35 |
+
default:
|
| 36 |
+
request.Model = "gpt-3.5-turbo"
|
| 37 |
+
}
|
| 38 |
+
requestURL := common.ChannelBaseURLs[channel.Type]
|
| 39 |
+
if channel.Type == common.ChannelTypeAzure {
|
| 40 |
+
requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model)
|
| 41 |
+
} else {
|
| 42 |
+
if channel.BaseURL != "" {
|
| 43 |
+
requestURL = channel.BaseURL
|
| 44 |
+
}
|
| 45 |
+
requestURL += "/v1/chat/completions"
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
jsonData, err := json.Marshal(request)
|
| 49 |
+
if err != nil {
|
| 50 |
+
return err, nil
|
| 51 |
+
}
|
| 52 |
+
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
|
| 53 |
+
if err != nil {
|
| 54 |
+
return err, nil
|
| 55 |
+
}
|
| 56 |
+
if channel.Type == common.ChannelTypeAzure {
|
| 57 |
+
req.Header.Set("api-key", channel.Key)
|
| 58 |
+
} else {
|
| 59 |
+
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
| 60 |
+
}
|
| 61 |
+
req.Header.Set("Content-Type", "application/json")
|
| 62 |
+
resp, err := httpClient.Do(req)
|
| 63 |
+
if err != nil {
|
| 64 |
+
return err, nil
|
| 65 |
+
}
|
| 66 |
+
defer resp.Body.Close()
|
| 67 |
+
var response TextResponse
|
| 68 |
+
err = json.NewDecoder(resp.Body).Decode(&response)
|
| 69 |
+
if err != nil {
|
| 70 |
+
return err, nil
|
| 71 |
+
}
|
| 72 |
+
if response.Usage.CompletionTokens == 0 {
|
| 73 |
+
return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error
|
| 74 |
+
}
|
| 75 |
+
return nil, nil
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
func buildTestRequest() *ChatRequest {
|
| 79 |
+
testRequest := &ChatRequest{
|
| 80 |
+
Model: "", // this will be set later
|
| 81 |
+
MaxTokens: 1,
|
| 82 |
+
}
|
| 83 |
+
testMessage := Message{
|
| 84 |
+
Role: "user",
|
| 85 |
+
Content: "hi",
|
| 86 |
+
}
|
| 87 |
+
testRequest.Messages = append(testRequest.Messages, testMessage)
|
| 88 |
+
return testRequest
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func TestChannel(c *gin.Context) {
|
| 92 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 93 |
+
if err != nil {
|
| 94 |
+
c.JSON(http.StatusOK, gin.H{
|
| 95 |
+
"success": false,
|
| 96 |
+
"message": err.Error(),
|
| 97 |
+
})
|
| 98 |
+
return
|
| 99 |
+
}
|
| 100 |
+
channel, err := model.GetChannelById(id, true)
|
| 101 |
+
if err != nil {
|
| 102 |
+
c.JSON(http.StatusOK, gin.H{
|
| 103 |
+
"success": false,
|
| 104 |
+
"message": err.Error(),
|
| 105 |
+
})
|
| 106 |
+
return
|
| 107 |
+
}
|
| 108 |
+
testRequest := buildTestRequest()
|
| 109 |
+
tik := time.Now()
|
| 110 |
+
err, _ = testChannel(channel, *testRequest)
|
| 111 |
+
tok := time.Now()
|
| 112 |
+
milliseconds := tok.Sub(tik).Milliseconds()
|
| 113 |
+
go channel.UpdateResponseTime(milliseconds)
|
| 114 |
+
consumedTime := float64(milliseconds) / 1000.0
|
| 115 |
+
if err != nil {
|
| 116 |
+
c.JSON(http.StatusOK, gin.H{
|
| 117 |
+
"success": false,
|
| 118 |
+
"message": err.Error(),
|
| 119 |
+
"time": consumedTime,
|
| 120 |
+
})
|
| 121 |
+
return
|
| 122 |
+
}
|
| 123 |
+
c.JSON(http.StatusOK, gin.H{
|
| 124 |
+
"success": true,
|
| 125 |
+
"message": "",
|
| 126 |
+
"time": consumedTime,
|
| 127 |
+
})
|
| 128 |
+
return
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
var testAllChannelsLock sync.Mutex
|
| 132 |
+
var testAllChannelsRunning bool = false
|
| 133 |
+
|
| 134 |
+
// disable & notify
|
| 135 |
+
func disableChannel(channelId int, channelName string, reason string) {
|
| 136 |
+
if common.RootUserEmail == "" {
|
| 137 |
+
common.RootUserEmail = model.GetRootUserEmail()
|
| 138 |
+
}
|
| 139 |
+
model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled)
|
| 140 |
+
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
|
| 141 |
+
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
|
| 142 |
+
err := common.SendEmail(subject, common.RootUserEmail, content)
|
| 143 |
+
if err != nil {
|
| 144 |
+
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
func testAllChannels(notify bool) error {
|
| 149 |
+
if common.RootUserEmail == "" {
|
| 150 |
+
common.RootUserEmail = model.GetRootUserEmail()
|
| 151 |
+
}
|
| 152 |
+
testAllChannelsLock.Lock()
|
| 153 |
+
if testAllChannelsRunning {
|
| 154 |
+
testAllChannelsLock.Unlock()
|
| 155 |
+
return errors.New("测试已在运行中")
|
| 156 |
+
}
|
| 157 |
+
testAllChannelsRunning = true
|
| 158 |
+
testAllChannelsLock.Unlock()
|
| 159 |
+
channels, err := model.GetAllChannels(0, 0, true)
|
| 160 |
+
if err != nil {
|
| 161 |
+
return err
|
| 162 |
+
}
|
| 163 |
+
testRequest := buildTestRequest()
|
| 164 |
+
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
| 165 |
+
if disableThreshold == 0 {
|
| 166 |
+
disableThreshold = 10000000 // a impossible value
|
| 167 |
+
}
|
| 168 |
+
go func() {
|
| 169 |
+
for _, channel := range channels {
|
| 170 |
+
if channel.Status != common.ChannelStatusEnabled {
|
| 171 |
+
continue
|
| 172 |
+
}
|
| 173 |
+
tik := time.Now()
|
| 174 |
+
err, openaiErr := testChannel(channel, *testRequest)
|
| 175 |
+
tok := time.Now()
|
| 176 |
+
milliseconds := tok.Sub(tik).Milliseconds()
|
| 177 |
+
if milliseconds > disableThreshold {
|
| 178 |
+
err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
|
| 179 |
+
disableChannel(channel.Id, channel.Name, err.Error())
|
| 180 |
+
}
|
| 181 |
+
if shouldDisableChannel(openaiErr, -1) {
|
| 182 |
+
disableChannel(channel.Id, channel.Name, err.Error())
|
| 183 |
+
}
|
| 184 |
+
channel.UpdateResponseTime(milliseconds)
|
| 185 |
+
time.Sleep(common.RequestInterval)
|
| 186 |
+
}
|
| 187 |
+
testAllChannelsLock.Lock()
|
| 188 |
+
testAllChannelsRunning = false
|
| 189 |
+
testAllChannelsLock.Unlock()
|
| 190 |
+
if notify {
|
| 191 |
+
err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
|
| 192 |
+
if err != nil {
|
| 193 |
+
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}()
|
| 197 |
+
return nil
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
func TestAllChannels(c *gin.Context) {
|
| 201 |
+
err := testAllChannels(true)
|
| 202 |
+
if err != nil {
|
| 203 |
+
c.JSON(http.StatusOK, gin.H{
|
| 204 |
+
"success": false,
|
| 205 |
+
"message": err.Error(),
|
| 206 |
+
})
|
| 207 |
+
return
|
| 208 |
+
}
|
| 209 |
+
c.JSON(http.StatusOK, gin.H{
|
| 210 |
+
"success": true,
|
| 211 |
+
"message": "",
|
| 212 |
+
})
|
| 213 |
+
return
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
func AutomaticallyTestChannels(frequency int) {
|
| 217 |
+
for {
|
| 218 |
+
time.Sleep(time.Duration(frequency) * time.Minute)
|
| 219 |
+
common.SysLog("testing all channels")
|
| 220 |
+
_ = testAllChannels(false)
|
| 221 |
+
common.SysLog("channel test finished")
|
| 222 |
+
}
|
| 223 |
+
}
|
controller/channel.go
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"net/http"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
"one-api/model"
|
| 8 |
+
"strconv"
|
| 9 |
+
"strings"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func GetAllChannels(c *gin.Context) {
|
| 13 |
+
p, _ := strconv.Atoi(c.Query("p"))
|
| 14 |
+
if p < 0 {
|
| 15 |
+
p = 0
|
| 16 |
+
}
|
| 17 |
+
channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage, false)
|
| 18 |
+
if err != nil {
|
| 19 |
+
c.JSON(http.StatusOK, gin.H{
|
| 20 |
+
"success": false,
|
| 21 |
+
"message": err.Error(),
|
| 22 |
+
})
|
| 23 |
+
return
|
| 24 |
+
}
|
| 25 |
+
c.JSON(http.StatusOK, gin.H{
|
| 26 |
+
"success": true,
|
| 27 |
+
"message": "",
|
| 28 |
+
"data": channels,
|
| 29 |
+
})
|
| 30 |
+
return
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func SearchChannels(c *gin.Context) {
|
| 34 |
+
keyword := c.Query("keyword")
|
| 35 |
+
channels, err := model.SearchChannels(keyword)
|
| 36 |
+
if err != nil {
|
| 37 |
+
c.JSON(http.StatusOK, gin.H{
|
| 38 |
+
"success": false,
|
| 39 |
+
"message": err.Error(),
|
| 40 |
+
})
|
| 41 |
+
return
|
| 42 |
+
}
|
| 43 |
+
c.JSON(http.StatusOK, gin.H{
|
| 44 |
+
"success": true,
|
| 45 |
+
"message": "",
|
| 46 |
+
"data": channels,
|
| 47 |
+
})
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func GetChannel(c *gin.Context) {
|
| 52 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 53 |
+
if err != nil {
|
| 54 |
+
c.JSON(http.StatusOK, gin.H{
|
| 55 |
+
"success": false,
|
| 56 |
+
"message": err.Error(),
|
| 57 |
+
})
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
channel, err := model.GetChannelById(id, false)
|
| 61 |
+
if err != nil {
|
| 62 |
+
c.JSON(http.StatusOK, gin.H{
|
| 63 |
+
"success": false,
|
| 64 |
+
"message": err.Error(),
|
| 65 |
+
})
|
| 66 |
+
return
|
| 67 |
+
}
|
| 68 |
+
c.JSON(http.StatusOK, gin.H{
|
| 69 |
+
"success": true,
|
| 70 |
+
"message": "",
|
| 71 |
+
"data": channel,
|
| 72 |
+
})
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
func AddChannel(c *gin.Context) {
|
| 77 |
+
channel := model.Channel{}
|
| 78 |
+
err := c.ShouldBindJSON(&channel)
|
| 79 |
+
if err != nil {
|
| 80 |
+
c.JSON(http.StatusOK, gin.H{
|
| 81 |
+
"success": false,
|
| 82 |
+
"message": err.Error(),
|
| 83 |
+
})
|
| 84 |
+
return
|
| 85 |
+
}
|
| 86 |
+
channel.CreatedTime = common.GetTimestamp()
|
| 87 |
+
keys := strings.Split(channel.Key, "\n")
|
| 88 |
+
channels := make([]model.Channel, 0)
|
| 89 |
+
for _, key := range keys {
|
| 90 |
+
if key == "" {
|
| 91 |
+
continue
|
| 92 |
+
}
|
| 93 |
+
localChannel := channel
|
| 94 |
+
localChannel.Key = key
|
| 95 |
+
channels = append(channels, localChannel)
|
| 96 |
+
}
|
| 97 |
+
err = model.BatchInsertChannels(channels)
|
| 98 |
+
if err != nil {
|
| 99 |
+
c.JSON(http.StatusOK, gin.H{
|
| 100 |
+
"success": false,
|
| 101 |
+
"message": err.Error(),
|
| 102 |
+
})
|
| 103 |
+
return
|
| 104 |
+
}
|
| 105 |
+
c.JSON(http.StatusOK, gin.H{
|
| 106 |
+
"success": true,
|
| 107 |
+
"message": "",
|
| 108 |
+
})
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
func DeleteChannel(c *gin.Context) {
|
| 113 |
+
id, _ := strconv.Atoi(c.Param("id"))
|
| 114 |
+
channel := model.Channel{Id: id}
|
| 115 |
+
err := channel.Delete()
|
| 116 |
+
if err != nil {
|
| 117 |
+
c.JSON(http.StatusOK, gin.H{
|
| 118 |
+
"success": false,
|
| 119 |
+
"message": err.Error(),
|
| 120 |
+
})
|
| 121 |
+
return
|
| 122 |
+
}
|
| 123 |
+
c.JSON(http.StatusOK, gin.H{
|
| 124 |
+
"success": true,
|
| 125 |
+
"message": "",
|
| 126 |
+
})
|
| 127 |
+
return
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
func UpdateChannel(c *gin.Context) {
|
| 131 |
+
channel := model.Channel{}
|
| 132 |
+
err := c.ShouldBindJSON(&channel)
|
| 133 |
+
if err != nil {
|
| 134 |
+
c.JSON(http.StatusOK, gin.H{
|
| 135 |
+
"success": false,
|
| 136 |
+
"message": err.Error(),
|
| 137 |
+
})
|
| 138 |
+
return
|
| 139 |
+
}
|
| 140 |
+
err = channel.Update()
|
| 141 |
+
if err != nil {
|
| 142 |
+
c.JSON(http.StatusOK, gin.H{
|
| 143 |
+
"success": false,
|
| 144 |
+
"message": err.Error(),
|
| 145 |
+
})
|
| 146 |
+
return
|
| 147 |
+
}
|
| 148 |
+
c.JSON(http.StatusOK, gin.H{
|
| 149 |
+
"success": true,
|
| 150 |
+
"message": "",
|
| 151 |
+
"data": channel,
|
| 152 |
+
})
|
| 153 |
+
return
|
| 154 |
+
}
|
controller/github.go
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"github.com/gin-contrib/sessions"
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"net/http"
|
| 11 |
+
"one-api/common"
|
| 12 |
+
"one-api/model"
|
| 13 |
+
"strconv"
|
| 14 |
+
"time"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type GitHubOAuthResponse struct {
|
| 18 |
+
AccessToken string `json:"access_token"`
|
| 19 |
+
Scope string `json:"scope"`
|
| 20 |
+
TokenType string `json:"token_type"`
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
type GitHubUser struct {
|
| 24 |
+
Login string `json:"login"`
|
| 25 |
+
Name string `json:"name"`
|
| 26 |
+
Email string `json:"email"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
|
| 30 |
+
if code == "" {
|
| 31 |
+
return nil, errors.New("无效的参数")
|
| 32 |
+
}
|
| 33 |
+
values := map[string]string{"client_id": common.GitHubClientId, "client_secret": common.GitHubClientSecret, "code": code}
|
| 34 |
+
jsonData, err := json.Marshal(values)
|
| 35 |
+
if err != nil {
|
| 36 |
+
return nil, err
|
| 37 |
+
}
|
| 38 |
+
req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData))
|
| 39 |
+
if err != nil {
|
| 40 |
+
return nil, err
|
| 41 |
+
}
|
| 42 |
+
req.Header.Set("Content-Type", "application/json")
|
| 43 |
+
req.Header.Set("Accept", "application/json")
|
| 44 |
+
client := http.Client{
|
| 45 |
+
Timeout: 5 * time.Second,
|
| 46 |
+
}
|
| 47 |
+
res, err := client.Do(req)
|
| 48 |
+
if err != nil {
|
| 49 |
+
common.SysLog(err.Error())
|
| 50 |
+
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
|
| 51 |
+
}
|
| 52 |
+
defer res.Body.Close()
|
| 53 |
+
var oAuthResponse GitHubOAuthResponse
|
| 54 |
+
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
|
| 55 |
+
if err != nil {
|
| 56 |
+
return nil, err
|
| 57 |
+
}
|
| 58 |
+
req, err = http.NewRequest("GET", "https://api.github.com/user", nil)
|
| 59 |
+
if err != nil {
|
| 60 |
+
return nil, err
|
| 61 |
+
}
|
| 62 |
+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken))
|
| 63 |
+
res2, err := client.Do(req)
|
| 64 |
+
if err != nil {
|
| 65 |
+
common.SysLog(err.Error())
|
| 66 |
+
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
|
| 67 |
+
}
|
| 68 |
+
defer res2.Body.Close()
|
| 69 |
+
var githubUser GitHubUser
|
| 70 |
+
err = json.NewDecoder(res2.Body).Decode(&githubUser)
|
| 71 |
+
if err != nil {
|
| 72 |
+
return nil, err
|
| 73 |
+
}
|
| 74 |
+
if githubUser.Login == "" {
|
| 75 |
+
return nil, errors.New("返回值非法,用户字段为空,请稍后重试!")
|
| 76 |
+
}
|
| 77 |
+
return &githubUser, nil
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
func GitHubOAuth(c *gin.Context) {
|
| 81 |
+
session := sessions.Default(c)
|
| 82 |
+
username := session.Get("username")
|
| 83 |
+
if username != nil {
|
| 84 |
+
GitHubBind(c)
|
| 85 |
+
return
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if !common.GitHubOAuthEnabled {
|
| 89 |
+
c.JSON(http.StatusOK, gin.H{
|
| 90 |
+
"success": false,
|
| 91 |
+
"message": "管理员未开启通过 GitHub 登录以及注册",
|
| 92 |
+
})
|
| 93 |
+
return
|
| 94 |
+
}
|
| 95 |
+
code := c.Query("code")
|
| 96 |
+
githubUser, err := getGitHubUserInfoByCode(code)
|
| 97 |
+
if err != nil {
|
| 98 |
+
c.JSON(http.StatusOK, gin.H{
|
| 99 |
+
"success": false,
|
| 100 |
+
"message": err.Error(),
|
| 101 |
+
})
|
| 102 |
+
return
|
| 103 |
+
}
|
| 104 |
+
user := model.User{
|
| 105 |
+
GitHubId: githubUser.Login,
|
| 106 |
+
}
|
| 107 |
+
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
| 108 |
+
err := user.FillUserByGitHubId()
|
| 109 |
+
if err != nil {
|
| 110 |
+
c.JSON(http.StatusOK, gin.H{
|
| 111 |
+
"success": false,
|
| 112 |
+
"message": err.Error(),
|
| 113 |
+
})
|
| 114 |
+
return
|
| 115 |
+
}
|
| 116 |
+
} else {
|
| 117 |
+
if common.RegisterEnabled {
|
| 118 |
+
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
|
| 119 |
+
if githubUser.Name != "" {
|
| 120 |
+
user.DisplayName = githubUser.Name
|
| 121 |
+
} else {
|
| 122 |
+
user.DisplayName = "GitHub User"
|
| 123 |
+
}
|
| 124 |
+
user.Email = githubUser.Email
|
| 125 |
+
user.Role = common.RoleCommonUser
|
| 126 |
+
user.Status = common.UserStatusEnabled
|
| 127 |
+
|
| 128 |
+
if err := user.Insert(0); err != nil {
|
| 129 |
+
c.JSON(http.StatusOK, gin.H{
|
| 130 |
+
"success": false,
|
| 131 |
+
"message": err.Error(),
|
| 132 |
+
})
|
| 133 |
+
return
|
| 134 |
+
}
|
| 135 |
+
} else {
|
| 136 |
+
c.JSON(http.StatusOK, gin.H{
|
| 137 |
+
"success": false,
|
| 138 |
+
"message": "管理员关闭了新用户注册",
|
| 139 |
+
})
|
| 140 |
+
return
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if user.Status != common.UserStatusEnabled {
|
| 145 |
+
c.JSON(http.StatusOK, gin.H{
|
| 146 |
+
"message": "用户已被封禁",
|
| 147 |
+
"success": false,
|
| 148 |
+
})
|
| 149 |
+
return
|
| 150 |
+
}
|
| 151 |
+
setupLogin(&user, c)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
func GitHubBind(c *gin.Context) {
|
| 155 |
+
if !common.GitHubOAuthEnabled {
|
| 156 |
+
c.JSON(http.StatusOK, gin.H{
|
| 157 |
+
"success": false,
|
| 158 |
+
"message": "管理员未开启通过 GitHub 登录以及注册",
|
| 159 |
+
})
|
| 160 |
+
return
|
| 161 |
+
}
|
| 162 |
+
code := c.Query("code")
|
| 163 |
+
githubUser, err := getGitHubUserInfoByCode(code)
|
| 164 |
+
if err != nil {
|
| 165 |
+
c.JSON(http.StatusOK, gin.H{
|
| 166 |
+
"success": false,
|
| 167 |
+
"message": err.Error(),
|
| 168 |
+
})
|
| 169 |
+
return
|
| 170 |
+
}
|
| 171 |
+
user := model.User{
|
| 172 |
+
GitHubId: githubUser.Login,
|
| 173 |
+
}
|
| 174 |
+
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
| 175 |
+
c.JSON(http.StatusOK, gin.H{
|
| 176 |
+
"success": false,
|
| 177 |
+
"message": "该 GitHub 账户已被绑定",
|
| 178 |
+
})
|
| 179 |
+
return
|
| 180 |
+
}
|
| 181 |
+
session := sessions.Default(c)
|
| 182 |
+
id := session.Get("id")
|
| 183 |
+
// id := c.GetInt("id") // critical bug!
|
| 184 |
+
user.Id = id.(int)
|
| 185 |
+
err = user.FillUserById()
|
| 186 |
+
if err != nil {
|
| 187 |
+
c.JSON(http.StatusOK, gin.H{
|
| 188 |
+
"success": false,
|
| 189 |
+
"message": err.Error(),
|
| 190 |
+
})
|
| 191 |
+
return
|
| 192 |
+
}
|
| 193 |
+
user.GitHubId = githubUser.Login
|
| 194 |
+
err = user.Update(false)
|
| 195 |
+
if err != nil {
|
| 196 |
+
c.JSON(http.StatusOK, gin.H{
|
| 197 |
+
"success": false,
|
| 198 |
+
"message": err.Error(),
|
| 199 |
+
})
|
| 200 |
+
return
|
| 201 |
+
}
|
| 202 |
+
c.JSON(http.StatusOK, gin.H{
|
| 203 |
+
"success": true,
|
| 204 |
+
"message": "bind",
|
| 205 |
+
})
|
| 206 |
+
return
|
| 207 |
+
}
|
controller/group.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"net/http"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func GetGroups(c *gin.Context) {
|
| 10 |
+
groupNames := make([]string, 0)
|
| 11 |
+
for groupName, _ := range common.GroupRatio {
|
| 12 |
+
groupNames = append(groupNames, groupName)
|
| 13 |
+
}
|
| 14 |
+
c.JSON(http.StatusOK, gin.H{
|
| 15 |
+
"success": true,
|
| 16 |
+
"message": "",
|
| 17 |
+
"data": groupNames,
|
| 18 |
+
})
|
| 19 |
+
}
|
controller/log.go
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"one-api/common"
|
| 6 |
+
"one-api/model"
|
| 7 |
+
"strconv"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func GetAllLogs(c *gin.Context) {
|
| 11 |
+
p, _ := strconv.Atoi(c.Query("p"))
|
| 12 |
+
if p < 0 {
|
| 13 |
+
p = 0
|
| 14 |
+
}
|
| 15 |
+
logType, _ := strconv.Atoi(c.Query("type"))
|
| 16 |
+
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
| 17 |
+
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
| 18 |
+
username := c.Query("username")
|
| 19 |
+
tokenName := c.Query("token_name")
|
| 20 |
+
modelName := c.Query("model_name")
|
| 21 |
+
logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*common.ItemsPerPage, common.ItemsPerPage)
|
| 22 |
+
if err != nil {
|
| 23 |
+
c.JSON(200, gin.H{
|
| 24 |
+
"success": false,
|
| 25 |
+
"message": err.Error(),
|
| 26 |
+
})
|
| 27 |
+
return
|
| 28 |
+
}
|
| 29 |
+
c.JSON(200, gin.H{
|
| 30 |
+
"success": true,
|
| 31 |
+
"message": "",
|
| 32 |
+
"data": logs,
|
| 33 |
+
})
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func GetUserLogs(c *gin.Context) {
|
| 37 |
+
p, _ := strconv.Atoi(c.Query("p"))
|
| 38 |
+
if p < 0 {
|
| 39 |
+
p = 0
|
| 40 |
+
}
|
| 41 |
+
userId := c.GetInt("id")
|
| 42 |
+
logType, _ := strconv.Atoi(c.Query("type"))
|
| 43 |
+
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
| 44 |
+
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
| 45 |
+
tokenName := c.Query("token_name")
|
| 46 |
+
modelName := c.Query("model_name")
|
| 47 |
+
logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*common.ItemsPerPage, common.ItemsPerPage)
|
| 48 |
+
if err != nil {
|
| 49 |
+
c.JSON(200, gin.H{
|
| 50 |
+
"success": false,
|
| 51 |
+
"message": err.Error(),
|
| 52 |
+
})
|
| 53 |
+
return
|
| 54 |
+
}
|
| 55 |
+
c.JSON(200, gin.H{
|
| 56 |
+
"success": true,
|
| 57 |
+
"message": "",
|
| 58 |
+
"data": logs,
|
| 59 |
+
})
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func SearchAllLogs(c *gin.Context) {
|
| 63 |
+
keyword := c.Query("keyword")
|
| 64 |
+
logs, err := model.SearchAllLogs(keyword)
|
| 65 |
+
if err != nil {
|
| 66 |
+
c.JSON(200, gin.H{
|
| 67 |
+
"success": false,
|
| 68 |
+
"message": err.Error(),
|
| 69 |
+
})
|
| 70 |
+
return
|
| 71 |
+
}
|
| 72 |
+
c.JSON(200, gin.H{
|
| 73 |
+
"success": true,
|
| 74 |
+
"message": "",
|
| 75 |
+
"data": logs,
|
| 76 |
+
})
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
func SearchUserLogs(c *gin.Context) {
|
| 80 |
+
keyword := c.Query("keyword")
|
| 81 |
+
userId := c.GetInt("id")
|
| 82 |
+
logs, err := model.SearchUserLogs(userId, keyword)
|
| 83 |
+
if err != nil {
|
| 84 |
+
c.JSON(200, gin.H{
|
| 85 |
+
"success": false,
|
| 86 |
+
"message": err.Error(),
|
| 87 |
+
})
|
| 88 |
+
return
|
| 89 |
+
}
|
| 90 |
+
c.JSON(200, gin.H{
|
| 91 |
+
"success": true,
|
| 92 |
+
"message": "",
|
| 93 |
+
"data": logs,
|
| 94 |
+
})
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
func GetLogsStat(c *gin.Context) {
|
| 98 |
+
logType, _ := strconv.Atoi(c.Query("type"))
|
| 99 |
+
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
| 100 |
+
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
| 101 |
+
tokenName := c.Query("token_name")
|
| 102 |
+
username := c.Query("username")
|
| 103 |
+
modelName := c.Query("model_name")
|
| 104 |
+
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
|
| 105 |
+
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "")
|
| 106 |
+
c.JSON(200, gin.H{
|
| 107 |
+
"success": true,
|
| 108 |
+
"message": "",
|
| 109 |
+
"data": gin.H{
|
| 110 |
+
"quota": quotaNum,
|
| 111 |
+
//"token": tokenNum,
|
| 112 |
+
},
|
| 113 |
+
})
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
func GetLogsSelfStat(c *gin.Context) {
|
| 117 |
+
username := c.GetString("username")
|
| 118 |
+
logType, _ := strconv.Atoi(c.Query("type"))
|
| 119 |
+
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
| 120 |
+
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
| 121 |
+
tokenName := c.Query("token_name")
|
| 122 |
+
modelName := c.Query("model_name")
|
| 123 |
+
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
|
| 124 |
+
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
|
| 125 |
+
c.JSON(200, gin.H{
|
| 126 |
+
"success": true,
|
| 127 |
+
"message": "",
|
| 128 |
+
"data": gin.H{
|
| 129 |
+
"quota": quotaNum,
|
| 130 |
+
//"token": tokenNum,
|
| 131 |
+
},
|
| 132 |
+
})
|
| 133 |
+
}
|
controller/misc.go
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"net/http"
|
| 7 |
+
"one-api/common"
|
| 8 |
+
"one-api/model"
|
| 9 |
+
"strings"
|
| 10 |
+
|
| 11 |
+
"github.com/gin-gonic/gin"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func GetStatus(c *gin.Context) {
|
| 15 |
+
c.JSON(http.StatusOK, gin.H{
|
| 16 |
+
"success": true,
|
| 17 |
+
"message": "",
|
| 18 |
+
"data": gin.H{
|
| 19 |
+
"version": common.Version,
|
| 20 |
+
"start_time": common.StartTime,
|
| 21 |
+
"email_verification": common.EmailVerificationEnabled,
|
| 22 |
+
"github_oauth": common.GitHubOAuthEnabled,
|
| 23 |
+
"github_client_id": common.GitHubClientId,
|
| 24 |
+
"system_name": common.SystemName,
|
| 25 |
+
"logo": common.Logo,
|
| 26 |
+
"footer_html": common.Footer,
|
| 27 |
+
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
| 28 |
+
"wechat_login": common.WeChatAuthEnabled,
|
| 29 |
+
"server_address": common.ServerAddress,
|
| 30 |
+
"turnstile_check": common.TurnstileCheckEnabled,
|
| 31 |
+
"turnstile_site_key": common.TurnstileSiteKey,
|
| 32 |
+
"top_up_link": common.TopUpLink,
|
| 33 |
+
"chat_link": common.ChatLink,
|
| 34 |
+
"quota_per_unit": common.QuotaPerUnit,
|
| 35 |
+
"display_in_currency": common.DisplayInCurrencyEnabled,
|
| 36 |
+
},
|
| 37 |
+
})
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func GetNotice(c *gin.Context) {
|
| 42 |
+
common.OptionMapRWMutex.RLock()
|
| 43 |
+
defer common.OptionMapRWMutex.RUnlock()
|
| 44 |
+
c.JSON(http.StatusOK, gin.H{
|
| 45 |
+
"success": true,
|
| 46 |
+
"message": "",
|
| 47 |
+
"data": common.OptionMap["Notice"],
|
| 48 |
+
})
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func GetAbout(c *gin.Context) {
|
| 53 |
+
common.OptionMapRWMutex.RLock()
|
| 54 |
+
defer common.OptionMapRWMutex.RUnlock()
|
| 55 |
+
c.JSON(http.StatusOK, gin.H{
|
| 56 |
+
"success": true,
|
| 57 |
+
"message": "",
|
| 58 |
+
"data": common.OptionMap["About"],
|
| 59 |
+
})
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
func GetHomePageContent(c *gin.Context) {
|
| 64 |
+
common.OptionMapRWMutex.RLock()
|
| 65 |
+
defer common.OptionMapRWMutex.RUnlock()
|
| 66 |
+
c.JSON(http.StatusOK, gin.H{
|
| 67 |
+
"success": true,
|
| 68 |
+
"message": "",
|
| 69 |
+
"data": common.OptionMap["HomePageContent"],
|
| 70 |
+
})
|
| 71 |
+
return
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func SendEmailVerification(c *gin.Context) {
|
| 75 |
+
email := c.Query("email")
|
| 76 |
+
if err := common.Validate.Var(email, "required,email"); err != nil {
|
| 77 |
+
c.JSON(http.StatusOK, gin.H{
|
| 78 |
+
"success": false,
|
| 79 |
+
"message": "无效的参数",
|
| 80 |
+
})
|
| 81 |
+
return
|
| 82 |
+
}
|
| 83 |
+
if common.EmailDomainRestrictionEnabled {
|
| 84 |
+
allowed := false
|
| 85 |
+
for _, domain := range common.EmailDomainWhitelist {
|
| 86 |
+
if strings.HasSuffix(email, "@"+domain) {
|
| 87 |
+
allowed = true
|
| 88 |
+
break
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
if !allowed {
|
| 92 |
+
c.JSON(http.StatusOK, gin.H{
|
| 93 |
+
"success": false,
|
| 94 |
+
"message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中",
|
| 95 |
+
})
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
if model.IsEmailAlreadyTaken(email) {
|
| 100 |
+
c.JSON(http.StatusOK, gin.H{
|
| 101 |
+
"success": false,
|
| 102 |
+
"message": "邮箱地址已被占用",
|
| 103 |
+
})
|
| 104 |
+
return
|
| 105 |
+
}
|
| 106 |
+
code := common.GenerateVerificationCode(6)
|
| 107 |
+
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
|
| 108 |
+
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
|
| 109 |
+
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
| 110 |
+
"<p>您的验证码为: <strong>%s</strong></p>"+
|
| 111 |
+
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
|
| 112 |
+
err := common.SendEmail(subject, email, content)
|
| 113 |
+
if err != nil {
|
| 114 |
+
c.JSON(http.StatusOK, gin.H{
|
| 115 |
+
"success": false,
|
| 116 |
+
"message": err.Error(),
|
| 117 |
+
})
|
| 118 |
+
return
|
| 119 |
+
}
|
| 120 |
+
c.JSON(http.StatusOK, gin.H{
|
| 121 |
+
"success": true,
|
| 122 |
+
"message": "",
|
| 123 |
+
})
|
| 124 |
+
return
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
func SendPasswordResetEmail(c *gin.Context) {
|
| 128 |
+
email := c.Query("email")
|
| 129 |
+
if err := common.Validate.Var(email, "required,email"); err != nil {
|
| 130 |
+
c.JSON(http.StatusOK, gin.H{
|
| 131 |
+
"success": false,
|
| 132 |
+
"message": "无效的参数",
|
| 133 |
+
})
|
| 134 |
+
return
|
| 135 |
+
}
|
| 136 |
+
if !model.IsEmailAlreadyTaken(email) {
|
| 137 |
+
c.JSON(http.StatusOK, gin.H{
|
| 138 |
+
"success": false,
|
| 139 |
+
"message": "该邮箱地址未注册",
|
| 140 |
+
})
|
| 141 |
+
return
|
| 142 |
+
}
|
| 143 |
+
code := common.GenerateVerificationCode(0)
|
| 144 |
+
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
| 145 |
+
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
| 146 |
+
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
| 147 |
+
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
| 148 |
+
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
| 149 |
+
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
| 150 |
+
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
|
| 151 |
+
err := common.SendEmail(subject, email, content)
|
| 152 |
+
if err != nil {
|
| 153 |
+
c.JSON(http.StatusOK, gin.H{
|
| 154 |
+
"success": false,
|
| 155 |
+
"message": err.Error(),
|
| 156 |
+
})
|
| 157 |
+
return
|
| 158 |
+
}
|
| 159 |
+
c.JSON(http.StatusOK, gin.H{
|
| 160 |
+
"success": true,
|
| 161 |
+
"message": "",
|
| 162 |
+
})
|
| 163 |
+
return
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
type PasswordResetRequest struct {
|
| 167 |
+
Email string `json:"email"`
|
| 168 |
+
Token string `json:"token"`
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
func ResetPassword(c *gin.Context) {
|
| 172 |
+
var req PasswordResetRequest
|
| 173 |
+
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
| 174 |
+
if req.Email == "" || req.Token == "" {
|
| 175 |
+
c.JSON(http.StatusOK, gin.H{
|
| 176 |
+
"success": false,
|
| 177 |
+
"message": "无效的参数",
|
| 178 |
+
})
|
| 179 |
+
return
|
| 180 |
+
}
|
| 181 |
+
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) {
|
| 182 |
+
c.JSON(http.StatusOK, gin.H{
|
| 183 |
+
"success": false,
|
| 184 |
+
"message": "重置链接非法或已过期",
|
| 185 |
+
})
|
| 186 |
+
return
|
| 187 |
+
}
|
| 188 |
+
password := common.GenerateVerificationCode(12)
|
| 189 |
+
err = model.ResetUserPasswordByEmail(req.Email, password)
|
| 190 |
+
if err != nil {
|
| 191 |
+
c.JSON(http.StatusOK, gin.H{
|
| 192 |
+
"success": false,
|
| 193 |
+
"message": err.Error(),
|
| 194 |
+
})
|
| 195 |
+
return
|
| 196 |
+
}
|
| 197 |
+
common.DeleteKey(req.Email, common.PasswordResetPurpose)
|
| 198 |
+
c.JSON(http.StatusOK, gin.H{
|
| 199 |
+
"success": true,
|
| 200 |
+
"message": "",
|
| 201 |
+
"data": password,
|
| 202 |
+
})
|
| 203 |
+
return
|
| 204 |
+
}
|
controller/model.go
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
// https://platform.openai.com/docs/api-reference/models/list
|
| 10 |
+
|
| 11 |
+
type OpenAIModelPermission struct {
|
| 12 |
+
Id string `json:"id"`
|
| 13 |
+
Object string `json:"object"`
|
| 14 |
+
Created int `json:"created"`
|
| 15 |
+
AllowCreateEngine bool `json:"allow_create_engine"`
|
| 16 |
+
AllowSampling bool `json:"allow_sampling"`
|
| 17 |
+
AllowLogprobs bool `json:"allow_logprobs"`
|
| 18 |
+
AllowSearchIndices bool `json:"allow_search_indices"`
|
| 19 |
+
AllowView bool `json:"allow_view"`
|
| 20 |
+
AllowFineTuning bool `json:"allow_fine_tuning"`
|
| 21 |
+
Organization string `json:"organization"`
|
| 22 |
+
Group *string `json:"group"`
|
| 23 |
+
IsBlocking bool `json:"is_blocking"`
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type OpenAIModels struct {
|
| 27 |
+
Id string `json:"id"`
|
| 28 |
+
Object string `json:"object"`
|
| 29 |
+
Created int `json:"created"`
|
| 30 |
+
OwnedBy string `json:"owned_by"`
|
| 31 |
+
Permission []OpenAIModelPermission `json:"permission"`
|
| 32 |
+
Root string `json:"root"`
|
| 33 |
+
Parent *string `json:"parent"`
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
var openAIModels []OpenAIModels
|
| 37 |
+
var openAIModelsMap map[string]OpenAIModels
|
| 38 |
+
|
| 39 |
+
func init() {
|
| 40 |
+
var permission []OpenAIModelPermission
|
| 41 |
+
permission = append(permission, OpenAIModelPermission{
|
| 42 |
+
Id: "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
|
| 43 |
+
Object: "model_permission",
|
| 44 |
+
Created: 1626777600,
|
| 45 |
+
AllowCreateEngine: true,
|
| 46 |
+
AllowSampling: true,
|
| 47 |
+
AllowLogprobs: true,
|
| 48 |
+
AllowSearchIndices: false,
|
| 49 |
+
AllowView: true,
|
| 50 |
+
AllowFineTuning: false,
|
| 51 |
+
Organization: "*",
|
| 52 |
+
Group: nil,
|
| 53 |
+
IsBlocking: false,
|
| 54 |
+
})
|
| 55 |
+
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
| 56 |
+
openAIModels = []OpenAIModels{
|
| 57 |
+
{
|
| 58 |
+
Id: "dall-e",
|
| 59 |
+
Object: "model",
|
| 60 |
+
Created: 1677649963,
|
| 61 |
+
OwnedBy: "openai",
|
| 62 |
+
Permission: permission,
|
| 63 |
+
Root: "dall-e",
|
| 64 |
+
Parent: nil,
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
Id: "whisper-1",
|
| 68 |
+
Object: "model",
|
| 69 |
+
Created: 1677649963,
|
| 70 |
+
OwnedBy: "openai",
|
| 71 |
+
Permission: permission,
|
| 72 |
+
Root: "whisper-1",
|
| 73 |
+
Parent: nil,
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
Id: "gpt-3.5-turbo",
|
| 77 |
+
Object: "model",
|
| 78 |
+
Created: 1677649963,
|
| 79 |
+
OwnedBy: "openai",
|
| 80 |
+
Permission: permission,
|
| 81 |
+
Root: "gpt-3.5-turbo",
|
| 82 |
+
Parent: nil,
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
Id: "gpt-3.5-turbo-0301",
|
| 86 |
+
Object: "model",
|
| 87 |
+
Created: 1677649963,
|
| 88 |
+
OwnedBy: "openai",
|
| 89 |
+
Permission: permission,
|
| 90 |
+
Root: "gpt-3.5-turbo-0301",
|
| 91 |
+
Parent: nil,
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
Id: "gpt-3.5-turbo-0613",
|
| 95 |
+
Object: "model",
|
| 96 |
+
Created: 1677649963,
|
| 97 |
+
OwnedBy: "openai",
|
| 98 |
+
Permission: permission,
|
| 99 |
+
Root: "gpt-3.5-turbo-0613",
|
| 100 |
+
Parent: nil,
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
Id: "gpt-3.5-turbo-16k",
|
| 104 |
+
Object: "model",
|
| 105 |
+
Created: 1677649963,
|
| 106 |
+
OwnedBy: "openai",
|
| 107 |
+
Permission: permission,
|
| 108 |
+
Root: "gpt-3.5-turbo-16k",
|
| 109 |
+
Parent: nil,
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
Id: "gpt-3.5-turbo-16k-0613",
|
| 113 |
+
Object: "model",
|
| 114 |
+
Created: 1677649963,
|
| 115 |
+
OwnedBy: "openai",
|
| 116 |
+
Permission: permission,
|
| 117 |
+
Root: "gpt-3.5-turbo-16k-0613",
|
| 118 |
+
Parent: nil,
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
Id: "gpt-4",
|
| 122 |
+
Object: "model",
|
| 123 |
+
Created: 1677649963,
|
| 124 |
+
OwnedBy: "openai",
|
| 125 |
+
Permission: permission,
|
| 126 |
+
Root: "gpt-4",
|
| 127 |
+
Parent: nil,
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
Id: "gpt-4-0314",
|
| 131 |
+
Object: "model",
|
| 132 |
+
Created: 1677649963,
|
| 133 |
+
OwnedBy: "openai",
|
| 134 |
+
Permission: permission,
|
| 135 |
+
Root: "gpt-4-0314",
|
| 136 |
+
Parent: nil,
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
Id: "gpt-4-0613",
|
| 140 |
+
Object: "model",
|
| 141 |
+
Created: 1677649963,
|
| 142 |
+
OwnedBy: "openai",
|
| 143 |
+
Permission: permission,
|
| 144 |
+
Root: "gpt-4-0613",
|
| 145 |
+
Parent: nil,
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
Id: "gpt-4-32k",
|
| 149 |
+
Object: "model",
|
| 150 |
+
Created: 1677649963,
|
| 151 |
+
OwnedBy: "openai",
|
| 152 |
+
Permission: permission,
|
| 153 |
+
Root: "gpt-4-32k",
|
| 154 |
+
Parent: nil,
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
Id: "gpt-4-32k-0314",
|
| 158 |
+
Object: "model",
|
| 159 |
+
Created: 1677649963,
|
| 160 |
+
OwnedBy: "openai",
|
| 161 |
+
Permission: permission,
|
| 162 |
+
Root: "gpt-4-32k-0314",
|
| 163 |
+
Parent: nil,
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
Id: "gpt-4-32k-0613",
|
| 167 |
+
Object: "model",
|
| 168 |
+
Created: 1677649963,
|
| 169 |
+
OwnedBy: "openai",
|
| 170 |
+
Permission: permission,
|
| 171 |
+
Root: "gpt-4-32k-0613",
|
| 172 |
+
Parent: nil,
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
Id: "text-embedding-ada-002",
|
| 176 |
+
Object: "model",
|
| 177 |
+
Created: 1677649963,
|
| 178 |
+
OwnedBy: "openai",
|
| 179 |
+
Permission: permission,
|
| 180 |
+
Root: "text-embedding-ada-002",
|
| 181 |
+
Parent: nil,
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
Id: "text-davinci-003",
|
| 185 |
+
Object: "model",
|
| 186 |
+
Created: 1677649963,
|
| 187 |
+
OwnedBy: "openai",
|
| 188 |
+
Permission: permission,
|
| 189 |
+
Root: "text-davinci-003",
|
| 190 |
+
Parent: nil,
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
Id: "text-davinci-002",
|
| 194 |
+
Object: "model",
|
| 195 |
+
Created: 1677649963,
|
| 196 |
+
OwnedBy: "openai",
|
| 197 |
+
Permission: permission,
|
| 198 |
+
Root: "text-davinci-002",
|
| 199 |
+
Parent: nil,
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
Id: "text-curie-001",
|
| 203 |
+
Object: "model",
|
| 204 |
+
Created: 1677649963,
|
| 205 |
+
OwnedBy: "openai",
|
| 206 |
+
Permission: permission,
|
| 207 |
+
Root: "text-curie-001",
|
| 208 |
+
Parent: nil,
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
Id: "text-babbage-001",
|
| 212 |
+
Object: "model",
|
| 213 |
+
Created: 1677649963,
|
| 214 |
+
OwnedBy: "openai",
|
| 215 |
+
Permission: permission,
|
| 216 |
+
Root: "text-babbage-001",
|
| 217 |
+
Parent: nil,
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
Id: "text-ada-001",
|
| 221 |
+
Object: "model",
|
| 222 |
+
Created: 1677649963,
|
| 223 |
+
OwnedBy: "openai",
|
| 224 |
+
Permission: permission,
|
| 225 |
+
Root: "text-ada-001",
|
| 226 |
+
Parent: nil,
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
Id: "text-moderation-latest",
|
| 230 |
+
Object: "model",
|
| 231 |
+
Created: 1677649963,
|
| 232 |
+
OwnedBy: "openai",
|
| 233 |
+
Permission: permission,
|
| 234 |
+
Root: "text-moderation-latest",
|
| 235 |
+
Parent: nil,
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
Id: "text-moderation-stable",
|
| 239 |
+
Object: "model",
|
| 240 |
+
Created: 1677649963,
|
| 241 |
+
OwnedBy: "openai",
|
| 242 |
+
Permission: permission,
|
| 243 |
+
Root: "text-moderation-stable",
|
| 244 |
+
Parent: nil,
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
Id: "text-davinci-edit-001",
|
| 248 |
+
Object: "model",
|
| 249 |
+
Created: 1677649963,
|
| 250 |
+
OwnedBy: "openai",
|
| 251 |
+
Permission: permission,
|
| 252 |
+
Root: "text-davinci-edit-001",
|
| 253 |
+
Parent: nil,
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
Id: "code-davinci-edit-001",
|
| 257 |
+
Object: "model",
|
| 258 |
+
Created: 1677649963,
|
| 259 |
+
OwnedBy: "openai",
|
| 260 |
+
Permission: permission,
|
| 261 |
+
Root: "code-davinci-edit-001",
|
| 262 |
+
Parent: nil,
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
Id: "claude-instant-1",
|
| 266 |
+
Object: "model",
|
| 267 |
+
Created: 1677649963,
|
| 268 |
+
OwnedBy: "anturopic",
|
| 269 |
+
Permission: permission,
|
| 270 |
+
Root: "claude-instant-1",
|
| 271 |
+
Parent: nil,
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
Id: "claude-2",
|
| 275 |
+
Object: "model",
|
| 276 |
+
Created: 1677649963,
|
| 277 |
+
OwnedBy: "anturopic",
|
| 278 |
+
Permission: permission,
|
| 279 |
+
Root: "claude-2",
|
| 280 |
+
Parent: nil,
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
Id: "ERNIE-Bot",
|
| 284 |
+
Object: "model",
|
| 285 |
+
Created: 1677649963,
|
| 286 |
+
OwnedBy: "baidu",
|
| 287 |
+
Permission: permission,
|
| 288 |
+
Root: "ERNIE-Bot",
|
| 289 |
+
Parent: nil,
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
Id: "ERNIE-Bot-turbo",
|
| 293 |
+
Object: "model",
|
| 294 |
+
Created: 1677649963,
|
| 295 |
+
OwnedBy: "baidu",
|
| 296 |
+
Permission: permission,
|
| 297 |
+
Root: "ERNIE-Bot-turbo",
|
| 298 |
+
Parent: nil,
|
| 299 |
+
},
|
| 300 |
+
{
|
| 301 |
+
Id: "Embedding-V1",
|
| 302 |
+
Object: "model",
|
| 303 |
+
Created: 1677649963,
|
| 304 |
+
OwnedBy: "baidu",
|
| 305 |
+
Permission: permission,
|
| 306 |
+
Root: "Embedding-V1",
|
| 307 |
+
Parent: nil,
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
Id: "PaLM-2",
|
| 311 |
+
Object: "model",
|
| 312 |
+
Created: 1677649963,
|
| 313 |
+
OwnedBy: "google",
|
| 314 |
+
Permission: permission,
|
| 315 |
+
Root: "PaLM-2",
|
| 316 |
+
Parent: nil,
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
Id: "chatglm_pro",
|
| 320 |
+
Object: "model",
|
| 321 |
+
Created: 1677649963,
|
| 322 |
+
OwnedBy: "zhipu",
|
| 323 |
+
Permission: permission,
|
| 324 |
+
Root: "chatglm_pro",
|
| 325 |
+
Parent: nil,
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
Id: "chatglm_std",
|
| 329 |
+
Object: "model",
|
| 330 |
+
Created: 1677649963,
|
| 331 |
+
OwnedBy: "zhipu",
|
| 332 |
+
Permission: permission,
|
| 333 |
+
Root: "chatglm_std",
|
| 334 |
+
Parent: nil,
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
Id: "chatglm_lite",
|
| 338 |
+
Object: "model",
|
| 339 |
+
Created: 1677649963,
|
| 340 |
+
OwnedBy: "zhipu",
|
| 341 |
+
Permission: permission,
|
| 342 |
+
Root: "chatglm_lite",
|
| 343 |
+
Parent: nil,
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
Id: "qwen-v1",
|
| 347 |
+
Object: "model",
|
| 348 |
+
Created: 1677649963,
|
| 349 |
+
OwnedBy: "ali",
|
| 350 |
+
Permission: permission,
|
| 351 |
+
Root: "qwen-v1",
|
| 352 |
+
Parent: nil,
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
Id: "qwen-plus-v1",
|
| 356 |
+
Object: "model",
|
| 357 |
+
Created: 1677649963,
|
| 358 |
+
OwnedBy: "ali",
|
| 359 |
+
Permission: permission,
|
| 360 |
+
Root: "qwen-plus-v1",
|
| 361 |
+
Parent: nil,
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
Id: "SparkDesk",
|
| 365 |
+
Object: "model",
|
| 366 |
+
Created: 1677649963,
|
| 367 |
+
OwnedBy: "xunfei",
|
| 368 |
+
Permission: permission,
|
| 369 |
+
Root: "SparkDesk",
|
| 370 |
+
Parent: nil,
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
Id: "360GPT_S2_V9",
|
| 374 |
+
Object: "model",
|
| 375 |
+
Created: 1677649963,
|
| 376 |
+
OwnedBy: "360",
|
| 377 |
+
Permission: permission,
|
| 378 |
+
Root: "360GPT_S2_V9",
|
| 379 |
+
Parent: nil,
|
| 380 |
+
},
|
| 381 |
+
{
|
| 382 |
+
Id: "embedding-bert-512-v1",
|
| 383 |
+
Object: "model",
|
| 384 |
+
Created: 1677649963,
|
| 385 |
+
OwnedBy: "360",
|
| 386 |
+
Permission: permission,
|
| 387 |
+
Root: "embedding-bert-512-v1",
|
| 388 |
+
Parent: nil,
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
Id: "embedding_s1_v1",
|
| 392 |
+
Object: "model",
|
| 393 |
+
Created: 1677649963,
|
| 394 |
+
OwnedBy: "360",
|
| 395 |
+
Permission: permission,
|
| 396 |
+
Root: "embedding_s1_v1",
|
| 397 |
+
Parent: nil,
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
Id: "semantic_similarity_s1_v1",
|
| 401 |
+
Object: "model",
|
| 402 |
+
Created: 1677649963,
|
| 403 |
+
OwnedBy: "360",
|
| 404 |
+
Permission: permission,
|
| 405 |
+
Root: "semantic_similarity_s1_v1",
|
| 406 |
+
Parent: nil,
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
Id: "360GPT_S2_V9.4",
|
| 410 |
+
Object: "model",
|
| 411 |
+
Created: 1677649963,
|
| 412 |
+
OwnedBy: "360",
|
| 413 |
+
Permission: permission,
|
| 414 |
+
Root: "360GPT_S2_V9.4",
|
| 415 |
+
Parent: nil,
|
| 416 |
+
},
|
| 417 |
+
}
|
| 418 |
+
openAIModelsMap = make(map[string]OpenAIModels)
|
| 419 |
+
for _, model := range openAIModels {
|
| 420 |
+
openAIModelsMap[model.Id] = model
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
func ListModels(c *gin.Context) {
|
| 425 |
+
c.JSON(200, gin.H{
|
| 426 |
+
"object": "list",
|
| 427 |
+
"data": openAIModels,
|
| 428 |
+
})
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
func RetrieveModel(c *gin.Context) {
|
| 432 |
+
modelId := c.Param("model")
|
| 433 |
+
if model, ok := openAIModelsMap[modelId]; ok {
|
| 434 |
+
c.JSON(200, model)
|
| 435 |
+
} else {
|
| 436 |
+
openAIError := OpenAIError{
|
| 437 |
+
Message: fmt.Sprintf("The model '%s' does not exist", modelId),
|
| 438 |
+
Type: "invalid_request_error",
|
| 439 |
+
Param: "model",
|
| 440 |
+
Code: "model_not_found",
|
| 441 |
+
}
|
| 442 |
+
c.JSON(200, gin.H{
|
| 443 |
+
"error": openAIError,
|
| 444 |
+
})
|
| 445 |
+
}
|
| 446 |
+
}
|
controller/option.go
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
"one-api/model"
|
| 8 |
+
"strings"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func GetOptions(c *gin.Context) {
|
| 14 |
+
var options []*model.Option
|
| 15 |
+
common.OptionMapRWMutex.Lock()
|
| 16 |
+
for k, v := range common.OptionMap {
|
| 17 |
+
if strings.HasSuffix(k, "Token") || strings.HasSuffix(k, "Secret") {
|
| 18 |
+
continue
|
| 19 |
+
}
|
| 20 |
+
options = append(options, &model.Option{
|
| 21 |
+
Key: k,
|
| 22 |
+
Value: common.Interface2String(v),
|
| 23 |
+
})
|
| 24 |
+
}
|
| 25 |
+
common.OptionMapRWMutex.Unlock()
|
| 26 |
+
c.JSON(http.StatusOK, gin.H{
|
| 27 |
+
"success": true,
|
| 28 |
+
"message": "",
|
| 29 |
+
"data": options,
|
| 30 |
+
})
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func UpdateOption(c *gin.Context) {
|
| 35 |
+
var option model.Option
|
| 36 |
+
err := json.NewDecoder(c.Request.Body).Decode(&option)
|
| 37 |
+
if err != nil {
|
| 38 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 39 |
+
"success": false,
|
| 40 |
+
"message": "无效的参数",
|
| 41 |
+
})
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
switch option.Key {
|
| 45 |
+
case "GitHubOAuthEnabled":
|
| 46 |
+
if option.Value == "true" && common.GitHubClientId == "" {
|
| 47 |
+
c.JSON(http.StatusOK, gin.H{
|
| 48 |
+
"success": false,
|
| 49 |
+
"message": "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!",
|
| 50 |
+
})
|
| 51 |
+
return
|
| 52 |
+
}
|
| 53 |
+
case "EmailDomainRestrictionEnabled":
|
| 54 |
+
if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 {
|
| 55 |
+
c.JSON(http.StatusOK, gin.H{
|
| 56 |
+
"success": false,
|
| 57 |
+
"message": "无法启用邮箱域名限制,请先填入限制的邮箱域名!",
|
| 58 |
+
})
|
| 59 |
+
return
|
| 60 |
+
}
|
| 61 |
+
case "WeChatAuthEnabled":
|
| 62 |
+
if option.Value == "true" && common.WeChatServerAddress == "" {
|
| 63 |
+
c.JSON(http.StatusOK, gin.H{
|
| 64 |
+
"success": false,
|
| 65 |
+
"message": "无法启用微信登录,请先填入微信登录相关配置信息!",
|
| 66 |
+
})
|
| 67 |
+
return
|
| 68 |
+
}
|
| 69 |
+
case "TurnstileCheckEnabled":
|
| 70 |
+
if option.Value == "true" && common.TurnstileSiteKey == "" {
|
| 71 |
+
c.JSON(http.StatusOK, gin.H{
|
| 72 |
+
"success": false,
|
| 73 |
+
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
| 74 |
+
})
|
| 75 |
+
return
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
err = model.UpdateOption(option.Key, option.Value)
|
| 79 |
+
if err != nil {
|
| 80 |
+
c.JSON(http.StatusOK, gin.H{
|
| 81 |
+
"success": false,
|
| 82 |
+
"message": err.Error(),
|
| 83 |
+
})
|
| 84 |
+
return
|
| 85 |
+
}
|
| 86 |
+
c.JSON(http.StatusOK, gin.H{
|
| 87 |
+
"success": true,
|
| 88 |
+
"message": "",
|
| 89 |
+
})
|
| 90 |
+
return
|
| 91 |
+
}
|
controller/redemption.go
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"net/http"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
"one-api/model"
|
| 8 |
+
"strconv"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func GetAllRedemptions(c *gin.Context) {
|
| 12 |
+
p, _ := strconv.Atoi(c.Query("p"))
|
| 13 |
+
if p < 0 {
|
| 14 |
+
p = 0
|
| 15 |
+
}
|
| 16 |
+
redemptions, err := model.GetAllRedemptions(p*common.ItemsPerPage, common.ItemsPerPage)
|
| 17 |
+
if err != nil {
|
| 18 |
+
c.JSON(http.StatusOK, gin.H{
|
| 19 |
+
"success": false,
|
| 20 |
+
"message": err.Error(),
|
| 21 |
+
})
|
| 22 |
+
return
|
| 23 |
+
}
|
| 24 |
+
c.JSON(http.StatusOK, gin.H{
|
| 25 |
+
"success": true,
|
| 26 |
+
"message": "",
|
| 27 |
+
"data": redemptions,
|
| 28 |
+
})
|
| 29 |
+
return
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func SearchRedemptions(c *gin.Context) {
|
| 33 |
+
keyword := c.Query("keyword")
|
| 34 |
+
redemptions, err := model.SearchRedemptions(keyword)
|
| 35 |
+
if err != nil {
|
| 36 |
+
c.JSON(http.StatusOK, gin.H{
|
| 37 |
+
"success": false,
|
| 38 |
+
"message": err.Error(),
|
| 39 |
+
})
|
| 40 |
+
return
|
| 41 |
+
}
|
| 42 |
+
c.JSON(http.StatusOK, gin.H{
|
| 43 |
+
"success": true,
|
| 44 |
+
"message": "",
|
| 45 |
+
"data": redemptions,
|
| 46 |
+
})
|
| 47 |
+
return
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func GetRedemption(c *gin.Context) {
|
| 51 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 52 |
+
if err != nil {
|
| 53 |
+
c.JSON(http.StatusOK, gin.H{
|
| 54 |
+
"success": false,
|
| 55 |
+
"message": err.Error(),
|
| 56 |
+
})
|
| 57 |
+
return
|
| 58 |
+
}
|
| 59 |
+
redemption, err := model.GetRedemptionById(id)
|
| 60 |
+
if err != nil {
|
| 61 |
+
c.JSON(http.StatusOK, gin.H{
|
| 62 |
+
"success": false,
|
| 63 |
+
"message": err.Error(),
|
| 64 |
+
})
|
| 65 |
+
return
|
| 66 |
+
}
|
| 67 |
+
c.JSON(http.StatusOK, gin.H{
|
| 68 |
+
"success": true,
|
| 69 |
+
"message": "",
|
| 70 |
+
"data": redemption,
|
| 71 |
+
})
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
func AddRedemption(c *gin.Context) {
|
| 76 |
+
redemption := model.Redemption{}
|
| 77 |
+
err := c.ShouldBindJSON(&redemption)
|
| 78 |
+
if err != nil {
|
| 79 |
+
c.JSON(http.StatusOK, gin.H{
|
| 80 |
+
"success": false,
|
| 81 |
+
"message": err.Error(),
|
| 82 |
+
})
|
| 83 |
+
return
|
| 84 |
+
}
|
| 85 |
+
if len(redemption.Name) == 0 || len(redemption.Name) > 20 {
|
| 86 |
+
c.JSON(http.StatusOK, gin.H{
|
| 87 |
+
"success": false,
|
| 88 |
+
"message": "兑换码名称长度必须在1-20之间",
|
| 89 |
+
})
|
| 90 |
+
return
|
| 91 |
+
}
|
| 92 |
+
if redemption.Count <= 0 {
|
| 93 |
+
c.JSON(http.StatusOK, gin.H{
|
| 94 |
+
"success": false,
|
| 95 |
+
"message": "兑换码个数必须大于0",
|
| 96 |
+
})
|
| 97 |
+
return
|
| 98 |
+
}
|
| 99 |
+
if redemption.Count > 100 {
|
| 100 |
+
c.JSON(http.StatusOK, gin.H{
|
| 101 |
+
"success": false,
|
| 102 |
+
"message": "一次兑换码批量生成的个数不能大于 100",
|
| 103 |
+
})
|
| 104 |
+
return
|
| 105 |
+
}
|
| 106 |
+
var keys []string
|
| 107 |
+
for i := 0; i < redemption.Count; i++ {
|
| 108 |
+
key := common.GetUUID()
|
| 109 |
+
cleanRedemption := model.Redemption{
|
| 110 |
+
UserId: c.GetInt("id"),
|
| 111 |
+
Name: redemption.Name,
|
| 112 |
+
Key: key,
|
| 113 |
+
CreatedTime: common.GetTimestamp(),
|
| 114 |
+
Quota: redemption.Quota,
|
| 115 |
+
}
|
| 116 |
+
err = cleanRedemption.Insert()
|
| 117 |
+
if err != nil {
|
| 118 |
+
c.JSON(http.StatusOK, gin.H{
|
| 119 |
+
"success": false,
|
| 120 |
+
"message": err.Error(),
|
| 121 |
+
"data": keys,
|
| 122 |
+
})
|
| 123 |
+
return
|
| 124 |
+
}
|
| 125 |
+
keys = append(keys, key)
|
| 126 |
+
}
|
| 127 |
+
c.JSON(http.StatusOK, gin.H{
|
| 128 |
+
"success": true,
|
| 129 |
+
"message": "",
|
| 130 |
+
"data": keys,
|
| 131 |
+
})
|
| 132 |
+
return
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
func DeleteRedemption(c *gin.Context) {
|
| 136 |
+
id, _ := strconv.Atoi(c.Param("id"))
|
| 137 |
+
err := model.DeleteRedemptionById(id)
|
| 138 |
+
if err != nil {
|
| 139 |
+
c.JSON(http.StatusOK, gin.H{
|
| 140 |
+
"success": false,
|
| 141 |
+
"message": err.Error(),
|
| 142 |
+
})
|
| 143 |
+
return
|
| 144 |
+
}
|
| 145 |
+
c.JSON(http.StatusOK, gin.H{
|
| 146 |
+
"success": true,
|
| 147 |
+
"message": "",
|
| 148 |
+
})
|
| 149 |
+
return
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
func UpdateRedemption(c *gin.Context) {
|
| 153 |
+
statusOnly := c.Query("status_only")
|
| 154 |
+
redemption := model.Redemption{}
|
| 155 |
+
err := c.ShouldBindJSON(&redemption)
|
| 156 |
+
if err != nil {
|
| 157 |
+
c.JSON(http.StatusOK, gin.H{
|
| 158 |
+
"success": false,
|
| 159 |
+
"message": err.Error(),
|
| 160 |
+
})
|
| 161 |
+
return
|
| 162 |
+
}
|
| 163 |
+
cleanRedemption, err := model.GetRedemptionById(redemption.Id)
|
| 164 |
+
if err != nil {
|
| 165 |
+
c.JSON(http.StatusOK, gin.H{
|
| 166 |
+
"success": false,
|
| 167 |
+
"message": err.Error(),
|
| 168 |
+
})
|
| 169 |
+
return
|
| 170 |
+
}
|
| 171 |
+
if statusOnly != "" {
|
| 172 |
+
cleanRedemption.Status = redemption.Status
|
| 173 |
+
} else {
|
| 174 |
+
// If you add more fields, please also update redemption.Update()
|
| 175 |
+
cleanRedemption.Name = redemption.Name
|
| 176 |
+
cleanRedemption.Quota = redemption.Quota
|
| 177 |
+
}
|
| 178 |
+
err = cleanRedemption.Update()
|
| 179 |
+
if err != nil {
|
| 180 |
+
c.JSON(http.StatusOK, gin.H{
|
| 181 |
+
"success": false,
|
| 182 |
+
"message": err.Error(),
|
| 183 |
+
})
|
| 184 |
+
return
|
| 185 |
+
}
|
| 186 |
+
c.JSON(http.StatusOK, gin.H{
|
| 187 |
+
"success": true,
|
| 188 |
+
"message": "",
|
| 189 |
+
"data": cleanRedemption,
|
| 190 |
+
})
|
| 191 |
+
return
|
| 192 |
+
}
|
controller/relay-ali.go
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"one-api/common"
|
| 10 |
+
"strings"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r
|
| 14 |
+
|
| 15 |
+
type AliMessage struct {
|
| 16 |
+
User string `json:"user"`
|
| 17 |
+
Bot string `json:"bot"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type AliInput struct {
|
| 21 |
+
Prompt string `json:"prompt"`
|
| 22 |
+
History []AliMessage `json:"history"`
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type AliParameters struct {
|
| 26 |
+
TopP float64 `json:"top_p,omitempty"`
|
| 27 |
+
TopK int `json:"top_k,omitempty"`
|
| 28 |
+
Seed uint64 `json:"seed,omitempty"`
|
| 29 |
+
EnableSearch bool `json:"enable_search,omitempty"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
type AliChatRequest struct {
|
| 33 |
+
Model string `json:"model"`
|
| 34 |
+
Input AliInput `json:"input"`
|
| 35 |
+
Parameters AliParameters `json:"parameters,omitempty"`
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
type AliError struct {
|
| 39 |
+
Code string `json:"code"`
|
| 40 |
+
Message string `json:"message"`
|
| 41 |
+
RequestId string `json:"request_id"`
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
type AliUsage struct {
|
| 45 |
+
InputTokens int `json:"input_tokens"`
|
| 46 |
+
OutputTokens int `json:"output_tokens"`
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
type AliOutput struct {
|
| 50 |
+
Text string `json:"text"`
|
| 51 |
+
FinishReason string `json:"finish_reason"`
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
type AliChatResponse struct {
|
| 55 |
+
Output AliOutput `json:"output"`
|
| 56 |
+
Usage AliUsage `json:"usage"`
|
| 57 |
+
AliError
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest {
|
| 61 |
+
messages := make([]AliMessage, 0, len(request.Messages))
|
| 62 |
+
prompt := ""
|
| 63 |
+
for i := 0; i < len(request.Messages); i++ {
|
| 64 |
+
message := request.Messages[i]
|
| 65 |
+
if message.Role == "system" {
|
| 66 |
+
messages = append(messages, AliMessage{
|
| 67 |
+
User: message.Content,
|
| 68 |
+
Bot: "Okay",
|
| 69 |
+
})
|
| 70 |
+
continue
|
| 71 |
+
} else {
|
| 72 |
+
if i == len(request.Messages)-1 {
|
| 73 |
+
prompt = message.Content
|
| 74 |
+
break
|
| 75 |
+
}
|
| 76 |
+
messages = append(messages, AliMessage{
|
| 77 |
+
User: message.Content,
|
| 78 |
+
Bot: request.Messages[i+1].Content,
|
| 79 |
+
})
|
| 80 |
+
i++
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
return &AliChatRequest{
|
| 84 |
+
Model: request.Model,
|
| 85 |
+
Input: AliInput{
|
| 86 |
+
Prompt: prompt,
|
| 87 |
+
History: messages,
|
| 88 |
+
},
|
| 89 |
+
//Parameters: AliParameters{ // ChatGPT's parameters are not compatible with Ali's
|
| 90 |
+
// TopP: request.TopP,
|
| 91 |
+
// TopK: 50,
|
| 92 |
+
// //Seed: 0,
|
| 93 |
+
// //EnableSearch: false,
|
| 94 |
+
//},
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse {
|
| 99 |
+
choice := OpenAITextResponseChoice{
|
| 100 |
+
Index: 0,
|
| 101 |
+
Message: Message{
|
| 102 |
+
Role: "assistant",
|
| 103 |
+
Content: response.Output.Text,
|
| 104 |
+
},
|
| 105 |
+
FinishReason: response.Output.FinishReason,
|
| 106 |
+
}
|
| 107 |
+
fullTextResponse := OpenAITextResponse{
|
| 108 |
+
Id: response.RequestId,
|
| 109 |
+
Object: "chat.completion",
|
| 110 |
+
Created: common.GetTimestamp(),
|
| 111 |
+
Choices: []OpenAITextResponseChoice{choice},
|
| 112 |
+
Usage: Usage{
|
| 113 |
+
PromptTokens: response.Usage.InputTokens,
|
| 114 |
+
CompletionTokens: response.Usage.OutputTokens,
|
| 115 |
+
TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
|
| 116 |
+
},
|
| 117 |
+
}
|
| 118 |
+
return &fullTextResponse
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse {
|
| 122 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 123 |
+
choice.Delta.Content = aliResponse.Output.Text
|
| 124 |
+
if aliResponse.Output.FinishReason != "null" {
|
| 125 |
+
finishReason := aliResponse.Output.FinishReason
|
| 126 |
+
choice.FinishReason = &finishReason
|
| 127 |
+
}
|
| 128 |
+
response := ChatCompletionsStreamResponse{
|
| 129 |
+
Id: aliResponse.RequestId,
|
| 130 |
+
Object: "chat.completion.chunk",
|
| 131 |
+
Created: common.GetTimestamp(),
|
| 132 |
+
Model: "ernie-bot",
|
| 133 |
+
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
| 134 |
+
}
|
| 135 |
+
return &response
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 139 |
+
var usage Usage
|
| 140 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 141 |
+
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
| 142 |
+
if atEOF && len(data) == 0 {
|
| 143 |
+
return 0, nil, nil
|
| 144 |
+
}
|
| 145 |
+
if i := strings.Index(string(data), "\n"); i >= 0 {
|
| 146 |
+
return i + 1, data[0:i], nil
|
| 147 |
+
}
|
| 148 |
+
if atEOF {
|
| 149 |
+
return len(data), data, nil
|
| 150 |
+
}
|
| 151 |
+
return 0, nil, nil
|
| 152 |
+
})
|
| 153 |
+
dataChan := make(chan string)
|
| 154 |
+
stopChan := make(chan bool)
|
| 155 |
+
go func() {
|
| 156 |
+
for scanner.Scan() {
|
| 157 |
+
data := scanner.Text()
|
| 158 |
+
if len(data) < 5 { // ignore blank line or wrong format
|
| 159 |
+
continue
|
| 160 |
+
}
|
| 161 |
+
if data[:5] != "data:" {
|
| 162 |
+
continue
|
| 163 |
+
}
|
| 164 |
+
data = data[5:]
|
| 165 |
+
dataChan <- data
|
| 166 |
+
}
|
| 167 |
+
stopChan <- true
|
| 168 |
+
}()
|
| 169 |
+
setEventStreamHeaders(c)
|
| 170 |
+
lastResponseText := ""
|
| 171 |
+
c.Stream(func(w io.Writer) bool {
|
| 172 |
+
select {
|
| 173 |
+
case data := <-dataChan:
|
| 174 |
+
var aliResponse AliChatResponse
|
| 175 |
+
err := json.Unmarshal([]byte(data), &aliResponse)
|
| 176 |
+
if err != nil {
|
| 177 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 178 |
+
return true
|
| 179 |
+
}
|
| 180 |
+
if aliResponse.Usage.OutputTokens != 0 {
|
| 181 |
+
usage.PromptTokens = aliResponse.Usage.InputTokens
|
| 182 |
+
usage.CompletionTokens = aliResponse.Usage.OutputTokens
|
| 183 |
+
usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens
|
| 184 |
+
}
|
| 185 |
+
response := streamResponseAli2OpenAI(&aliResponse)
|
| 186 |
+
response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText)
|
| 187 |
+
lastResponseText = aliResponse.Output.Text
|
| 188 |
+
jsonResponse, err := json.Marshal(response)
|
| 189 |
+
if err != nil {
|
| 190 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 191 |
+
return true
|
| 192 |
+
}
|
| 193 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
| 194 |
+
return true
|
| 195 |
+
case <-stopChan:
|
| 196 |
+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
| 197 |
+
return false
|
| 198 |
+
}
|
| 199 |
+
})
|
| 200 |
+
err := resp.Body.Close()
|
| 201 |
+
if err != nil {
|
| 202 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 203 |
+
}
|
| 204 |
+
return nil, &usage
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
func aliHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 208 |
+
var aliResponse AliChatResponse
|
| 209 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 210 |
+
if err != nil {
|
| 211 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 212 |
+
}
|
| 213 |
+
err = resp.Body.Close()
|
| 214 |
+
if err != nil {
|
| 215 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 216 |
+
}
|
| 217 |
+
err = json.Unmarshal(responseBody, &aliResponse)
|
| 218 |
+
if err != nil {
|
| 219 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 220 |
+
}
|
| 221 |
+
if aliResponse.Code != "" {
|
| 222 |
+
return &OpenAIErrorWithStatusCode{
|
| 223 |
+
OpenAIError: OpenAIError{
|
| 224 |
+
Message: aliResponse.Message,
|
| 225 |
+
Type: aliResponse.Code,
|
| 226 |
+
Param: aliResponse.RequestId,
|
| 227 |
+
Code: aliResponse.Code,
|
| 228 |
+
},
|
| 229 |
+
StatusCode: resp.StatusCode,
|
| 230 |
+
}, nil
|
| 231 |
+
}
|
| 232 |
+
fullTextResponse := responseAli2OpenAI(&aliResponse)
|
| 233 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 234 |
+
if err != nil {
|
| 235 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 236 |
+
}
|
| 237 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 238 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 239 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 240 |
+
return nil, &fullTextResponse.Usage
|
| 241 |
+
}
|
controller/relay-audio.go
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"one-api/common"
|
| 10 |
+
"one-api/model"
|
| 11 |
+
|
| 12 |
+
"github.com/gin-gonic/gin"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
| 16 |
+
audioModel := "whisper-1"
|
| 17 |
+
|
| 18 |
+
tokenId := c.GetInt("token_id")
|
| 19 |
+
channelType := c.GetInt("channel")
|
| 20 |
+
userId := c.GetInt("id")
|
| 21 |
+
group := c.GetString("group")
|
| 22 |
+
|
| 23 |
+
preConsumedTokens := common.PreConsumedQuota
|
| 24 |
+
modelRatio := common.GetModelRatio(audioModel)
|
| 25 |
+
groupRatio := common.GetGroupRatio(group)
|
| 26 |
+
ratio := modelRatio * groupRatio
|
| 27 |
+
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
| 28 |
+
userQuota, err := model.CacheGetUserQuota(userId)
|
| 29 |
+
if err != nil {
|
| 30 |
+
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
| 31 |
+
}
|
| 32 |
+
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
| 33 |
+
if err != nil {
|
| 34 |
+
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
| 35 |
+
}
|
| 36 |
+
if userQuota > 100*preConsumedQuota {
|
| 37 |
+
// in this case, we do not pre-consume quota
|
| 38 |
+
// because the user has enough quota
|
| 39 |
+
preConsumedQuota = 0
|
| 40 |
+
}
|
| 41 |
+
if preConsumedQuota > 0 {
|
| 42 |
+
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
|
| 43 |
+
if err != nil {
|
| 44 |
+
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// map model name
|
| 49 |
+
modelMapping := c.GetString("model_mapping")
|
| 50 |
+
if modelMapping != "" {
|
| 51 |
+
modelMap := make(map[string]string)
|
| 52 |
+
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
| 53 |
+
if err != nil {
|
| 54 |
+
return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
| 55 |
+
}
|
| 56 |
+
if modelMap[audioModel] != "" {
|
| 57 |
+
audioModel = modelMap[audioModel]
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
baseURL := common.ChannelBaseURLs[channelType]
|
| 62 |
+
requestURL := c.Request.URL.String()
|
| 63 |
+
|
| 64 |
+
if c.GetString("base_url") != "" {
|
| 65 |
+
baseURL = c.GetString("base_url")
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
| 69 |
+
requestBody := c.Request.Body
|
| 70 |
+
|
| 71 |
+
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
| 72 |
+
if err != nil {
|
| 73 |
+
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
| 74 |
+
}
|
| 75 |
+
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
| 76 |
+
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
| 77 |
+
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
| 78 |
+
|
| 79 |
+
resp, err := httpClient.Do(req)
|
| 80 |
+
if err != nil {
|
| 81 |
+
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
err = req.Body.Close()
|
| 85 |
+
if err != nil {
|
| 86 |
+
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
| 87 |
+
}
|
| 88 |
+
err = c.Request.Body.Close()
|
| 89 |
+
if err != nil {
|
| 90 |
+
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
| 91 |
+
}
|
| 92 |
+
var audioResponse AudioResponse
|
| 93 |
+
|
| 94 |
+
defer func() {
|
| 95 |
+
go func() {
|
| 96 |
+
quota := countTokenText(audioResponse.Text, audioModel)
|
| 97 |
+
quotaDelta := quota - preConsumedQuota
|
| 98 |
+
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
|
| 99 |
+
if err != nil {
|
| 100 |
+
common.SysError("error consuming token remain quota: " + err.Error())
|
| 101 |
+
}
|
| 102 |
+
err = model.CacheUpdateUserQuota(userId)
|
| 103 |
+
if err != nil {
|
| 104 |
+
common.SysError("error update user quota cache: " + err.Error())
|
| 105 |
+
}
|
| 106 |
+
if quota != 0 {
|
| 107 |
+
tokenName := c.GetString("token_name")
|
| 108 |
+
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
| 109 |
+
model.RecordConsumeLog(userId, 0, 0, audioModel, tokenName, quota, logContent)
|
| 110 |
+
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
| 111 |
+
channelId := c.GetInt("channel_id")
|
| 112 |
+
model.UpdateChannelUsedQuota(channelId, quota)
|
| 113 |
+
}
|
| 114 |
+
}()
|
| 115 |
+
}()
|
| 116 |
+
|
| 117 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 118 |
+
|
| 119 |
+
if err != nil {
|
| 120 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
| 121 |
+
}
|
| 122 |
+
err = resp.Body.Close()
|
| 123 |
+
if err != nil {
|
| 124 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
| 125 |
+
}
|
| 126 |
+
err = json.Unmarshal(responseBody, &audioResponse)
|
| 127 |
+
if err != nil {
|
| 128 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
| 132 |
+
|
| 133 |
+
for k, v := range resp.Header {
|
| 134 |
+
c.Writer.Header().Set(k, v[0])
|
| 135 |
+
}
|
| 136 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 137 |
+
|
| 138 |
+
_, err = io.Copy(c.Writer, resp.Body)
|
| 139 |
+
if err != nil {
|
| 140 |
+
return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
|
| 141 |
+
}
|
| 142 |
+
err = resp.Body.Close()
|
| 143 |
+
if err != nil {
|
| 144 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
| 145 |
+
}
|
| 146 |
+
return nil
|
| 147 |
+
}
|
controller/relay-baidu.go
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
"io"
|
| 10 |
+
"net/http"
|
| 11 |
+
"one-api/common"
|
| 12 |
+
"strings"
|
| 13 |
+
"sync"
|
| 14 |
+
"time"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2
|
| 18 |
+
|
| 19 |
+
type BaiduTokenResponse struct {
|
| 20 |
+
ExpiresIn int `json:"expires_in"`
|
| 21 |
+
AccessToken string `json:"access_token"`
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
type BaiduMessage struct {
|
| 25 |
+
Role string `json:"role"`
|
| 26 |
+
Content string `json:"content"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type BaiduChatRequest struct {
|
| 30 |
+
Messages []BaiduMessage `json:"messages"`
|
| 31 |
+
Stream bool `json:"stream"`
|
| 32 |
+
UserId string `json:"user_id,omitempty"`
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
type BaiduError struct {
|
| 36 |
+
ErrorCode int `json:"error_code"`
|
| 37 |
+
ErrorMsg string `json:"error_msg"`
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
type BaiduChatResponse struct {
|
| 41 |
+
Id string `json:"id"`
|
| 42 |
+
Object string `json:"object"`
|
| 43 |
+
Created int64 `json:"created"`
|
| 44 |
+
Result string `json:"result"`
|
| 45 |
+
IsTruncated bool `json:"is_truncated"`
|
| 46 |
+
NeedClearHistory bool `json:"need_clear_history"`
|
| 47 |
+
Usage Usage `json:"usage"`
|
| 48 |
+
BaiduError
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
type BaiduChatStreamResponse struct {
|
| 52 |
+
BaiduChatResponse
|
| 53 |
+
SentenceId int `json:"sentence_id"`
|
| 54 |
+
IsEnd bool `json:"is_end"`
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
type BaiduEmbeddingRequest struct {
|
| 58 |
+
Input []string `json:"input"`
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
type BaiduEmbeddingData struct {
|
| 62 |
+
Object string `json:"object"`
|
| 63 |
+
Embedding []float64 `json:"embedding"`
|
| 64 |
+
Index int `json:"index"`
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
type BaiduEmbeddingResponse struct {
|
| 68 |
+
Id string `json:"id"`
|
| 69 |
+
Object string `json:"object"`
|
| 70 |
+
Created int64 `json:"created"`
|
| 71 |
+
Data []BaiduEmbeddingData `json:"data"`
|
| 72 |
+
Usage Usage `json:"usage"`
|
| 73 |
+
BaiduError
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
type BaiduAccessToken struct {
|
| 77 |
+
AccessToken string `json:"access_token"`
|
| 78 |
+
Error string `json:"error,omitempty"`
|
| 79 |
+
ErrorDescription string `json:"error_description,omitempty"`
|
| 80 |
+
ExpiresIn int64 `json:"expires_in,omitempty"`
|
| 81 |
+
ExpiresAt time.Time `json:"-"`
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
var baiduTokenStore sync.Map
|
| 85 |
+
|
| 86 |
+
func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
| 87 |
+
messages := make([]BaiduMessage, 0, len(request.Messages))
|
| 88 |
+
for _, message := range request.Messages {
|
| 89 |
+
if message.Role == "system" {
|
| 90 |
+
messages = append(messages, BaiduMessage{
|
| 91 |
+
Role: "user",
|
| 92 |
+
Content: message.Content,
|
| 93 |
+
})
|
| 94 |
+
messages = append(messages, BaiduMessage{
|
| 95 |
+
Role: "assistant",
|
| 96 |
+
Content: "Okay",
|
| 97 |
+
})
|
| 98 |
+
} else {
|
| 99 |
+
messages = append(messages, BaiduMessage{
|
| 100 |
+
Role: message.Role,
|
| 101 |
+
Content: message.Content,
|
| 102 |
+
})
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
return &BaiduChatRequest{
|
| 106 |
+
Messages: messages,
|
| 107 |
+
Stream: request.Stream,
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
func responseBaidu2OpenAI(response *BaiduChatResponse) *OpenAITextResponse {
|
| 112 |
+
choice := OpenAITextResponseChoice{
|
| 113 |
+
Index: 0,
|
| 114 |
+
Message: Message{
|
| 115 |
+
Role: "assistant",
|
| 116 |
+
Content: response.Result,
|
| 117 |
+
},
|
| 118 |
+
FinishReason: "stop",
|
| 119 |
+
}
|
| 120 |
+
fullTextResponse := OpenAITextResponse{
|
| 121 |
+
Id: response.Id,
|
| 122 |
+
Object: "chat.completion",
|
| 123 |
+
Created: response.Created,
|
| 124 |
+
Choices: []OpenAITextResponseChoice{choice},
|
| 125 |
+
Usage: response.Usage,
|
| 126 |
+
}
|
| 127 |
+
return &fullTextResponse
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
|
| 131 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 132 |
+
choice.Delta.Content = baiduResponse.Result
|
| 133 |
+
if baiduResponse.IsEnd {
|
| 134 |
+
choice.FinishReason = &stopFinishReason
|
| 135 |
+
}
|
| 136 |
+
response := ChatCompletionsStreamResponse{
|
| 137 |
+
Id: baiduResponse.Id,
|
| 138 |
+
Object: "chat.completion.chunk",
|
| 139 |
+
Created: baiduResponse.Created,
|
| 140 |
+
Model: "ernie-bot",
|
| 141 |
+
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
| 142 |
+
}
|
| 143 |
+
return &response
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
func embeddingRequestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduEmbeddingRequest {
|
| 147 |
+
baiduEmbeddingRequest := BaiduEmbeddingRequest{
|
| 148 |
+
Input: nil,
|
| 149 |
+
}
|
| 150 |
+
switch request.Input.(type) {
|
| 151 |
+
case string:
|
| 152 |
+
baiduEmbeddingRequest.Input = []string{request.Input.(string)}
|
| 153 |
+
case []any:
|
| 154 |
+
for _, item := range request.Input.([]any) {
|
| 155 |
+
if str, ok := item.(string); ok {
|
| 156 |
+
baiduEmbeddingRequest.Input = append(baiduEmbeddingRequest.Input, str)
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
return &baiduEmbeddingRequest
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
func embeddingResponseBaidu2OpenAI(response *BaiduEmbeddingResponse) *OpenAIEmbeddingResponse {
|
| 164 |
+
openAIEmbeddingResponse := OpenAIEmbeddingResponse{
|
| 165 |
+
Object: "list",
|
| 166 |
+
Data: make([]OpenAIEmbeddingResponseItem, 0, len(response.Data)),
|
| 167 |
+
Model: "baidu-embedding",
|
| 168 |
+
Usage: response.Usage,
|
| 169 |
+
}
|
| 170 |
+
for _, item := range response.Data {
|
| 171 |
+
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, OpenAIEmbeddingResponseItem{
|
| 172 |
+
Object: item.Object,
|
| 173 |
+
Index: item.Index,
|
| 174 |
+
Embedding: item.Embedding,
|
| 175 |
+
})
|
| 176 |
+
}
|
| 177 |
+
return &openAIEmbeddingResponse
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 181 |
+
var usage Usage
|
| 182 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 183 |
+
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
| 184 |
+
if atEOF && len(data) == 0 {
|
| 185 |
+
return 0, nil, nil
|
| 186 |
+
}
|
| 187 |
+
if i := strings.Index(string(data), "\n"); i >= 0 {
|
| 188 |
+
return i + 1, data[0:i], nil
|
| 189 |
+
}
|
| 190 |
+
if atEOF {
|
| 191 |
+
return len(data), data, nil
|
| 192 |
+
}
|
| 193 |
+
return 0, nil, nil
|
| 194 |
+
})
|
| 195 |
+
dataChan := make(chan string)
|
| 196 |
+
stopChan := make(chan bool)
|
| 197 |
+
go func() {
|
| 198 |
+
for scanner.Scan() {
|
| 199 |
+
data := scanner.Text()
|
| 200 |
+
if len(data) < 6 { // ignore blank line or wrong format
|
| 201 |
+
continue
|
| 202 |
+
}
|
| 203 |
+
data = data[6:]
|
| 204 |
+
dataChan <- data
|
| 205 |
+
}
|
| 206 |
+
stopChan <- true
|
| 207 |
+
}()
|
| 208 |
+
setEventStreamHeaders(c)
|
| 209 |
+
c.Stream(func(w io.Writer) bool {
|
| 210 |
+
select {
|
| 211 |
+
case data := <-dataChan:
|
| 212 |
+
var baiduResponse BaiduChatStreamResponse
|
| 213 |
+
err := json.Unmarshal([]byte(data), &baiduResponse)
|
| 214 |
+
if err != nil {
|
| 215 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 216 |
+
return true
|
| 217 |
+
}
|
| 218 |
+
if baiduResponse.Usage.TotalTokens != 0 {
|
| 219 |
+
usage.TotalTokens = baiduResponse.Usage.TotalTokens
|
| 220 |
+
usage.PromptTokens = baiduResponse.Usage.PromptTokens
|
| 221 |
+
usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens
|
| 222 |
+
}
|
| 223 |
+
response := streamResponseBaidu2OpenAI(&baiduResponse)
|
| 224 |
+
jsonResponse, err := json.Marshal(response)
|
| 225 |
+
if err != nil {
|
| 226 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 227 |
+
return true
|
| 228 |
+
}
|
| 229 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
| 230 |
+
return true
|
| 231 |
+
case <-stopChan:
|
| 232 |
+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
| 233 |
+
return false
|
| 234 |
+
}
|
| 235 |
+
})
|
| 236 |
+
err := resp.Body.Close()
|
| 237 |
+
if err != nil {
|
| 238 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 239 |
+
}
|
| 240 |
+
return nil, &usage
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
func baiduHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 244 |
+
var baiduResponse BaiduChatResponse
|
| 245 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 246 |
+
if err != nil {
|
| 247 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 248 |
+
}
|
| 249 |
+
err = resp.Body.Close()
|
| 250 |
+
if err != nil {
|
| 251 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 252 |
+
}
|
| 253 |
+
err = json.Unmarshal(responseBody, &baiduResponse)
|
| 254 |
+
if err != nil {
|
| 255 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 256 |
+
}
|
| 257 |
+
if baiduResponse.ErrorMsg != "" {
|
| 258 |
+
return &OpenAIErrorWithStatusCode{
|
| 259 |
+
OpenAIError: OpenAIError{
|
| 260 |
+
Message: baiduResponse.ErrorMsg,
|
| 261 |
+
Type: "baidu_error",
|
| 262 |
+
Param: "",
|
| 263 |
+
Code: baiduResponse.ErrorCode,
|
| 264 |
+
},
|
| 265 |
+
StatusCode: resp.StatusCode,
|
| 266 |
+
}, nil
|
| 267 |
+
}
|
| 268 |
+
fullTextResponse := responseBaidu2OpenAI(&baiduResponse)
|
| 269 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 270 |
+
if err != nil {
|
| 271 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 272 |
+
}
|
| 273 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 274 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 275 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 276 |
+
return nil, &fullTextResponse.Usage
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 280 |
+
var baiduResponse BaiduEmbeddingResponse
|
| 281 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 282 |
+
if err != nil {
|
| 283 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 284 |
+
}
|
| 285 |
+
err = resp.Body.Close()
|
| 286 |
+
if err != nil {
|
| 287 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 288 |
+
}
|
| 289 |
+
err = json.Unmarshal(responseBody, &baiduResponse)
|
| 290 |
+
if err != nil {
|
| 291 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 292 |
+
}
|
| 293 |
+
if baiduResponse.ErrorMsg != "" {
|
| 294 |
+
return &OpenAIErrorWithStatusCode{
|
| 295 |
+
OpenAIError: OpenAIError{
|
| 296 |
+
Message: baiduResponse.ErrorMsg,
|
| 297 |
+
Type: "baidu_error",
|
| 298 |
+
Param: "",
|
| 299 |
+
Code: baiduResponse.ErrorCode,
|
| 300 |
+
},
|
| 301 |
+
StatusCode: resp.StatusCode,
|
| 302 |
+
}, nil
|
| 303 |
+
}
|
| 304 |
+
fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse)
|
| 305 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 306 |
+
if err != nil {
|
| 307 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 308 |
+
}
|
| 309 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 310 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 311 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 312 |
+
return nil, &fullTextResponse.Usage
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
func getBaiduAccessToken(apiKey string) (string, error) {
|
| 316 |
+
if val, ok := baiduTokenStore.Load(apiKey); ok {
|
| 317 |
+
var accessToken BaiduAccessToken
|
| 318 |
+
if accessToken, ok = val.(BaiduAccessToken); ok {
|
| 319 |
+
// soon this will expire
|
| 320 |
+
if time.Now().Add(time.Hour).After(accessToken.ExpiresAt) {
|
| 321 |
+
go func() {
|
| 322 |
+
_, _ = getBaiduAccessTokenHelper(apiKey)
|
| 323 |
+
}()
|
| 324 |
+
}
|
| 325 |
+
return accessToken.AccessToken, nil
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
accessToken, err := getBaiduAccessTokenHelper(apiKey)
|
| 329 |
+
if err != nil {
|
| 330 |
+
return "", err
|
| 331 |
+
}
|
| 332 |
+
if accessToken == nil {
|
| 333 |
+
return "", errors.New("getBaiduAccessToken return a nil token")
|
| 334 |
+
}
|
| 335 |
+
return (*accessToken).AccessToken, nil
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
func getBaiduAccessTokenHelper(apiKey string) (*BaiduAccessToken, error) {
|
| 339 |
+
parts := strings.Split(apiKey, "|")
|
| 340 |
+
if len(parts) != 2 {
|
| 341 |
+
return nil, errors.New("invalid baidu apikey")
|
| 342 |
+
}
|
| 343 |
+
req, err := http.NewRequest("POST", fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
|
| 344 |
+
parts[0], parts[1]), nil)
|
| 345 |
+
if err != nil {
|
| 346 |
+
return nil, err
|
| 347 |
+
}
|
| 348 |
+
req.Header.Add("Content-Type", "application/json")
|
| 349 |
+
req.Header.Add("Accept", "application/json")
|
| 350 |
+
res, err := impatientHTTPClient.Do(req)
|
| 351 |
+
if err != nil {
|
| 352 |
+
return nil, err
|
| 353 |
+
}
|
| 354 |
+
defer res.Body.Close()
|
| 355 |
+
|
| 356 |
+
var accessToken BaiduAccessToken
|
| 357 |
+
err = json.NewDecoder(res.Body).Decode(&accessToken)
|
| 358 |
+
if err != nil {
|
| 359 |
+
return nil, err
|
| 360 |
+
}
|
| 361 |
+
if accessToken.Error != "" {
|
| 362 |
+
return nil, errors.New(accessToken.Error + ": " + accessToken.ErrorDescription)
|
| 363 |
+
}
|
| 364 |
+
if accessToken.AccessToken == "" {
|
| 365 |
+
return nil, errors.New("getBaiduAccessTokenHelper get empty access token")
|
| 366 |
+
}
|
| 367 |
+
accessToken.ExpiresAt = time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second)
|
| 368 |
+
baiduTokenStore.Store(apiKey, accessToken)
|
| 369 |
+
return &accessToken, nil
|
| 370 |
+
}
|
controller/relay-claude.go
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
"one-api/common"
|
| 11 |
+
"strings"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type ClaudeMetadata struct {
|
| 15 |
+
UserId string `json:"user_id"`
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type ClaudeRequest struct {
|
| 19 |
+
Model string `json:"model"`
|
| 20 |
+
Prompt string `json:"prompt"`
|
| 21 |
+
MaxTokensToSample int `json:"max_tokens_to_sample"`
|
| 22 |
+
StopSequences []string `json:"stop_sequences,omitempty"`
|
| 23 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 24 |
+
TopP float64 `json:"top_p,omitempty"`
|
| 25 |
+
TopK int `json:"top_k,omitempty"`
|
| 26 |
+
//ClaudeMetadata `json:"metadata,omitempty"`
|
| 27 |
+
Stream bool `json:"stream,omitempty"`
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
type ClaudeError struct {
|
| 31 |
+
Type string `json:"type"`
|
| 32 |
+
Message string `json:"message"`
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
type ClaudeResponse struct {
|
| 36 |
+
Completion string `json:"completion"`
|
| 37 |
+
StopReason string `json:"stop_reason"`
|
| 38 |
+
Model string `json:"model"`
|
| 39 |
+
Error ClaudeError `json:"error"`
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func stopReasonClaude2OpenAI(reason string) string {
|
| 43 |
+
switch reason {
|
| 44 |
+
case "stop_sequence":
|
| 45 |
+
return "stop"
|
| 46 |
+
case "max_tokens":
|
| 47 |
+
return "length"
|
| 48 |
+
default:
|
| 49 |
+
return reason
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
|
| 54 |
+
claudeRequest := ClaudeRequest{
|
| 55 |
+
Model: textRequest.Model,
|
| 56 |
+
Prompt: "",
|
| 57 |
+
MaxTokensToSample: textRequest.MaxTokens,
|
| 58 |
+
StopSequences: nil,
|
| 59 |
+
Temperature: textRequest.Temperature,
|
| 60 |
+
TopP: textRequest.TopP,
|
| 61 |
+
Stream: textRequest.Stream,
|
| 62 |
+
}
|
| 63 |
+
if claudeRequest.MaxTokensToSample == 0 {
|
| 64 |
+
claudeRequest.MaxTokensToSample = 1000000
|
| 65 |
+
}
|
| 66 |
+
prompt := ""
|
| 67 |
+
for _, message := range textRequest.Messages {
|
| 68 |
+
if message.Role == "user" {
|
| 69 |
+
prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
|
| 70 |
+
} else if message.Role == "assistant" {
|
| 71 |
+
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
|
| 72 |
+
} else if message.Role == "system" {
|
| 73 |
+
prompt += fmt.Sprintf("\n\nSystem: %s", message.Content)
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
prompt += "\n\nAssistant:"
|
| 77 |
+
claudeRequest.Prompt = prompt
|
| 78 |
+
return &claudeRequest
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
|
| 82 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 83 |
+
choice.Delta.Content = claudeResponse.Completion
|
| 84 |
+
finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason)
|
| 85 |
+
if finishReason != "null" {
|
| 86 |
+
choice.FinishReason = &finishReason
|
| 87 |
+
}
|
| 88 |
+
var response ChatCompletionsStreamResponse
|
| 89 |
+
response.Object = "chat.completion.chunk"
|
| 90 |
+
response.Model = claudeResponse.Model
|
| 91 |
+
response.Choices = []ChatCompletionsStreamResponseChoice{choice}
|
| 92 |
+
return &response
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
func responseClaude2OpenAI(claudeResponse *ClaudeResponse) *OpenAITextResponse {
|
| 96 |
+
choice := OpenAITextResponseChoice{
|
| 97 |
+
Index: 0,
|
| 98 |
+
Message: Message{
|
| 99 |
+
Role: "assistant",
|
| 100 |
+
Content: strings.TrimPrefix(claudeResponse.Completion, " "),
|
| 101 |
+
Name: nil,
|
| 102 |
+
},
|
| 103 |
+
FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),
|
| 104 |
+
}
|
| 105 |
+
fullTextResponse := OpenAITextResponse{
|
| 106 |
+
Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
|
| 107 |
+
Object: "chat.completion",
|
| 108 |
+
Created: common.GetTimestamp(),
|
| 109 |
+
Choices: []OpenAITextResponseChoice{choice},
|
| 110 |
+
}
|
| 111 |
+
return &fullTextResponse
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
func claudeStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
|
| 115 |
+
responseText := ""
|
| 116 |
+
responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
|
| 117 |
+
createdTime := common.GetTimestamp()
|
| 118 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 119 |
+
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
| 120 |
+
if atEOF && len(data) == 0 {
|
| 121 |
+
return 0, nil, nil
|
| 122 |
+
}
|
| 123 |
+
if i := strings.Index(string(data), "\r\n\r\n"); i >= 0 {
|
| 124 |
+
return i + 4, data[0:i], nil
|
| 125 |
+
}
|
| 126 |
+
if atEOF {
|
| 127 |
+
return len(data), data, nil
|
| 128 |
+
}
|
| 129 |
+
return 0, nil, nil
|
| 130 |
+
})
|
| 131 |
+
dataChan := make(chan string)
|
| 132 |
+
stopChan := make(chan bool)
|
| 133 |
+
go func() {
|
| 134 |
+
for scanner.Scan() {
|
| 135 |
+
data := scanner.Text()
|
| 136 |
+
if !strings.HasPrefix(data, "event: completion") {
|
| 137 |
+
continue
|
| 138 |
+
}
|
| 139 |
+
data = strings.TrimPrefix(data, "event: completion\r\ndata: ")
|
| 140 |
+
dataChan <- data
|
| 141 |
+
}
|
| 142 |
+
stopChan <- true
|
| 143 |
+
}()
|
| 144 |
+
setEventStreamHeaders(c)
|
| 145 |
+
c.Stream(func(w io.Writer) bool {
|
| 146 |
+
select {
|
| 147 |
+
case data := <-dataChan:
|
| 148 |
+
// some implementations may add \r at the end of data
|
| 149 |
+
data = strings.TrimSuffix(data, "\r")
|
| 150 |
+
var claudeResponse ClaudeResponse
|
| 151 |
+
err := json.Unmarshal([]byte(data), &claudeResponse)
|
| 152 |
+
if err != nil {
|
| 153 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 154 |
+
return true
|
| 155 |
+
}
|
| 156 |
+
responseText += claudeResponse.Completion
|
| 157 |
+
response := streamResponseClaude2OpenAI(&claudeResponse)
|
| 158 |
+
response.Id = responseId
|
| 159 |
+
response.Created = createdTime
|
| 160 |
+
jsonStr, err := json.Marshal(response)
|
| 161 |
+
if err != nil {
|
| 162 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 163 |
+
return true
|
| 164 |
+
}
|
| 165 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)})
|
| 166 |
+
return true
|
| 167 |
+
case <-stopChan:
|
| 168 |
+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
| 169 |
+
return false
|
| 170 |
+
}
|
| 171 |
+
})
|
| 172 |
+
err := resp.Body.Close()
|
| 173 |
+
if err != nil {
|
| 174 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
|
| 175 |
+
}
|
| 176 |
+
return nil, responseText
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
func claudeHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 180 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 181 |
+
if err != nil {
|
| 182 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 183 |
+
}
|
| 184 |
+
err = resp.Body.Close()
|
| 185 |
+
if err != nil {
|
| 186 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 187 |
+
}
|
| 188 |
+
var claudeResponse ClaudeResponse
|
| 189 |
+
err = json.Unmarshal(responseBody, &claudeResponse)
|
| 190 |
+
if err != nil {
|
| 191 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 192 |
+
}
|
| 193 |
+
if claudeResponse.Error.Type != "" {
|
| 194 |
+
return &OpenAIErrorWithStatusCode{
|
| 195 |
+
OpenAIError: OpenAIError{
|
| 196 |
+
Message: claudeResponse.Error.Message,
|
| 197 |
+
Type: claudeResponse.Error.Type,
|
| 198 |
+
Param: "",
|
| 199 |
+
Code: claudeResponse.Error.Type,
|
| 200 |
+
},
|
| 201 |
+
StatusCode: resp.StatusCode,
|
| 202 |
+
}, nil
|
| 203 |
+
}
|
| 204 |
+
fullTextResponse := responseClaude2OpenAI(&claudeResponse)
|
| 205 |
+
completionTokens := countTokenText(claudeResponse.Completion, model)
|
| 206 |
+
usage := Usage{
|
| 207 |
+
PromptTokens: promptTokens,
|
| 208 |
+
CompletionTokens: completionTokens,
|
| 209 |
+
TotalTokens: promptTokens + completionTokens,
|
| 210 |
+
}
|
| 211 |
+
fullTextResponse.Usage = usage
|
| 212 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 213 |
+
if err != nil {
|
| 214 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 215 |
+
}
|
| 216 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 217 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 218 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 219 |
+
return nil, &usage
|
| 220 |
+
}
|
controller/relay-image.go
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
"one-api/common"
|
| 11 |
+
"one-api/model"
|
| 12 |
+
|
| 13 |
+
"github.com/gin-gonic/gin"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
| 17 |
+
imageModel := "dall-e"
|
| 18 |
+
|
| 19 |
+
tokenId := c.GetInt("token_id")
|
| 20 |
+
channelType := c.GetInt("channel")
|
| 21 |
+
userId := c.GetInt("id")
|
| 22 |
+
consumeQuota := c.GetBool("consume_quota")
|
| 23 |
+
group := c.GetString("group")
|
| 24 |
+
|
| 25 |
+
var imageRequest ImageRequest
|
| 26 |
+
if consumeQuota {
|
| 27 |
+
err := common.UnmarshalBodyReusable(c, &imageRequest)
|
| 28 |
+
if err != nil {
|
| 29 |
+
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Prompt validation
|
| 34 |
+
if imageRequest.Prompt == "" {
|
| 35 |
+
return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Not "256x256", "512x512", or "1024x1024"
|
| 39 |
+
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
|
| 40 |
+
return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// N should between 1 and 10
|
| 44 |
+
if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
|
| 45 |
+
return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// map model name
|
| 49 |
+
modelMapping := c.GetString("model_mapping")
|
| 50 |
+
isModelMapped := false
|
| 51 |
+
if modelMapping != "" {
|
| 52 |
+
modelMap := make(map[string]string)
|
| 53 |
+
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
| 54 |
+
if err != nil {
|
| 55 |
+
return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
| 56 |
+
}
|
| 57 |
+
if modelMap[imageModel] != "" {
|
| 58 |
+
imageModel = modelMap[imageModel]
|
| 59 |
+
isModelMapped = true
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
baseURL := common.ChannelBaseURLs[channelType]
|
| 64 |
+
requestURL := c.Request.URL.String()
|
| 65 |
+
|
| 66 |
+
if c.GetString("base_url") != "" {
|
| 67 |
+
baseURL = c.GetString("base_url")
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
| 71 |
+
|
| 72 |
+
var requestBody io.Reader
|
| 73 |
+
if isModelMapped {
|
| 74 |
+
jsonStr, err := json.Marshal(imageRequest)
|
| 75 |
+
if err != nil {
|
| 76 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 77 |
+
}
|
| 78 |
+
requestBody = bytes.NewBuffer(jsonStr)
|
| 79 |
+
} else {
|
| 80 |
+
requestBody = c.Request.Body
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
modelRatio := common.GetModelRatio(imageModel)
|
| 84 |
+
groupRatio := common.GetGroupRatio(group)
|
| 85 |
+
ratio := modelRatio * groupRatio
|
| 86 |
+
userQuota, err := model.CacheGetUserQuota(userId)
|
| 87 |
+
|
| 88 |
+
sizeRatio := 1.0
|
| 89 |
+
// Size
|
| 90 |
+
if imageRequest.Size == "256x256" {
|
| 91 |
+
sizeRatio = 1
|
| 92 |
+
} else if imageRequest.Size == "512x512" {
|
| 93 |
+
sizeRatio = 1.125
|
| 94 |
+
} else if imageRequest.Size == "1024x1024" {
|
| 95 |
+
sizeRatio = 1.25
|
| 96 |
+
}
|
| 97 |
+
quota := int(ratio*sizeRatio*1000) * imageRequest.N
|
| 98 |
+
|
| 99 |
+
if consumeQuota && userQuota-quota < 0 {
|
| 100 |
+
return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
| 104 |
+
if err != nil {
|
| 105 |
+
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
| 106 |
+
}
|
| 107 |
+
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
| 108 |
+
|
| 109 |
+
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
| 110 |
+
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
| 111 |
+
|
| 112 |
+
resp, err := httpClient.Do(req)
|
| 113 |
+
if err != nil {
|
| 114 |
+
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
err = req.Body.Close()
|
| 118 |
+
if err != nil {
|
| 119 |
+
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
| 120 |
+
}
|
| 121 |
+
err = c.Request.Body.Close()
|
| 122 |
+
if err != nil {
|
| 123 |
+
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
| 124 |
+
}
|
| 125 |
+
var textResponse ImageResponse
|
| 126 |
+
|
| 127 |
+
defer func() {
|
| 128 |
+
if consumeQuota {
|
| 129 |
+
err := model.PostConsumeTokenQuota(tokenId, quota)
|
| 130 |
+
if err != nil {
|
| 131 |
+
common.SysError("error consuming token remain quota: " + err.Error())
|
| 132 |
+
}
|
| 133 |
+
err = model.CacheUpdateUserQuota(userId)
|
| 134 |
+
if err != nil {
|
| 135 |
+
common.SysError("error update user quota cache: " + err.Error())
|
| 136 |
+
}
|
| 137 |
+
if quota != 0 {
|
| 138 |
+
tokenName := c.GetString("token_name")
|
| 139 |
+
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
| 140 |
+
model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent)
|
| 141 |
+
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
| 142 |
+
channelId := c.GetInt("channel_id")
|
| 143 |
+
model.UpdateChannelUsedQuota(channelId, quota)
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
}()
|
| 147 |
+
|
| 148 |
+
if consumeQuota {
|
| 149 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 150 |
+
|
| 151 |
+
if err != nil {
|
| 152 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
| 153 |
+
}
|
| 154 |
+
err = resp.Body.Close()
|
| 155 |
+
if err != nil {
|
| 156 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
| 157 |
+
}
|
| 158 |
+
err = json.Unmarshal(responseBody, &textResponse)
|
| 159 |
+
if err != nil {
|
| 160 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
for k, v := range resp.Header {
|
| 167 |
+
c.Writer.Header().Set(k, v[0])
|
| 168 |
+
}
|
| 169 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 170 |
+
|
| 171 |
+
_, err = io.Copy(c.Writer, resp.Body)
|
| 172 |
+
if err != nil {
|
| 173 |
+
return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
|
| 174 |
+
}
|
| 175 |
+
err = resp.Body.Close()
|
| 176 |
+
if err != nil {
|
| 177 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
| 178 |
+
}
|
| 179 |
+
return nil
|
| 180 |
+
}
|
controller/relay-openai.go
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
"one-api/common"
|
| 11 |
+
"strings"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) {
|
| 15 |
+
responseText := ""
|
| 16 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 17 |
+
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
| 18 |
+
if atEOF && len(data) == 0 {
|
| 19 |
+
return 0, nil, nil
|
| 20 |
+
}
|
| 21 |
+
if i := strings.Index(string(data), "\n"); i >= 0 {
|
| 22 |
+
return i + 1, data[0:i], nil
|
| 23 |
+
}
|
| 24 |
+
if atEOF {
|
| 25 |
+
return len(data), data, nil
|
| 26 |
+
}
|
| 27 |
+
return 0, nil, nil
|
| 28 |
+
})
|
| 29 |
+
dataChan := make(chan string)
|
| 30 |
+
stopChan := make(chan bool)
|
| 31 |
+
go func() {
|
| 32 |
+
for scanner.Scan() {
|
| 33 |
+
data := scanner.Text()
|
| 34 |
+
if len(data) < 6 { // ignore blank line or wrong format
|
| 35 |
+
continue
|
| 36 |
+
}
|
| 37 |
+
if data[:6] != "data: " && data[:6] != "[DONE]" {
|
| 38 |
+
continue
|
| 39 |
+
}
|
| 40 |
+
dataChan <- data
|
| 41 |
+
data = data[6:]
|
| 42 |
+
if !strings.HasPrefix(data, "[DONE]") {
|
| 43 |
+
switch relayMode {
|
| 44 |
+
case RelayModeChatCompletions:
|
| 45 |
+
var streamResponse ChatCompletionsStreamResponse
|
| 46 |
+
err := json.Unmarshal([]byte(data), &streamResponse)
|
| 47 |
+
if err != nil {
|
| 48 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 49 |
+
continue // just ignore the error
|
| 50 |
+
}
|
| 51 |
+
for _, choice := range streamResponse.Choices {
|
| 52 |
+
responseText += choice.Delta.Content
|
| 53 |
+
}
|
| 54 |
+
case RelayModeCompletions:
|
| 55 |
+
var streamResponse CompletionsStreamResponse
|
| 56 |
+
err := json.Unmarshal([]byte(data), &streamResponse)
|
| 57 |
+
if err != nil {
|
| 58 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 59 |
+
continue
|
| 60 |
+
}
|
| 61 |
+
for _, choice := range streamResponse.Choices {
|
| 62 |
+
responseText += choice.Text
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
stopChan <- true
|
| 68 |
+
}()
|
| 69 |
+
setEventStreamHeaders(c)
|
| 70 |
+
c.Stream(func(w io.Writer) bool {
|
| 71 |
+
select {
|
| 72 |
+
case data := <-dataChan:
|
| 73 |
+
if strings.HasPrefix(data, "data: [DONE]") {
|
| 74 |
+
data = data[:12]
|
| 75 |
+
}
|
| 76 |
+
// some implementations may add \r at the end of data
|
| 77 |
+
data = strings.TrimSuffix(data, "\r")
|
| 78 |
+
c.Render(-1, common.CustomEvent{Data: data})
|
| 79 |
+
return true
|
| 80 |
+
case <-stopChan:
|
| 81 |
+
return false
|
| 82 |
+
}
|
| 83 |
+
})
|
| 84 |
+
err := resp.Body.Close()
|
| 85 |
+
if err != nil {
|
| 86 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
|
| 87 |
+
}
|
| 88 |
+
return nil, responseText
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 92 |
+
var textResponse TextResponse
|
| 93 |
+
if consumeQuota {
|
| 94 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 95 |
+
if err != nil {
|
| 96 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 97 |
+
}
|
| 98 |
+
err = resp.Body.Close()
|
| 99 |
+
if err != nil {
|
| 100 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 101 |
+
}
|
| 102 |
+
err = json.Unmarshal(responseBody, &textResponse)
|
| 103 |
+
if err != nil {
|
| 104 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 105 |
+
}
|
| 106 |
+
if textResponse.Error.Type != "" {
|
| 107 |
+
return &OpenAIErrorWithStatusCode{
|
| 108 |
+
OpenAIError: textResponse.Error,
|
| 109 |
+
StatusCode: resp.StatusCode,
|
| 110 |
+
}, nil
|
| 111 |
+
}
|
| 112 |
+
// Reset response body
|
| 113 |
+
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
| 114 |
+
}
|
| 115 |
+
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
| 116 |
+
// And then we will have to send an error response, but in this case, the header has already been set.
|
| 117 |
+
// So the httpClient will be confused by the response.
|
| 118 |
+
// For example, Postman will report error, and we cannot check the response at all.
|
| 119 |
+
for k, v := range resp.Header {
|
| 120 |
+
c.Writer.Header().Set(k, v[0])
|
| 121 |
+
}
|
| 122 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 123 |
+
_, err := io.Copy(c.Writer, resp.Body)
|
| 124 |
+
if err != nil {
|
| 125 |
+
return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
|
| 126 |
+
}
|
| 127 |
+
err = resp.Body.Close()
|
| 128 |
+
if err != nil {
|
| 129 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
if textResponse.Usage.TotalTokens == 0 {
|
| 133 |
+
completionTokens := 0
|
| 134 |
+
for _, choice := range textResponse.Choices {
|
| 135 |
+
completionTokens += countTokenText(choice.Message.Content, model)
|
| 136 |
+
}
|
| 137 |
+
textResponse.Usage = Usage{
|
| 138 |
+
PromptTokens: promptTokens,
|
| 139 |
+
CompletionTokens: completionTokens,
|
| 140 |
+
TotalTokens: promptTokens + completionTokens,
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
return nil, &textResponse.Usage
|
| 144 |
+
}
|
controller/relay-palm.go
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"one-api/common"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#request-body
|
| 13 |
+
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#response-body
|
| 14 |
+
|
| 15 |
+
type PaLMChatMessage struct {
|
| 16 |
+
Author string `json:"author"`
|
| 17 |
+
Content string `json:"content"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type PaLMFilter struct {
|
| 21 |
+
Reason string `json:"reason"`
|
| 22 |
+
Message string `json:"message"`
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type PaLMPrompt struct {
|
| 26 |
+
Messages []PaLMChatMessage `json:"messages"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type PaLMChatRequest struct {
|
| 30 |
+
Prompt PaLMPrompt `json:"prompt"`
|
| 31 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 32 |
+
CandidateCount int `json:"candidateCount,omitempty"`
|
| 33 |
+
TopP float64 `json:"topP,omitempty"`
|
| 34 |
+
TopK int `json:"topK,omitempty"`
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
type PaLMError struct {
|
| 38 |
+
Code int `json:"code"`
|
| 39 |
+
Message string `json:"message"`
|
| 40 |
+
Status string `json:"status"`
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
type PaLMChatResponse struct {
|
| 44 |
+
Candidates []PaLMChatMessage `json:"candidates"`
|
| 45 |
+
Messages []Message `json:"messages"`
|
| 46 |
+
Filters []PaLMFilter `json:"filters"`
|
| 47 |
+
Error PaLMError `json:"error"`
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func requestOpenAI2PaLM(textRequest GeneralOpenAIRequest) *PaLMChatRequest {
|
| 51 |
+
palmRequest := PaLMChatRequest{
|
| 52 |
+
Prompt: PaLMPrompt{
|
| 53 |
+
Messages: make([]PaLMChatMessage, 0, len(textRequest.Messages)),
|
| 54 |
+
},
|
| 55 |
+
Temperature: textRequest.Temperature,
|
| 56 |
+
CandidateCount: textRequest.N,
|
| 57 |
+
TopP: textRequest.TopP,
|
| 58 |
+
TopK: textRequest.MaxTokens,
|
| 59 |
+
}
|
| 60 |
+
for _, message := range textRequest.Messages {
|
| 61 |
+
palmMessage := PaLMChatMessage{
|
| 62 |
+
Content: message.Content,
|
| 63 |
+
}
|
| 64 |
+
if message.Role == "user" {
|
| 65 |
+
palmMessage.Author = "0"
|
| 66 |
+
} else {
|
| 67 |
+
palmMessage.Author = "1"
|
| 68 |
+
}
|
| 69 |
+
palmRequest.Prompt.Messages = append(palmRequest.Prompt.Messages, palmMessage)
|
| 70 |
+
}
|
| 71 |
+
return &palmRequest
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func responsePaLM2OpenAI(response *PaLMChatResponse) *OpenAITextResponse {
|
| 75 |
+
fullTextResponse := OpenAITextResponse{
|
| 76 |
+
Choices: make([]OpenAITextResponseChoice, 0, len(response.Candidates)),
|
| 77 |
+
}
|
| 78 |
+
for i, candidate := range response.Candidates {
|
| 79 |
+
choice := OpenAITextResponseChoice{
|
| 80 |
+
Index: i,
|
| 81 |
+
Message: Message{
|
| 82 |
+
Role: "assistant",
|
| 83 |
+
Content: candidate.Content,
|
| 84 |
+
},
|
| 85 |
+
FinishReason: "stop",
|
| 86 |
+
}
|
| 87 |
+
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
| 88 |
+
}
|
| 89 |
+
return &fullTextResponse
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *ChatCompletionsStreamResponse {
|
| 93 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 94 |
+
if len(palmResponse.Candidates) > 0 {
|
| 95 |
+
choice.Delta.Content = palmResponse.Candidates[0].Content
|
| 96 |
+
}
|
| 97 |
+
choice.FinishReason = &stopFinishReason
|
| 98 |
+
var response ChatCompletionsStreamResponse
|
| 99 |
+
response.Object = "chat.completion.chunk"
|
| 100 |
+
response.Model = "palm2"
|
| 101 |
+
response.Choices = []ChatCompletionsStreamResponseChoice{choice}
|
| 102 |
+
return &response
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
func palmStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
|
| 106 |
+
responseText := ""
|
| 107 |
+
responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
|
| 108 |
+
createdTime := common.GetTimestamp()
|
| 109 |
+
dataChan := make(chan string)
|
| 110 |
+
stopChan := make(chan bool)
|
| 111 |
+
go func() {
|
| 112 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 113 |
+
if err != nil {
|
| 114 |
+
common.SysError("error reading stream response: " + err.Error())
|
| 115 |
+
stopChan <- true
|
| 116 |
+
return
|
| 117 |
+
}
|
| 118 |
+
err = resp.Body.Close()
|
| 119 |
+
if err != nil {
|
| 120 |
+
common.SysError("error closing stream response: " + err.Error())
|
| 121 |
+
stopChan <- true
|
| 122 |
+
return
|
| 123 |
+
}
|
| 124 |
+
var palmResponse PaLMChatResponse
|
| 125 |
+
err = json.Unmarshal(responseBody, &palmResponse)
|
| 126 |
+
if err != nil {
|
| 127 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 128 |
+
stopChan <- true
|
| 129 |
+
return
|
| 130 |
+
}
|
| 131 |
+
fullTextResponse := streamResponsePaLM2OpenAI(&palmResponse)
|
| 132 |
+
fullTextResponse.Id = responseId
|
| 133 |
+
fullTextResponse.Created = createdTime
|
| 134 |
+
if len(palmResponse.Candidates) > 0 {
|
| 135 |
+
responseText = palmResponse.Candidates[0].Content
|
| 136 |
+
}
|
| 137 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 138 |
+
if err != nil {
|
| 139 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 140 |
+
stopChan <- true
|
| 141 |
+
return
|
| 142 |
+
}
|
| 143 |
+
dataChan <- string(jsonResponse)
|
| 144 |
+
stopChan <- true
|
| 145 |
+
}()
|
| 146 |
+
setEventStreamHeaders(c)
|
| 147 |
+
c.Stream(func(w io.Writer) bool {
|
| 148 |
+
select {
|
| 149 |
+
case data := <-dataChan:
|
| 150 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + data})
|
| 151 |
+
return true
|
| 152 |
+
case <-stopChan:
|
| 153 |
+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
| 154 |
+
return false
|
| 155 |
+
}
|
| 156 |
+
})
|
| 157 |
+
err := resp.Body.Close()
|
| 158 |
+
if err != nil {
|
| 159 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
|
| 160 |
+
}
|
| 161 |
+
return nil, responseText
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
func palmHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 165 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 166 |
+
if err != nil {
|
| 167 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 168 |
+
}
|
| 169 |
+
err = resp.Body.Close()
|
| 170 |
+
if err != nil {
|
| 171 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 172 |
+
}
|
| 173 |
+
var palmResponse PaLMChatResponse
|
| 174 |
+
err = json.Unmarshal(responseBody, &palmResponse)
|
| 175 |
+
if err != nil {
|
| 176 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 177 |
+
}
|
| 178 |
+
if palmResponse.Error.Code != 0 || len(palmResponse.Candidates) == 0 {
|
| 179 |
+
return &OpenAIErrorWithStatusCode{
|
| 180 |
+
OpenAIError: OpenAIError{
|
| 181 |
+
Message: palmResponse.Error.Message,
|
| 182 |
+
Type: palmResponse.Error.Status,
|
| 183 |
+
Param: "",
|
| 184 |
+
Code: palmResponse.Error.Code,
|
| 185 |
+
},
|
| 186 |
+
StatusCode: resp.StatusCode,
|
| 187 |
+
}, nil
|
| 188 |
+
}
|
| 189 |
+
fullTextResponse := responsePaLM2OpenAI(&palmResponse)
|
| 190 |
+
completionTokens := countTokenText(palmResponse.Candidates[0].Content, model)
|
| 191 |
+
usage := Usage{
|
| 192 |
+
PromptTokens: promptTokens,
|
| 193 |
+
CompletionTokens: completionTokens,
|
| 194 |
+
TotalTokens: promptTokens + completionTokens,
|
| 195 |
+
}
|
| 196 |
+
fullTextResponse.Usage = usage
|
| 197 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 198 |
+
if err != nil {
|
| 199 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 200 |
+
}
|
| 201 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 202 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 203 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 204 |
+
return nil, &usage
|
| 205 |
+
}
|
controller/relay-text.go
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
"io"
|
| 10 |
+
"net/http"
|
| 11 |
+
"one-api/common"
|
| 12 |
+
"one-api/model"
|
| 13 |
+
"strings"
|
| 14 |
+
"time"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
const (
|
| 18 |
+
APITypeOpenAI = iota
|
| 19 |
+
APITypeClaude
|
| 20 |
+
APITypePaLM
|
| 21 |
+
APITypeBaidu
|
| 22 |
+
APITypeZhipu
|
| 23 |
+
APITypeAli
|
| 24 |
+
APITypeXunfei
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
var httpClient *http.Client
|
| 28 |
+
var impatientHTTPClient *http.Client
|
| 29 |
+
|
| 30 |
+
func init() {
|
| 31 |
+
httpClient = &http.Client{}
|
| 32 |
+
impatientHTTPClient = &http.Client{
|
| 33 |
+
Timeout: 5 * time.Second,
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
| 38 |
+
channelType := c.GetInt("channel")
|
| 39 |
+
tokenId := c.GetInt("token_id")
|
| 40 |
+
userId := c.GetInt("id")
|
| 41 |
+
consumeQuota := c.GetBool("consume_quota")
|
| 42 |
+
group := c.GetString("group")
|
| 43 |
+
var textRequest GeneralOpenAIRequest
|
| 44 |
+
if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM {
|
| 45 |
+
err := common.UnmarshalBodyReusable(c, &textRequest)
|
| 46 |
+
if err != nil {
|
| 47 |
+
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
if relayMode == RelayModeModerations && textRequest.Model == "" {
|
| 51 |
+
textRequest.Model = "text-moderation-latest"
|
| 52 |
+
}
|
| 53 |
+
if relayMode == RelayModeEmbeddings && textRequest.Model == "" {
|
| 54 |
+
textRequest.Model = c.Param("model")
|
| 55 |
+
}
|
| 56 |
+
// request validation
|
| 57 |
+
if textRequest.Model == "" {
|
| 58 |
+
return errorWrapper(errors.New("model is required"), "required_field_missing", http.StatusBadRequest)
|
| 59 |
+
}
|
| 60 |
+
switch relayMode {
|
| 61 |
+
case RelayModeCompletions:
|
| 62 |
+
if textRequest.Prompt == "" {
|
| 63 |
+
return errorWrapper(errors.New("field prompt is required"), "required_field_missing", http.StatusBadRequest)
|
| 64 |
+
}
|
| 65 |
+
case RelayModeChatCompletions:
|
| 66 |
+
if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
|
| 67 |
+
return errorWrapper(errors.New("field messages is required"), "required_field_missing", http.StatusBadRequest)
|
| 68 |
+
}
|
| 69 |
+
case RelayModeEmbeddings:
|
| 70 |
+
case RelayModeModerations:
|
| 71 |
+
if textRequest.Input == "" {
|
| 72 |
+
return errorWrapper(errors.New("field input is required"), "required_field_missing", http.StatusBadRequest)
|
| 73 |
+
}
|
| 74 |
+
case RelayModeEdits:
|
| 75 |
+
if textRequest.Instruction == "" {
|
| 76 |
+
return errorWrapper(errors.New("field instruction is required"), "required_field_missing", http.StatusBadRequest)
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
// map model name
|
| 80 |
+
modelMapping := c.GetString("model_mapping")
|
| 81 |
+
isModelMapped := false
|
| 82 |
+
if modelMapping != "" && modelMapping != "{}" {
|
| 83 |
+
modelMap := make(map[string]string)
|
| 84 |
+
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
| 85 |
+
if err != nil {
|
| 86 |
+
return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
| 87 |
+
}
|
| 88 |
+
if modelMap[textRequest.Model] != "" {
|
| 89 |
+
textRequest.Model = modelMap[textRequest.Model]
|
| 90 |
+
isModelMapped = true
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
apiType := APITypeOpenAI
|
| 94 |
+
switch channelType {
|
| 95 |
+
case common.ChannelTypeAnthropic:
|
| 96 |
+
apiType = APITypeClaude
|
| 97 |
+
case common.ChannelTypeBaidu:
|
| 98 |
+
apiType = APITypeBaidu
|
| 99 |
+
case common.ChannelTypePaLM:
|
| 100 |
+
apiType = APITypePaLM
|
| 101 |
+
case common.ChannelTypeZhipu:
|
| 102 |
+
apiType = APITypeZhipu
|
| 103 |
+
case common.ChannelTypeAli:
|
| 104 |
+
apiType = APITypeAli
|
| 105 |
+
case common.ChannelTypeXunfei:
|
| 106 |
+
apiType = APITypeXunfei
|
| 107 |
+
}
|
| 108 |
+
baseURL := common.ChannelBaseURLs[channelType]
|
| 109 |
+
requestURL := c.Request.URL.String()
|
| 110 |
+
if c.GetString("base_url") != "" {
|
| 111 |
+
baseURL = c.GetString("base_url")
|
| 112 |
+
}
|
| 113 |
+
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
| 114 |
+
switch apiType {
|
| 115 |
+
case APITypeOpenAI:
|
| 116 |
+
if channelType == common.ChannelTypeAzure {
|
| 117 |
+
// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
|
| 118 |
+
query := c.Request.URL.Query()
|
| 119 |
+
apiVersion := query.Get("api-version")
|
| 120 |
+
if apiVersion == "" {
|
| 121 |
+
apiVersion = c.GetString("api_version")
|
| 122 |
+
}
|
| 123 |
+
requestURL := strings.Split(requestURL, "?")[0]
|
| 124 |
+
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
|
| 125 |
+
baseURL = c.GetString("base_url")
|
| 126 |
+
task := strings.TrimPrefix(requestURL, "/v1/")
|
| 127 |
+
model_ := textRequest.Model
|
| 128 |
+
model_ = strings.Replace(model_, ".", "", -1)
|
| 129 |
+
// https://github.com/songquanpeng/one-api/issues/67
|
| 130 |
+
model_ = strings.TrimSuffix(model_, "-0301")
|
| 131 |
+
model_ = strings.TrimSuffix(model_, "-0314")
|
| 132 |
+
model_ = strings.TrimSuffix(model_, "-0613")
|
| 133 |
+
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
|
| 134 |
+
}
|
| 135 |
+
case APITypeClaude:
|
| 136 |
+
fullRequestURL = "https://api.anthropic.com/v1/complete"
|
| 137 |
+
if baseURL != "" {
|
| 138 |
+
fullRequestURL = fmt.Sprintf("%s/v1/complete", baseURL)
|
| 139 |
+
}
|
| 140 |
+
case APITypeBaidu:
|
| 141 |
+
switch textRequest.Model {
|
| 142 |
+
case "ERNIE-Bot":
|
| 143 |
+
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions"
|
| 144 |
+
case "ERNIE-Bot-turbo":
|
| 145 |
+
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant"
|
| 146 |
+
case "BLOOMZ-7B":
|
| 147 |
+
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/bloomz_7b1"
|
| 148 |
+
case "Embedding-V1":
|
| 149 |
+
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1"
|
| 150 |
+
}
|
| 151 |
+
apiKey := c.Request.Header.Get("Authorization")
|
| 152 |
+
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
| 153 |
+
var err error
|
| 154 |
+
if apiKey, err = getBaiduAccessToken(apiKey); err != nil {
|
| 155 |
+
return errorWrapper(err, "invalid_baidu_config", http.StatusInternalServerError)
|
| 156 |
+
}
|
| 157 |
+
fullRequestURL += "?access_token=" + apiKey
|
| 158 |
+
case APITypePaLM:
|
| 159 |
+
fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage"
|
| 160 |
+
if baseURL != "" {
|
| 161 |
+
fullRequestURL = fmt.Sprintf("%s/v1beta2/models/chat-bison-001:generateMessage", baseURL)
|
| 162 |
+
}
|
| 163 |
+
apiKey := c.Request.Header.Get("Authorization")
|
| 164 |
+
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
| 165 |
+
fullRequestURL += "?key=" + apiKey
|
| 166 |
+
case APITypeZhipu:
|
| 167 |
+
method := "invoke"
|
| 168 |
+
if textRequest.Stream {
|
| 169 |
+
method = "sse-invoke"
|
| 170 |
+
}
|
| 171 |
+
fullRequestURL = fmt.Sprintf("https://open.bigmodel.cn/api/paas/v3/model-api/%s/%s", textRequest.Model, method)
|
| 172 |
+
case APITypeAli:
|
| 173 |
+
fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
|
| 174 |
+
}
|
| 175 |
+
var promptTokens int
|
| 176 |
+
var completionTokens int
|
| 177 |
+
switch relayMode {
|
| 178 |
+
case RelayModeChatCompletions:
|
| 179 |
+
promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model)
|
| 180 |
+
case RelayModeCompletions:
|
| 181 |
+
promptTokens = countTokenInput(textRequest.Prompt, textRequest.Model)
|
| 182 |
+
case RelayModeModerations:
|
| 183 |
+
promptTokens = countTokenInput(textRequest.Input, textRequest.Model)
|
| 184 |
+
}
|
| 185 |
+
preConsumedTokens := common.PreConsumedQuota
|
| 186 |
+
if textRequest.MaxTokens != 0 {
|
| 187 |
+
preConsumedTokens = promptTokens + textRequest.MaxTokens
|
| 188 |
+
}
|
| 189 |
+
modelRatio := common.GetModelRatio(textRequest.Model)
|
| 190 |
+
groupRatio := common.GetGroupRatio(group)
|
| 191 |
+
ratio := modelRatio * groupRatio
|
| 192 |
+
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
| 193 |
+
userQuota, err := model.CacheGetUserQuota(userId)
|
| 194 |
+
if err != nil {
|
| 195 |
+
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
| 196 |
+
}
|
| 197 |
+
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
| 198 |
+
if err != nil {
|
| 199 |
+
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
| 200 |
+
}
|
| 201 |
+
if userQuota > 100*preConsumedQuota {
|
| 202 |
+
// in this case, we do not pre-consume quota
|
| 203 |
+
// because the user has enough quota
|
| 204 |
+
preConsumedQuota = 0
|
| 205 |
+
}
|
| 206 |
+
if consumeQuota && preConsumedQuota > 0 {
|
| 207 |
+
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
|
| 208 |
+
if err != nil {
|
| 209 |
+
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
var requestBody io.Reader
|
| 213 |
+
if isModelMapped {
|
| 214 |
+
jsonStr, err := json.Marshal(textRequest)
|
| 215 |
+
if err != nil {
|
| 216 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 217 |
+
}
|
| 218 |
+
requestBody = bytes.NewBuffer(jsonStr)
|
| 219 |
+
} else {
|
| 220 |
+
requestBody = c.Request.Body
|
| 221 |
+
}
|
| 222 |
+
switch apiType {
|
| 223 |
+
case APITypeClaude:
|
| 224 |
+
claudeRequest := requestOpenAI2Claude(textRequest)
|
| 225 |
+
jsonStr, err := json.Marshal(claudeRequest)
|
| 226 |
+
if err != nil {
|
| 227 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 228 |
+
}
|
| 229 |
+
requestBody = bytes.NewBuffer(jsonStr)
|
| 230 |
+
case APITypeBaidu:
|
| 231 |
+
var jsonData []byte
|
| 232 |
+
var err error
|
| 233 |
+
switch relayMode {
|
| 234 |
+
case RelayModeEmbeddings:
|
| 235 |
+
baiduEmbeddingRequest := embeddingRequestOpenAI2Baidu(textRequest)
|
| 236 |
+
jsonData, err = json.Marshal(baiduEmbeddingRequest)
|
| 237 |
+
default:
|
| 238 |
+
baiduRequest := requestOpenAI2Baidu(textRequest)
|
| 239 |
+
jsonData, err = json.Marshal(baiduRequest)
|
| 240 |
+
}
|
| 241 |
+
if err != nil {
|
| 242 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 243 |
+
}
|
| 244 |
+
requestBody = bytes.NewBuffer(jsonData)
|
| 245 |
+
case APITypePaLM:
|
| 246 |
+
palmRequest := requestOpenAI2PaLM(textRequest)
|
| 247 |
+
jsonStr, err := json.Marshal(palmRequest)
|
| 248 |
+
if err != nil {
|
| 249 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 250 |
+
}
|
| 251 |
+
requestBody = bytes.NewBuffer(jsonStr)
|
| 252 |
+
case APITypeZhipu:
|
| 253 |
+
zhipuRequest := requestOpenAI2Zhipu(textRequest)
|
| 254 |
+
jsonStr, err := json.Marshal(zhipuRequest)
|
| 255 |
+
if err != nil {
|
| 256 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 257 |
+
}
|
| 258 |
+
requestBody = bytes.NewBuffer(jsonStr)
|
| 259 |
+
case APITypeAli:
|
| 260 |
+
aliRequest := requestOpenAI2Ali(textRequest)
|
| 261 |
+
jsonStr, err := json.Marshal(aliRequest)
|
| 262 |
+
if err != nil {
|
| 263 |
+
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
| 264 |
+
}
|
| 265 |
+
requestBody = bytes.NewBuffer(jsonStr)
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
var req *http.Request
|
| 269 |
+
var resp *http.Response
|
| 270 |
+
isStream := textRequest.Stream
|
| 271 |
+
|
| 272 |
+
if apiType != APITypeXunfei { // cause xunfei use websocket
|
| 273 |
+
req, err = http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
| 274 |
+
if err != nil {
|
| 275 |
+
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
| 276 |
+
}
|
| 277 |
+
apiKey := c.Request.Header.Get("Authorization")
|
| 278 |
+
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
| 279 |
+
switch apiType {
|
| 280 |
+
case APITypeOpenAI:
|
| 281 |
+
if channelType == common.ChannelTypeAzure {
|
| 282 |
+
req.Header.Set("api-key", apiKey)
|
| 283 |
+
} else {
|
| 284 |
+
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
| 285 |
+
if channelType == common.ChannelTypeOpenRouter {
|
| 286 |
+
req.Header.Set("HTTP-Referer", "https://github.com/songquanpeng/one-api")
|
| 287 |
+
req.Header.Set("X-Title", "One API")
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
case APITypeClaude:
|
| 291 |
+
req.Header.Set("x-api-key", apiKey)
|
| 292 |
+
anthropicVersion := c.Request.Header.Get("anthropic-version")
|
| 293 |
+
if anthropicVersion == "" {
|
| 294 |
+
anthropicVersion = "2023-06-01"
|
| 295 |
+
}
|
| 296 |
+
req.Header.Set("anthropic-version", anthropicVersion)
|
| 297 |
+
case APITypeZhipu:
|
| 298 |
+
token := getZhipuToken(apiKey)
|
| 299 |
+
req.Header.Set("Authorization", token)
|
| 300 |
+
case APITypeAli:
|
| 301 |
+
req.Header.Set("Authorization", "Bearer "+apiKey)
|
| 302 |
+
if textRequest.Stream {
|
| 303 |
+
req.Header.Set("X-DashScope-SSE", "enable")
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
| 307 |
+
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
| 308 |
+
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
| 309 |
+
resp, err = httpClient.Do(req)
|
| 310 |
+
if err != nil {
|
| 311 |
+
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
| 312 |
+
}
|
| 313 |
+
err = req.Body.Close()
|
| 314 |
+
if err != nil {
|
| 315 |
+
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
| 316 |
+
}
|
| 317 |
+
err = c.Request.Body.Close()
|
| 318 |
+
if err != nil {
|
| 319 |
+
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
| 320 |
+
}
|
| 321 |
+
isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
| 322 |
+
|
| 323 |
+
if resp.StatusCode != http.StatusOK {
|
| 324 |
+
return relayErrorHandler(resp)
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
var textResponse TextResponse
|
| 329 |
+
tokenName := c.GetString("token_name")
|
| 330 |
+
channelId := c.GetInt("channel_id")
|
| 331 |
+
|
| 332 |
+
defer func() {
|
| 333 |
+
// c.Writer.Flush()
|
| 334 |
+
go func() {
|
| 335 |
+
if consumeQuota {
|
| 336 |
+
quota := 0
|
| 337 |
+
completionRatio := common.GetCompletionRatio(textRequest.Model)
|
| 338 |
+
promptTokens = textResponse.Usage.PromptTokens
|
| 339 |
+
completionTokens = textResponse.Usage.CompletionTokens
|
| 340 |
+
|
| 341 |
+
quota = promptTokens + int(float64(completionTokens)*completionRatio)
|
| 342 |
+
quota = int(float64(quota) * ratio)
|
| 343 |
+
if ratio != 0 && quota <= 0 {
|
| 344 |
+
quota = 1
|
| 345 |
+
}
|
| 346 |
+
totalTokens := promptTokens + completionTokens
|
| 347 |
+
if totalTokens == 0 {
|
| 348 |
+
// in this case, must be some error happened
|
| 349 |
+
// we cannot just return, because we may have to return the pre-consumed quota
|
| 350 |
+
quota = 0
|
| 351 |
+
}
|
| 352 |
+
quotaDelta := quota - preConsumedQuota
|
| 353 |
+
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
|
| 354 |
+
if err != nil {
|
| 355 |
+
common.SysError("error consuming token remain quota: " + err.Error())
|
| 356 |
+
}
|
| 357 |
+
err = model.CacheUpdateUserQuota(userId)
|
| 358 |
+
if err != nil {
|
| 359 |
+
common.SysError("error update user quota cache: " + err.Error())
|
| 360 |
+
}
|
| 361 |
+
if quota != 0 {
|
| 362 |
+
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
| 363 |
+
model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent)
|
| 364 |
+
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
| 365 |
+
|
| 366 |
+
model.UpdateChannelUsedQuota(channelId, quota)
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
}()
|
| 370 |
+
}()
|
| 371 |
+
switch apiType {
|
| 372 |
+
case APITypeOpenAI:
|
| 373 |
+
if isStream {
|
| 374 |
+
err, responseText := openaiStreamHandler(c, resp, relayMode)
|
| 375 |
+
if err != nil {
|
| 376 |
+
return err
|
| 377 |
+
}
|
| 378 |
+
textResponse.Usage.PromptTokens = promptTokens
|
| 379 |
+
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
| 380 |
+
return nil
|
| 381 |
+
} else {
|
| 382 |
+
err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model)
|
| 383 |
+
if err != nil {
|
| 384 |
+
return err
|
| 385 |
+
}
|
| 386 |
+
if usage != nil {
|
| 387 |
+
textResponse.Usage = *usage
|
| 388 |
+
}
|
| 389 |
+
return nil
|
| 390 |
+
}
|
| 391 |
+
case APITypeClaude:
|
| 392 |
+
if isStream {
|
| 393 |
+
err, responseText := claudeStreamHandler(c, resp)
|
| 394 |
+
if err != nil {
|
| 395 |
+
return err
|
| 396 |
+
}
|
| 397 |
+
textResponse.Usage.PromptTokens = promptTokens
|
| 398 |
+
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
| 399 |
+
return nil
|
| 400 |
+
} else {
|
| 401 |
+
err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model)
|
| 402 |
+
if err != nil {
|
| 403 |
+
return err
|
| 404 |
+
}
|
| 405 |
+
if usage != nil {
|
| 406 |
+
textResponse.Usage = *usage
|
| 407 |
+
}
|
| 408 |
+
return nil
|
| 409 |
+
}
|
| 410 |
+
case APITypeBaidu:
|
| 411 |
+
if isStream {
|
| 412 |
+
err, usage := baiduStreamHandler(c, resp)
|
| 413 |
+
if err != nil {
|
| 414 |
+
return err
|
| 415 |
+
}
|
| 416 |
+
if usage != nil {
|
| 417 |
+
textResponse.Usage = *usage
|
| 418 |
+
}
|
| 419 |
+
return nil
|
| 420 |
+
} else {
|
| 421 |
+
var err *OpenAIErrorWithStatusCode
|
| 422 |
+
var usage *Usage
|
| 423 |
+
switch relayMode {
|
| 424 |
+
case RelayModeEmbeddings:
|
| 425 |
+
err, usage = baiduEmbeddingHandler(c, resp)
|
| 426 |
+
default:
|
| 427 |
+
err, usage = baiduHandler(c, resp)
|
| 428 |
+
}
|
| 429 |
+
if err != nil {
|
| 430 |
+
return err
|
| 431 |
+
}
|
| 432 |
+
if usage != nil {
|
| 433 |
+
textResponse.Usage = *usage
|
| 434 |
+
}
|
| 435 |
+
return nil
|
| 436 |
+
}
|
| 437 |
+
case APITypePaLM:
|
| 438 |
+
if textRequest.Stream { // PaLM2 API does not support stream
|
| 439 |
+
err, responseText := palmStreamHandler(c, resp)
|
| 440 |
+
if err != nil {
|
| 441 |
+
return err
|
| 442 |
+
}
|
| 443 |
+
textResponse.Usage.PromptTokens = promptTokens
|
| 444 |
+
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
| 445 |
+
return nil
|
| 446 |
+
} else {
|
| 447 |
+
err, usage := palmHandler(c, resp, promptTokens, textRequest.Model)
|
| 448 |
+
if err != nil {
|
| 449 |
+
return err
|
| 450 |
+
}
|
| 451 |
+
if usage != nil {
|
| 452 |
+
textResponse.Usage = *usage
|
| 453 |
+
}
|
| 454 |
+
return nil
|
| 455 |
+
}
|
| 456 |
+
case APITypeZhipu:
|
| 457 |
+
if isStream {
|
| 458 |
+
err, usage := zhipuStreamHandler(c, resp)
|
| 459 |
+
if err != nil {
|
| 460 |
+
return err
|
| 461 |
+
}
|
| 462 |
+
if usage != nil {
|
| 463 |
+
textResponse.Usage = *usage
|
| 464 |
+
}
|
| 465 |
+
// zhipu's API does not return prompt tokens & completion tokens
|
| 466 |
+
textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens
|
| 467 |
+
return nil
|
| 468 |
+
} else {
|
| 469 |
+
err, usage := zhipuHandler(c, resp)
|
| 470 |
+
if err != nil {
|
| 471 |
+
return err
|
| 472 |
+
}
|
| 473 |
+
if usage != nil {
|
| 474 |
+
textResponse.Usage = *usage
|
| 475 |
+
}
|
| 476 |
+
// zhipu's API does not return prompt tokens & completion tokens
|
| 477 |
+
textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens
|
| 478 |
+
return nil
|
| 479 |
+
}
|
| 480 |
+
case APITypeAli:
|
| 481 |
+
if isStream {
|
| 482 |
+
err, usage := aliStreamHandler(c, resp)
|
| 483 |
+
if err != nil {
|
| 484 |
+
return err
|
| 485 |
+
}
|
| 486 |
+
if usage != nil {
|
| 487 |
+
textResponse.Usage = *usage
|
| 488 |
+
}
|
| 489 |
+
return nil
|
| 490 |
+
} else {
|
| 491 |
+
err, usage := aliHandler(c, resp)
|
| 492 |
+
if err != nil {
|
| 493 |
+
return err
|
| 494 |
+
}
|
| 495 |
+
if usage != nil {
|
| 496 |
+
textResponse.Usage = *usage
|
| 497 |
+
}
|
| 498 |
+
return nil
|
| 499 |
+
}
|
| 500 |
+
case APITypeXunfei:
|
| 501 |
+
if isStream {
|
| 502 |
+
auth := c.Request.Header.Get("Authorization")
|
| 503 |
+
auth = strings.TrimPrefix(auth, "Bearer ")
|
| 504 |
+
splits := strings.Split(auth, "|")
|
| 505 |
+
if len(splits) != 3 {
|
| 506 |
+
return errorWrapper(errors.New("invalid auth"), "invalid_auth", http.StatusBadRequest)
|
| 507 |
+
}
|
| 508 |
+
err, usage := xunfeiStreamHandler(c, textRequest, splits[0], splits[1], splits[2])
|
| 509 |
+
if err != nil {
|
| 510 |
+
return err
|
| 511 |
+
}
|
| 512 |
+
if usage != nil {
|
| 513 |
+
textResponse.Usage = *usage
|
| 514 |
+
}
|
| 515 |
+
return nil
|
| 516 |
+
} else {
|
| 517 |
+
return errorWrapper(errors.New("xunfei api does not support non-stream mode"), "invalid_api_type", http.StatusBadRequest)
|
| 518 |
+
}
|
| 519 |
+
default:
|
| 520 |
+
return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError)
|
| 521 |
+
}
|
| 522 |
+
}
|
controller/relay-utils.go
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
"github.com/pkoukk/tiktoken-go"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
"one-api/common"
|
| 11 |
+
"strconv"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var stopFinishReason = "stop"
|
| 15 |
+
|
| 16 |
+
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
| 17 |
+
|
| 18 |
+
func InitTokenEncoders() {
|
| 19 |
+
common.SysLog("initializing token encoders")
|
| 20 |
+
fallbackTokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
|
| 21 |
+
if err != nil {
|
| 22 |
+
common.FatalLog(fmt.Sprintf("failed to get fallback token encoder: %s", err.Error()))
|
| 23 |
+
}
|
| 24 |
+
for model, _ := range common.ModelRatio {
|
| 25 |
+
tokenEncoder, err := tiktoken.EncodingForModel(model)
|
| 26 |
+
if err != nil {
|
| 27 |
+
common.SysError(fmt.Sprintf("using fallback encoder for model %s", model))
|
| 28 |
+
tokenEncoderMap[model] = fallbackTokenEncoder
|
| 29 |
+
continue
|
| 30 |
+
}
|
| 31 |
+
tokenEncoderMap[model] = tokenEncoder
|
| 32 |
+
}
|
| 33 |
+
common.SysLog("token encoders initialized")
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func getTokenEncoder(model string) *tiktoken.Tiktoken {
|
| 37 |
+
if tokenEncoder, ok := tokenEncoderMap[model]; ok {
|
| 38 |
+
return tokenEncoder
|
| 39 |
+
}
|
| 40 |
+
tokenEncoder, err := tiktoken.EncodingForModel(model)
|
| 41 |
+
if err != nil {
|
| 42 |
+
common.SysError(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error()))
|
| 43 |
+
tokenEncoder, err = tiktoken.EncodingForModel("gpt-3.5-turbo")
|
| 44 |
+
if err != nil {
|
| 45 |
+
common.FatalLog(fmt.Sprintf("failed to get token encoder for model gpt-3.5-turbo: %s", err.Error()))
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
tokenEncoderMap[model] = tokenEncoder
|
| 49 |
+
return tokenEncoder
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int {
|
| 53 |
+
if common.ApproximateTokenEnabled {
|
| 54 |
+
return int(float64(len(text)) * 0.38)
|
| 55 |
+
}
|
| 56 |
+
return len(tokenEncoder.Encode(text, nil, nil))
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func countTokenMessages(messages []Message, model string) int {
|
| 60 |
+
tokenEncoder := getTokenEncoder(model)
|
| 61 |
+
// Reference:
|
| 62 |
+
// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
| 63 |
+
// https://github.com/pkoukk/tiktoken-go/issues/6
|
| 64 |
+
//
|
| 65 |
+
// Every message follows <|start|>{role/name}\n{content}<|end|>\n
|
| 66 |
+
var tokensPerMessage int
|
| 67 |
+
var tokensPerName int
|
| 68 |
+
if model == "gpt-3.5-turbo-0301" {
|
| 69 |
+
tokensPerMessage = 4
|
| 70 |
+
tokensPerName = -1 // If there's a name, the role is omitted
|
| 71 |
+
} else {
|
| 72 |
+
tokensPerMessage = 3
|
| 73 |
+
tokensPerName = 1
|
| 74 |
+
}
|
| 75 |
+
tokenNum := 0
|
| 76 |
+
for _, message := range messages {
|
| 77 |
+
tokenNum += tokensPerMessage
|
| 78 |
+
tokenNum += getTokenNum(tokenEncoder, message.Content)
|
| 79 |
+
tokenNum += getTokenNum(tokenEncoder, message.Role)
|
| 80 |
+
if message.Name != nil {
|
| 81 |
+
tokenNum += tokensPerName
|
| 82 |
+
tokenNum += getTokenNum(tokenEncoder, *message.Name)
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
tokenNum += 3 // Every reply is primed with <|start|>assistant<|message|>
|
| 86 |
+
return tokenNum
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
func countTokenInput(input any, model string) int {
|
| 90 |
+
switch input.(type) {
|
| 91 |
+
case string:
|
| 92 |
+
return countTokenText(input.(string), model)
|
| 93 |
+
case []string:
|
| 94 |
+
text := ""
|
| 95 |
+
for _, s := range input.([]string) {
|
| 96 |
+
text += s
|
| 97 |
+
}
|
| 98 |
+
return countTokenText(text, model)
|
| 99 |
+
}
|
| 100 |
+
return 0
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
func countTokenText(text string, model string) int {
|
| 104 |
+
tokenEncoder := getTokenEncoder(model)
|
| 105 |
+
return getTokenNum(tokenEncoder, text)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatusCode {
|
| 109 |
+
openAIError := OpenAIError{
|
| 110 |
+
Message: err.Error(),
|
| 111 |
+
Type: "one_api_error",
|
| 112 |
+
Code: code,
|
| 113 |
+
}
|
| 114 |
+
return &OpenAIErrorWithStatusCode{
|
| 115 |
+
OpenAIError: openAIError,
|
| 116 |
+
StatusCode: statusCode,
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
func shouldDisableChannel(err *OpenAIError, statusCode int) bool {
|
| 121 |
+
if !common.AutomaticDisableChannelEnabled {
|
| 122 |
+
return false
|
| 123 |
+
}
|
| 124 |
+
if err == nil {
|
| 125 |
+
return false
|
| 126 |
+
}
|
| 127 |
+
if statusCode == http.StatusUnauthorized {
|
| 128 |
+
return true
|
| 129 |
+
}
|
| 130 |
+
if err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
|
| 131 |
+
return true
|
| 132 |
+
}
|
| 133 |
+
return false
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
func setEventStreamHeaders(c *gin.Context) {
|
| 137 |
+
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
| 138 |
+
c.Writer.Header().Set("Cache-Control", "no-cache")
|
| 139 |
+
c.Writer.Header().Set("Connection", "keep-alive")
|
| 140 |
+
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
| 141 |
+
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
func relayErrorHandler(resp *http.Response) (openAIErrorWithStatusCode *OpenAIErrorWithStatusCode) {
|
| 145 |
+
openAIErrorWithStatusCode = &OpenAIErrorWithStatusCode{
|
| 146 |
+
StatusCode: resp.StatusCode,
|
| 147 |
+
OpenAIError: OpenAIError{
|
| 148 |
+
Message: fmt.Sprintf("bad response status code %d", resp.StatusCode),
|
| 149 |
+
Type: "one_api_error",
|
| 150 |
+
Code: "bad_response_status_code",
|
| 151 |
+
Param: strconv.Itoa(resp.StatusCode),
|
| 152 |
+
},
|
| 153 |
+
}
|
| 154 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 155 |
+
if err != nil {
|
| 156 |
+
return
|
| 157 |
+
}
|
| 158 |
+
err = resp.Body.Close()
|
| 159 |
+
if err != nil {
|
| 160 |
+
return
|
| 161 |
+
}
|
| 162 |
+
var textResponse TextResponse
|
| 163 |
+
err = json.Unmarshal(responseBody, &textResponse)
|
| 164 |
+
if err != nil {
|
| 165 |
+
return
|
| 166 |
+
}
|
| 167 |
+
openAIErrorWithStatusCode.OpenAIError = textResponse.Error
|
| 168 |
+
return
|
| 169 |
+
}
|
controller/relay-xunfei.go
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"crypto/hmac"
|
| 5 |
+
"crypto/sha256"
|
| 6 |
+
"encoding/base64"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"fmt"
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"github.com/gorilla/websocket"
|
| 11 |
+
"io"
|
| 12 |
+
"net/http"
|
| 13 |
+
"net/url"
|
| 14 |
+
"one-api/common"
|
| 15 |
+
"strings"
|
| 16 |
+
"time"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
// https://console.xfyun.cn/services/cbm
|
| 20 |
+
// https://www.xfyun.cn/doc/spark/Web.html
|
| 21 |
+
|
| 22 |
+
type XunfeiMessage struct {
|
| 23 |
+
Role string `json:"role"`
|
| 24 |
+
Content string `json:"content"`
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
type XunfeiChatRequest struct {
|
| 28 |
+
Header struct {
|
| 29 |
+
AppId string `json:"app_id"`
|
| 30 |
+
} `json:"header"`
|
| 31 |
+
Parameter struct {
|
| 32 |
+
Chat struct {
|
| 33 |
+
Domain string `json:"domain,omitempty"`
|
| 34 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 35 |
+
TopK int `json:"top_k,omitempty"`
|
| 36 |
+
MaxTokens int `json:"max_tokens,omitempty"`
|
| 37 |
+
Auditing bool `json:"auditing,omitempty"`
|
| 38 |
+
} `json:"chat"`
|
| 39 |
+
} `json:"parameter"`
|
| 40 |
+
Payload struct {
|
| 41 |
+
Message struct {
|
| 42 |
+
Text []XunfeiMessage `json:"text"`
|
| 43 |
+
} `json:"message"`
|
| 44 |
+
} `json:"payload"`
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
type XunfeiChatResponseTextItem struct {
|
| 48 |
+
Content string `json:"content"`
|
| 49 |
+
Role string `json:"role"`
|
| 50 |
+
Index int `json:"index"`
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
type XunfeiChatResponse struct {
|
| 54 |
+
Header struct {
|
| 55 |
+
Code int `json:"code"`
|
| 56 |
+
Message string `json:"message"`
|
| 57 |
+
Sid string `json:"sid"`
|
| 58 |
+
Status int `json:"status"`
|
| 59 |
+
} `json:"header"`
|
| 60 |
+
Payload struct {
|
| 61 |
+
Choices struct {
|
| 62 |
+
Status int `json:"status"`
|
| 63 |
+
Seq int `json:"seq"`
|
| 64 |
+
Text []XunfeiChatResponseTextItem `json:"text"`
|
| 65 |
+
} `json:"choices"`
|
| 66 |
+
Usage struct {
|
| 67 |
+
//Text struct {
|
| 68 |
+
// QuestionTokens string `json:"question_tokens"`
|
| 69 |
+
// PromptTokens string `json:"prompt_tokens"`
|
| 70 |
+
// CompletionTokens string `json:"completion_tokens"`
|
| 71 |
+
// TotalTokens string `json:"total_tokens"`
|
| 72 |
+
//} `json:"text"`
|
| 73 |
+
Text Usage `json:"text"`
|
| 74 |
+
} `json:"usage"`
|
| 75 |
+
} `json:"payload"`
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string, domain string) *XunfeiChatRequest {
|
| 79 |
+
messages := make([]XunfeiMessage, 0, len(request.Messages))
|
| 80 |
+
for _, message := range request.Messages {
|
| 81 |
+
if message.Role == "system" {
|
| 82 |
+
messages = append(messages, XunfeiMessage{
|
| 83 |
+
Role: "user",
|
| 84 |
+
Content: message.Content,
|
| 85 |
+
})
|
| 86 |
+
messages = append(messages, XunfeiMessage{
|
| 87 |
+
Role: "assistant",
|
| 88 |
+
Content: "Okay",
|
| 89 |
+
})
|
| 90 |
+
} else {
|
| 91 |
+
messages = append(messages, XunfeiMessage{
|
| 92 |
+
Role: message.Role,
|
| 93 |
+
Content: message.Content,
|
| 94 |
+
})
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
xunfeiRequest := XunfeiChatRequest{}
|
| 98 |
+
xunfeiRequest.Header.AppId = xunfeiAppId
|
| 99 |
+
xunfeiRequest.Parameter.Chat.Domain = domain
|
| 100 |
+
xunfeiRequest.Parameter.Chat.Temperature = request.Temperature
|
| 101 |
+
xunfeiRequest.Parameter.Chat.TopK = request.N
|
| 102 |
+
xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens
|
| 103 |
+
xunfeiRequest.Payload.Message.Text = messages
|
| 104 |
+
return &xunfeiRequest
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse {
|
| 108 |
+
if len(response.Payload.Choices.Text) == 0 {
|
| 109 |
+
response.Payload.Choices.Text = []XunfeiChatResponseTextItem{
|
| 110 |
+
{
|
| 111 |
+
Content: "",
|
| 112 |
+
},
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
choice := OpenAITextResponseChoice{
|
| 116 |
+
Index: 0,
|
| 117 |
+
Message: Message{
|
| 118 |
+
Role: "assistant",
|
| 119 |
+
Content: response.Payload.Choices.Text[0].Content,
|
| 120 |
+
},
|
| 121 |
+
}
|
| 122 |
+
fullTextResponse := OpenAITextResponse{
|
| 123 |
+
Object: "chat.completion",
|
| 124 |
+
Created: common.GetTimestamp(),
|
| 125 |
+
Choices: []OpenAITextResponseChoice{choice},
|
| 126 |
+
Usage: response.Payload.Usage.Text,
|
| 127 |
+
}
|
| 128 |
+
return &fullTextResponse
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatCompletionsStreamResponse {
|
| 132 |
+
if len(xunfeiResponse.Payload.Choices.Text) == 0 {
|
| 133 |
+
xunfeiResponse.Payload.Choices.Text = []XunfeiChatResponseTextItem{
|
| 134 |
+
{
|
| 135 |
+
Content: "",
|
| 136 |
+
},
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 140 |
+
choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content
|
| 141 |
+
if xunfeiResponse.Payload.Choices.Status == 2 {
|
| 142 |
+
choice.FinishReason = &stopFinishReason
|
| 143 |
+
}
|
| 144 |
+
response := ChatCompletionsStreamResponse{
|
| 145 |
+
Object: "chat.completion.chunk",
|
| 146 |
+
Created: common.GetTimestamp(),
|
| 147 |
+
Model: "SparkDesk",
|
| 148 |
+
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
| 149 |
+
}
|
| 150 |
+
return &response
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string {
|
| 154 |
+
HmacWithShaToBase64 := func(algorithm, data, key string) string {
|
| 155 |
+
mac := hmac.New(sha256.New, []byte(key))
|
| 156 |
+
mac.Write([]byte(data))
|
| 157 |
+
encodeData := mac.Sum(nil)
|
| 158 |
+
return base64.StdEncoding.EncodeToString(encodeData)
|
| 159 |
+
}
|
| 160 |
+
ul, err := url.Parse(hostUrl)
|
| 161 |
+
if err != nil {
|
| 162 |
+
fmt.Println(err)
|
| 163 |
+
}
|
| 164 |
+
date := time.Now().UTC().Format(time.RFC1123)
|
| 165 |
+
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
|
| 166 |
+
sign := strings.Join(signString, "\n")
|
| 167 |
+
sha := HmacWithShaToBase64("hmac-sha256", sign, apiSecret)
|
| 168 |
+
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
|
| 169 |
+
"hmac-sha256", "host date request-line", sha)
|
| 170 |
+
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
|
| 171 |
+
v := url.Values{}
|
| 172 |
+
v.Add("host", ul.Host)
|
| 173 |
+
v.Add("date", date)
|
| 174 |
+
v.Add("authorization", authorization)
|
| 175 |
+
callUrl := hostUrl + "?" + v.Encode()
|
| 176 |
+
return callUrl
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 180 |
+
var usage Usage
|
| 181 |
+
query := c.Request.URL.Query()
|
| 182 |
+
apiVersion := query.Get("api-version")
|
| 183 |
+
if apiVersion == "" {
|
| 184 |
+
apiVersion = c.GetString("api_version")
|
| 185 |
+
}
|
| 186 |
+
if apiVersion == "" {
|
| 187 |
+
apiVersion = "v1.1"
|
| 188 |
+
common.SysLog("api_version not found, use default: " + apiVersion)
|
| 189 |
+
}
|
| 190 |
+
domain := "general"
|
| 191 |
+
if apiVersion == "v2.1" {
|
| 192 |
+
domain = "generalv2"
|
| 193 |
+
}
|
| 194 |
+
hostUrl := fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion)
|
| 195 |
+
d := websocket.Dialer{
|
| 196 |
+
HandshakeTimeout: 5 * time.Second,
|
| 197 |
+
}
|
| 198 |
+
conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil)
|
| 199 |
+
if err != nil || resp.StatusCode != 101 {
|
| 200 |
+
return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil
|
| 201 |
+
}
|
| 202 |
+
data := requestOpenAI2Xunfei(textRequest, appId, domain)
|
| 203 |
+
err = conn.WriteJSON(data)
|
| 204 |
+
if err != nil {
|
| 205 |
+
return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil
|
| 206 |
+
}
|
| 207 |
+
dataChan := make(chan XunfeiChatResponse)
|
| 208 |
+
stopChan := make(chan bool)
|
| 209 |
+
go func() {
|
| 210 |
+
for {
|
| 211 |
+
_, msg, err := conn.ReadMessage()
|
| 212 |
+
if err != nil {
|
| 213 |
+
common.SysError("error reading stream response: " + err.Error())
|
| 214 |
+
break
|
| 215 |
+
}
|
| 216 |
+
var response XunfeiChatResponse
|
| 217 |
+
err = json.Unmarshal(msg, &response)
|
| 218 |
+
if err != nil {
|
| 219 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 220 |
+
break
|
| 221 |
+
}
|
| 222 |
+
dataChan <- response
|
| 223 |
+
if response.Payload.Choices.Status == 2 {
|
| 224 |
+
err := conn.Close()
|
| 225 |
+
if err != nil {
|
| 226 |
+
common.SysError("error closing websocket connection: " + err.Error())
|
| 227 |
+
}
|
| 228 |
+
break
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
stopChan <- true
|
| 232 |
+
}()
|
| 233 |
+
setEventStreamHeaders(c)
|
| 234 |
+
c.Stream(func(w io.Writer) bool {
|
| 235 |
+
select {
|
| 236 |
+
case xunfeiResponse := <-dataChan:
|
| 237 |
+
usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens
|
| 238 |
+
usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens
|
| 239 |
+
usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens
|
| 240 |
+
response := streamResponseXunfei2OpenAI(&xunfeiResponse)
|
| 241 |
+
jsonResponse, err := json.Marshal(response)
|
| 242 |
+
if err != nil {
|
| 243 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 244 |
+
return true
|
| 245 |
+
}
|
| 246 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
| 247 |
+
return true
|
| 248 |
+
case <-stopChan:
|
| 249 |
+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
| 250 |
+
return false
|
| 251 |
+
}
|
| 252 |
+
})
|
| 253 |
+
return nil, &usage
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
func xunfeiHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 257 |
+
var xunfeiResponse XunfeiChatResponse
|
| 258 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 259 |
+
if err != nil {
|
| 260 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 261 |
+
}
|
| 262 |
+
err = resp.Body.Close()
|
| 263 |
+
if err != nil {
|
| 264 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 265 |
+
}
|
| 266 |
+
err = json.Unmarshal(responseBody, &xunfeiResponse)
|
| 267 |
+
if err != nil {
|
| 268 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 269 |
+
}
|
| 270 |
+
if xunfeiResponse.Header.Code != 0 {
|
| 271 |
+
return &OpenAIErrorWithStatusCode{
|
| 272 |
+
OpenAIError: OpenAIError{
|
| 273 |
+
Message: xunfeiResponse.Header.Message,
|
| 274 |
+
Type: "xunfei_error",
|
| 275 |
+
Param: "",
|
| 276 |
+
Code: xunfeiResponse.Header.Code,
|
| 277 |
+
},
|
| 278 |
+
StatusCode: resp.StatusCode,
|
| 279 |
+
}, nil
|
| 280 |
+
}
|
| 281 |
+
fullTextResponse := responseXunfei2OpenAI(&xunfeiResponse)
|
| 282 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 283 |
+
if err != nil {
|
| 284 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 285 |
+
}
|
| 286 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 287 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 288 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 289 |
+
return nil, &fullTextResponse.Usage
|
| 290 |
+
}
|
controller/relay-zhipu.go
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
"github.com/golang-jwt/jwt"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
"one-api/common"
|
| 11 |
+
"strings"
|
| 12 |
+
"sync"
|
| 13 |
+
"time"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// https://open.bigmodel.cn/doc/api#chatglm_std
|
| 17 |
+
// chatglm_std, chatglm_lite
|
| 18 |
+
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/invoke
|
| 19 |
+
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke
|
| 20 |
+
|
| 21 |
+
type ZhipuMessage struct {
|
| 22 |
+
Role string `json:"role"`
|
| 23 |
+
Content string `json:"content"`
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type ZhipuRequest struct {
|
| 27 |
+
Prompt []ZhipuMessage `json:"prompt"`
|
| 28 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 29 |
+
TopP float64 `json:"top_p,omitempty"`
|
| 30 |
+
RequestId string `json:"request_id,omitempty"`
|
| 31 |
+
Incremental bool `json:"incremental,omitempty"`
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
type ZhipuResponseData struct {
|
| 35 |
+
TaskId string `json:"task_id"`
|
| 36 |
+
RequestId string `json:"request_id"`
|
| 37 |
+
TaskStatus string `json:"task_status"`
|
| 38 |
+
Choices []ZhipuMessage `json:"choices"`
|
| 39 |
+
Usage `json:"usage"`
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
type ZhipuResponse struct {
|
| 43 |
+
Code int `json:"code"`
|
| 44 |
+
Msg string `json:"msg"`
|
| 45 |
+
Success bool `json:"success"`
|
| 46 |
+
Data ZhipuResponseData `json:"data"`
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
type ZhipuStreamMetaResponse struct {
|
| 50 |
+
RequestId string `json:"request_id"`
|
| 51 |
+
TaskId string `json:"task_id"`
|
| 52 |
+
TaskStatus string `json:"task_status"`
|
| 53 |
+
Usage `json:"usage"`
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
type zhipuTokenData struct {
|
| 57 |
+
Token string
|
| 58 |
+
ExpiryTime time.Time
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
var zhipuTokens sync.Map
|
| 62 |
+
var expSeconds int64 = 24 * 3600
|
| 63 |
+
|
| 64 |
+
func getZhipuToken(apikey string) string {
|
| 65 |
+
data, ok := zhipuTokens.Load(apikey)
|
| 66 |
+
if ok {
|
| 67 |
+
tokenData := data.(zhipuTokenData)
|
| 68 |
+
if time.Now().Before(tokenData.ExpiryTime) {
|
| 69 |
+
return tokenData.Token
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
split := strings.Split(apikey, ".")
|
| 74 |
+
if len(split) != 2 {
|
| 75 |
+
common.SysError("invalid zhipu key: " + apikey)
|
| 76 |
+
return ""
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
id := split[0]
|
| 80 |
+
secret := split[1]
|
| 81 |
+
|
| 82 |
+
expMillis := time.Now().Add(time.Duration(expSeconds)*time.Second).UnixNano() / 1e6
|
| 83 |
+
expiryTime := time.Now().Add(time.Duration(expSeconds) * time.Second)
|
| 84 |
+
|
| 85 |
+
timestamp := time.Now().UnixNano() / 1e6
|
| 86 |
+
|
| 87 |
+
payload := jwt.MapClaims{
|
| 88 |
+
"api_key": id,
|
| 89 |
+
"exp": expMillis,
|
| 90 |
+
"timestamp": timestamp,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
| 94 |
+
|
| 95 |
+
token.Header["alg"] = "HS256"
|
| 96 |
+
token.Header["sign_type"] = "SIGN"
|
| 97 |
+
|
| 98 |
+
tokenString, err := token.SignedString([]byte(secret))
|
| 99 |
+
if err != nil {
|
| 100 |
+
return ""
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
zhipuTokens.Store(apikey, zhipuTokenData{
|
| 104 |
+
Token: tokenString,
|
| 105 |
+
ExpiryTime: expiryTime,
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
return tokenString
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
|
| 112 |
+
messages := make([]ZhipuMessage, 0, len(request.Messages))
|
| 113 |
+
for _, message := range request.Messages {
|
| 114 |
+
if message.Role == "system" {
|
| 115 |
+
messages = append(messages, ZhipuMessage{
|
| 116 |
+
Role: "system",
|
| 117 |
+
Content: message.Content,
|
| 118 |
+
})
|
| 119 |
+
messages = append(messages, ZhipuMessage{
|
| 120 |
+
Role: "user",
|
| 121 |
+
Content: "Okay",
|
| 122 |
+
})
|
| 123 |
+
} else {
|
| 124 |
+
messages = append(messages, ZhipuMessage{
|
| 125 |
+
Role: message.Role,
|
| 126 |
+
Content: message.Content,
|
| 127 |
+
})
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
return &ZhipuRequest{
|
| 131 |
+
Prompt: messages,
|
| 132 |
+
Temperature: request.Temperature,
|
| 133 |
+
TopP: request.TopP,
|
| 134 |
+
Incremental: false,
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse {
|
| 139 |
+
fullTextResponse := OpenAITextResponse{
|
| 140 |
+
Id: response.Data.TaskId,
|
| 141 |
+
Object: "chat.completion",
|
| 142 |
+
Created: common.GetTimestamp(),
|
| 143 |
+
Choices: make([]OpenAITextResponseChoice, 0, len(response.Data.Choices)),
|
| 144 |
+
Usage: response.Data.Usage,
|
| 145 |
+
}
|
| 146 |
+
for i, choice := range response.Data.Choices {
|
| 147 |
+
openaiChoice := OpenAITextResponseChoice{
|
| 148 |
+
Index: i,
|
| 149 |
+
Message: Message{
|
| 150 |
+
Role: choice.Role,
|
| 151 |
+
Content: strings.Trim(choice.Content, "\""),
|
| 152 |
+
},
|
| 153 |
+
FinishReason: "",
|
| 154 |
+
}
|
| 155 |
+
if i == len(response.Data.Choices)-1 {
|
| 156 |
+
openaiChoice.FinishReason = "stop"
|
| 157 |
+
}
|
| 158 |
+
fullTextResponse.Choices = append(fullTextResponse.Choices, openaiChoice)
|
| 159 |
+
}
|
| 160 |
+
return &fullTextResponse
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
|
| 164 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 165 |
+
choice.Delta.Content = zhipuResponse
|
| 166 |
+
response := ChatCompletionsStreamResponse{
|
| 167 |
+
Object: "chat.completion.chunk",
|
| 168 |
+
Created: common.GetTimestamp(),
|
| 169 |
+
Model: "chatglm",
|
| 170 |
+
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
| 171 |
+
}
|
| 172 |
+
return &response
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
|
| 176 |
+
var choice ChatCompletionsStreamResponseChoice
|
| 177 |
+
choice.Delta.Content = ""
|
| 178 |
+
choice.FinishReason = &stopFinishReason
|
| 179 |
+
response := ChatCompletionsStreamResponse{
|
| 180 |
+
Id: zhipuResponse.RequestId,
|
| 181 |
+
Object: "chat.completion.chunk",
|
| 182 |
+
Created: common.GetTimestamp(),
|
| 183 |
+
Model: "chatglm",
|
| 184 |
+
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
| 185 |
+
}
|
| 186 |
+
return &response, &zhipuResponse.Usage
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 190 |
+
var usage *Usage
|
| 191 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 192 |
+
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
| 193 |
+
if atEOF && len(data) == 0 {
|
| 194 |
+
return 0, nil, nil
|
| 195 |
+
}
|
| 196 |
+
if i := strings.Index(string(data), "\n\n"); i >= 0 && strings.Index(string(data), ":") >= 0 {
|
| 197 |
+
return i + 2, data[0:i], nil
|
| 198 |
+
}
|
| 199 |
+
if atEOF {
|
| 200 |
+
return len(data), data, nil
|
| 201 |
+
}
|
| 202 |
+
return 0, nil, nil
|
| 203 |
+
})
|
| 204 |
+
dataChan := make(chan string)
|
| 205 |
+
metaChan := make(chan string)
|
| 206 |
+
stopChan := make(chan bool)
|
| 207 |
+
go func() {
|
| 208 |
+
for scanner.Scan() {
|
| 209 |
+
data := scanner.Text()
|
| 210 |
+
lines := strings.Split(data, "\n")
|
| 211 |
+
for i, line := range lines {
|
| 212 |
+
if len(line) < 5 {
|
| 213 |
+
continue
|
| 214 |
+
}
|
| 215 |
+
if line[:5] == "data:" {
|
| 216 |
+
dataChan <- line[5:]
|
| 217 |
+
if i != len(lines)-1 {
|
| 218 |
+
dataChan <- "\n"
|
| 219 |
+
}
|
| 220 |
+
} else if line[:5] == "meta:" {
|
| 221 |
+
metaChan <- line[5:]
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
stopChan <- true
|
| 226 |
+
}()
|
| 227 |
+
setEventStreamHeaders(c)
|
| 228 |
+
c.Stream(func(w io.Writer) bool {
|
| 229 |
+
select {
|
| 230 |
+
case data := <-dataChan:
|
| 231 |
+
response := streamResponseZhipu2OpenAI(data)
|
| 232 |
+
jsonResponse, err := json.Marshal(response)
|
| 233 |
+
if err != nil {
|
| 234 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 235 |
+
return true
|
| 236 |
+
}
|
| 237 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
| 238 |
+
return true
|
| 239 |
+
case data := <-metaChan:
|
| 240 |
+
var zhipuResponse ZhipuStreamMetaResponse
|
| 241 |
+
err := json.Unmarshal([]byte(data), &zhipuResponse)
|
| 242 |
+
if err != nil {
|
| 243 |
+
common.SysError("error unmarshalling stream response: " + err.Error())
|
| 244 |
+
return true
|
| 245 |
+
}
|
| 246 |
+
response, zhipuUsage := streamMetaResponseZhipu2OpenAI(&zhipuResponse)
|
| 247 |
+
jsonResponse, err := json.Marshal(response)
|
| 248 |
+
if err != nil {
|
| 249 |
+
common.SysError("error marshalling stream response: " + err.Error())
|
| 250 |
+
return true
|
| 251 |
+
}
|
| 252 |
+
usage = zhipuUsage
|
| 253 |
+
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
| 254 |
+
return true
|
| 255 |
+
case <-stopChan:
|
| 256 |
+
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
| 257 |
+
return false
|
| 258 |
+
}
|
| 259 |
+
})
|
| 260 |
+
err := resp.Body.Close()
|
| 261 |
+
if err != nil {
|
| 262 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 263 |
+
}
|
| 264 |
+
return nil, usage
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
func zhipuHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
| 268 |
+
var zhipuResponse ZhipuResponse
|
| 269 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 270 |
+
if err != nil {
|
| 271 |
+
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
| 272 |
+
}
|
| 273 |
+
err = resp.Body.Close()
|
| 274 |
+
if err != nil {
|
| 275 |
+
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
| 276 |
+
}
|
| 277 |
+
err = json.Unmarshal(responseBody, &zhipuResponse)
|
| 278 |
+
if err != nil {
|
| 279 |
+
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
| 280 |
+
}
|
| 281 |
+
if !zhipuResponse.Success {
|
| 282 |
+
return &OpenAIErrorWithStatusCode{
|
| 283 |
+
OpenAIError: OpenAIError{
|
| 284 |
+
Message: zhipuResponse.Msg,
|
| 285 |
+
Type: "zhipu_error",
|
| 286 |
+
Param: "",
|
| 287 |
+
Code: zhipuResponse.Code,
|
| 288 |
+
},
|
| 289 |
+
StatusCode: resp.StatusCode,
|
| 290 |
+
}, nil
|
| 291 |
+
}
|
| 292 |
+
fullTextResponse := responseZhipu2OpenAI(&zhipuResponse)
|
| 293 |
+
jsonResponse, err := json.Marshal(fullTextResponse)
|
| 294 |
+
if err != nil {
|
| 295 |
+
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
| 296 |
+
}
|
| 297 |
+
c.Writer.Header().Set("Content-Type", "application/json")
|
| 298 |
+
c.Writer.WriteHeader(resp.StatusCode)
|
| 299 |
+
_, err = c.Writer.Write(jsonResponse)
|
| 300 |
+
return nil, &fullTextResponse.Usage
|
| 301 |
+
}
|
controller/relay.go
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"net/http"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
"strconv"
|
| 8 |
+
"strings"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type Message struct {
|
| 14 |
+
Role string `json:"role"`
|
| 15 |
+
Content string `json:"content"`
|
| 16 |
+
Name *string `json:"name,omitempty"`
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const (
|
| 20 |
+
RelayModeUnknown = iota
|
| 21 |
+
RelayModeChatCompletions
|
| 22 |
+
RelayModeCompletions
|
| 23 |
+
RelayModeEmbeddings
|
| 24 |
+
RelayModeModerations
|
| 25 |
+
RelayModeImagesGenerations
|
| 26 |
+
RelayModeEdits
|
| 27 |
+
RelayModeAudio
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
// https://platform.openai.com/docs/api-reference/chat
|
| 31 |
+
|
| 32 |
+
type GeneralOpenAIRequest struct {
|
| 33 |
+
Model string `json:"model,omitempty"`
|
| 34 |
+
Messages []Message `json:"messages,omitempty"`
|
| 35 |
+
Prompt any `json:"prompt,omitempty"`
|
| 36 |
+
Stream bool `json:"stream,omitempty"`
|
| 37 |
+
MaxTokens int `json:"max_tokens,omitempty"`
|
| 38 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 39 |
+
TopP float64 `json:"top_p,omitempty"`
|
| 40 |
+
N int `json:"n,omitempty"`
|
| 41 |
+
Input any `json:"input,omitempty"`
|
| 42 |
+
Instruction string `json:"instruction,omitempty"`
|
| 43 |
+
Size string `json:"size,omitempty"`
|
| 44 |
+
Functions any `json:"functions,omitempty"`
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
type ChatRequest struct {
|
| 48 |
+
Model string `json:"model"`
|
| 49 |
+
Messages []Message `json:"messages"`
|
| 50 |
+
MaxTokens int `json:"max_tokens"`
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
type TextRequest struct {
|
| 54 |
+
Model string `json:"model"`
|
| 55 |
+
Messages []Message `json:"messages"`
|
| 56 |
+
Prompt string `json:"prompt"`
|
| 57 |
+
MaxTokens int `json:"max_tokens"`
|
| 58 |
+
//Stream bool `json:"stream"`
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
type ImageRequest struct {
|
| 62 |
+
Prompt string `json:"prompt"`
|
| 63 |
+
N int `json:"n"`
|
| 64 |
+
Size string `json:"size"`
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
type AudioResponse struct {
|
| 68 |
+
Text string `json:"text,omitempty"`
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
type Usage struct {
|
| 72 |
+
PromptTokens int `json:"prompt_tokens"`
|
| 73 |
+
CompletionTokens int `json:"completion_tokens"`
|
| 74 |
+
TotalTokens int `json:"total_tokens"`
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
type OpenAIError struct {
|
| 78 |
+
Message string `json:"message"`
|
| 79 |
+
Type string `json:"type"`
|
| 80 |
+
Param string `json:"param"`
|
| 81 |
+
Code any `json:"code"`
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
type OpenAIErrorWithStatusCode struct {
|
| 85 |
+
OpenAIError
|
| 86 |
+
StatusCode int `json:"status_code"`
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
type TextResponse struct {
|
| 90 |
+
Choices []OpenAITextResponseChoice `json:"choices"`
|
| 91 |
+
Usage `json:"usage"`
|
| 92 |
+
Error OpenAIError `json:"error"`
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
type OpenAITextResponseChoice struct {
|
| 96 |
+
Index int `json:"index"`
|
| 97 |
+
Message `json:"message"`
|
| 98 |
+
FinishReason string `json:"finish_reason"`
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
type OpenAITextResponse struct {
|
| 102 |
+
Id string `json:"id"`
|
| 103 |
+
Object string `json:"object"`
|
| 104 |
+
Created int64 `json:"created"`
|
| 105 |
+
Choices []OpenAITextResponseChoice `json:"choices"`
|
| 106 |
+
Usage `json:"usage"`
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
type OpenAIEmbeddingResponseItem struct {
|
| 110 |
+
Object string `json:"object"`
|
| 111 |
+
Index int `json:"index"`
|
| 112 |
+
Embedding []float64 `json:"embedding"`
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
type OpenAIEmbeddingResponse struct {
|
| 116 |
+
Object string `json:"object"`
|
| 117 |
+
Data []OpenAIEmbeddingResponseItem `json:"data"`
|
| 118 |
+
Model string `json:"model"`
|
| 119 |
+
Usage `json:"usage"`
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
type ImageResponse struct {
|
| 123 |
+
Created int `json:"created"`
|
| 124 |
+
Data []struct {
|
| 125 |
+
Url string `json:"url"`
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
type ChatCompletionsStreamResponseChoice struct {
|
| 130 |
+
Delta struct {
|
| 131 |
+
Content string `json:"content"`
|
| 132 |
+
} `json:"delta"`
|
| 133 |
+
FinishReason *string `json:"finish_reason"`
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
type ChatCompletionsStreamResponse struct {
|
| 137 |
+
Id string `json:"id"`
|
| 138 |
+
Object string `json:"object"`
|
| 139 |
+
Created int64 `json:"created"`
|
| 140 |
+
Model string `json:"model"`
|
| 141 |
+
Choices []ChatCompletionsStreamResponseChoice `json:"choices"`
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
type CompletionsStreamResponse struct {
|
| 145 |
+
Choices []struct {
|
| 146 |
+
Text string `json:"text"`
|
| 147 |
+
FinishReason string `json:"finish_reason"`
|
| 148 |
+
} `json:"choices"`
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
func Relay(c *gin.Context) {
|
| 152 |
+
relayMode := RelayModeUnknown
|
| 153 |
+
if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") {
|
| 154 |
+
relayMode = RelayModeChatCompletions
|
| 155 |
+
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") {
|
| 156 |
+
relayMode = RelayModeCompletions
|
| 157 |
+
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
| 158 |
+
relayMode = RelayModeEmbeddings
|
| 159 |
+
} else if strings.HasSuffix(c.Request.URL.Path, "embeddings") {
|
| 160 |
+
relayMode = RelayModeEmbeddings
|
| 161 |
+
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
| 162 |
+
relayMode = RelayModeModerations
|
| 163 |
+
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
| 164 |
+
relayMode = RelayModeImagesGenerations
|
| 165 |
+
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/edits") {
|
| 166 |
+
relayMode = RelayModeEdits
|
| 167 |
+
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
| 168 |
+
relayMode = RelayModeAudio
|
| 169 |
+
}
|
| 170 |
+
var err *OpenAIErrorWithStatusCode
|
| 171 |
+
switch relayMode {
|
| 172 |
+
case RelayModeImagesGenerations:
|
| 173 |
+
err = relayImageHelper(c, relayMode)
|
| 174 |
+
case RelayModeAudio:
|
| 175 |
+
err = relayAudioHelper(c, relayMode)
|
| 176 |
+
default:
|
| 177 |
+
err = relayTextHelper(c, relayMode)
|
| 178 |
+
}
|
| 179 |
+
if err != nil {
|
| 180 |
+
retryTimesStr := c.Query("retry")
|
| 181 |
+
retryTimes, _ := strconv.Atoi(retryTimesStr)
|
| 182 |
+
if retryTimesStr == "" {
|
| 183 |
+
retryTimes = common.RetryTimes
|
| 184 |
+
}
|
| 185 |
+
if retryTimes > 0 {
|
| 186 |
+
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
| 187 |
+
} else {
|
| 188 |
+
if err.StatusCode == http.StatusTooManyRequests {
|
| 189 |
+
err.OpenAIError.Message = "当前分组上游负载已饱和,请稍后再试"
|
| 190 |
+
}
|
| 191 |
+
c.JSON(err.StatusCode, gin.H{
|
| 192 |
+
"error": err.OpenAIError,
|
| 193 |
+
})
|
| 194 |
+
}
|
| 195 |
+
channelId := c.GetInt("channel_id")
|
| 196 |
+
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
| 197 |
+
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
| 198 |
+
if shouldDisableChannel(&err.OpenAIError, err.StatusCode) {
|
| 199 |
+
channelId := c.GetInt("channel_id")
|
| 200 |
+
channelName := c.GetString("channel_name")
|
| 201 |
+
disableChannel(channelId, channelName, err.Message)
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
func RelayNotImplemented(c *gin.Context) {
|
| 207 |
+
err := OpenAIError{
|
| 208 |
+
Message: "API not implemented",
|
| 209 |
+
Type: "one_api_error",
|
| 210 |
+
Param: "",
|
| 211 |
+
Code: "api_not_implemented",
|
| 212 |
+
}
|
| 213 |
+
c.JSON(http.StatusNotImplemented, gin.H{
|
| 214 |
+
"error": err,
|
| 215 |
+
})
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
func RelayNotFound(c *gin.Context) {
|
| 219 |
+
err := OpenAIError{
|
| 220 |
+
Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path),
|
| 221 |
+
Type: "invalid_request_error",
|
| 222 |
+
Param: "",
|
| 223 |
+
Code: "",
|
| 224 |
+
}
|
| 225 |
+
c.JSON(http.StatusNotFound, gin.H{
|
| 226 |
+
"error": err,
|
| 227 |
+
})
|
| 228 |
+
}
|
controller/token.go
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"net/http"
|
| 6 |
+
"one-api/common"
|
| 7 |
+
"one-api/model"
|
| 8 |
+
"strconv"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func GetAllTokens(c *gin.Context) {
|
| 12 |
+
userId := c.GetInt("id")
|
| 13 |
+
p, _ := strconv.Atoi(c.Query("p"))
|
| 14 |
+
if p < 0 {
|
| 15 |
+
p = 0
|
| 16 |
+
}
|
| 17 |
+
tokens, err := model.GetAllUserTokens(userId, p*common.ItemsPerPage, common.ItemsPerPage)
|
| 18 |
+
if err != nil {
|
| 19 |
+
c.JSON(http.StatusOK, gin.H{
|
| 20 |
+
"success": false,
|
| 21 |
+
"message": err.Error(),
|
| 22 |
+
})
|
| 23 |
+
return
|
| 24 |
+
}
|
| 25 |
+
c.JSON(http.StatusOK, gin.H{
|
| 26 |
+
"success": true,
|
| 27 |
+
"message": "",
|
| 28 |
+
"data": tokens,
|
| 29 |
+
})
|
| 30 |
+
return
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func SearchTokens(c *gin.Context) {
|
| 34 |
+
userId := c.GetInt("id")
|
| 35 |
+
keyword := c.Query("keyword")
|
| 36 |
+
tokens, err := model.SearchUserTokens(userId, keyword)
|
| 37 |
+
if err != nil {
|
| 38 |
+
c.JSON(http.StatusOK, gin.H{
|
| 39 |
+
"success": false,
|
| 40 |
+
"message": err.Error(),
|
| 41 |
+
})
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
c.JSON(http.StatusOK, gin.H{
|
| 45 |
+
"success": true,
|
| 46 |
+
"message": "",
|
| 47 |
+
"data": tokens,
|
| 48 |
+
})
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func GetToken(c *gin.Context) {
|
| 53 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 54 |
+
userId := c.GetInt("id")
|
| 55 |
+
if err != nil {
|
| 56 |
+
c.JSON(http.StatusOK, gin.H{
|
| 57 |
+
"success": false,
|
| 58 |
+
"message": err.Error(),
|
| 59 |
+
})
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
token, err := model.GetTokenByIds(id, userId)
|
| 63 |
+
if err != nil {
|
| 64 |
+
c.JSON(http.StatusOK, gin.H{
|
| 65 |
+
"success": false,
|
| 66 |
+
"message": err.Error(),
|
| 67 |
+
})
|
| 68 |
+
return
|
| 69 |
+
}
|
| 70 |
+
c.JSON(http.StatusOK, gin.H{
|
| 71 |
+
"success": true,
|
| 72 |
+
"message": "",
|
| 73 |
+
"data": token,
|
| 74 |
+
})
|
| 75 |
+
return
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
func GetTokenStatus(c *gin.Context) {
|
| 79 |
+
tokenId := c.GetInt("token_id")
|
| 80 |
+
userId := c.GetInt("id")
|
| 81 |
+
token, err := model.GetTokenByIds(tokenId, userId)
|
| 82 |
+
if err != nil {
|
| 83 |
+
c.JSON(http.StatusOK, gin.H{
|
| 84 |
+
"success": false,
|
| 85 |
+
"message": err.Error(),
|
| 86 |
+
})
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
expiredAt := token.ExpiredTime
|
| 90 |
+
if expiredAt == -1 {
|
| 91 |
+
expiredAt = 0
|
| 92 |
+
}
|
| 93 |
+
c.JSON(http.StatusOK, gin.H{
|
| 94 |
+
"object": "credit_summary",
|
| 95 |
+
"total_granted": token.RemainQuota,
|
| 96 |
+
"total_used": 0, // not supported currently
|
| 97 |
+
"total_available": token.RemainQuota,
|
| 98 |
+
"expires_at": expiredAt * 1000,
|
| 99 |
+
})
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
func AddToken(c *gin.Context) {
|
| 103 |
+
token := model.Token{}
|
| 104 |
+
err := c.ShouldBindJSON(&token)
|
| 105 |
+
if err != nil {
|
| 106 |
+
c.JSON(http.StatusOK, gin.H{
|
| 107 |
+
"success": false,
|
| 108 |
+
"message": err.Error(),
|
| 109 |
+
})
|
| 110 |
+
return
|
| 111 |
+
}
|
| 112 |
+
if len(token.Name) > 30 {
|
| 113 |
+
c.JSON(http.StatusOK, gin.H{
|
| 114 |
+
"success": false,
|
| 115 |
+
"message": "令牌名称过长",
|
| 116 |
+
})
|
| 117 |
+
return
|
| 118 |
+
}
|
| 119 |
+
cleanToken := model.Token{
|
| 120 |
+
UserId: c.GetInt("id"),
|
| 121 |
+
Name: token.Name,
|
| 122 |
+
Key: common.GenerateKey(),
|
| 123 |
+
CreatedTime: common.GetTimestamp(),
|
| 124 |
+
AccessedTime: common.GetTimestamp(),
|
| 125 |
+
ExpiredTime: token.ExpiredTime,
|
| 126 |
+
RemainQuota: token.RemainQuota,
|
| 127 |
+
UnlimitedQuota: token.UnlimitedQuota,
|
| 128 |
+
}
|
| 129 |
+
err = cleanToken.Insert()
|
| 130 |
+
if err != nil {
|
| 131 |
+
c.JSON(http.StatusOK, gin.H{
|
| 132 |
+
"success": false,
|
| 133 |
+
"message": err.Error(),
|
| 134 |
+
})
|
| 135 |
+
return
|
| 136 |
+
}
|
| 137 |
+
c.JSON(http.StatusOK, gin.H{
|
| 138 |
+
"success": true,
|
| 139 |
+
"message": "",
|
| 140 |
+
})
|
| 141 |
+
return
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
func DeleteToken(c *gin.Context) {
|
| 145 |
+
id, _ := strconv.Atoi(c.Param("id"))
|
| 146 |
+
userId := c.GetInt("id")
|
| 147 |
+
err := model.DeleteTokenById(id, userId)
|
| 148 |
+
if err != nil {
|
| 149 |
+
c.JSON(http.StatusOK, gin.H{
|
| 150 |
+
"success": false,
|
| 151 |
+
"message": err.Error(),
|
| 152 |
+
})
|
| 153 |
+
return
|
| 154 |
+
}
|
| 155 |
+
c.JSON(http.StatusOK, gin.H{
|
| 156 |
+
"success": true,
|
| 157 |
+
"message": "",
|
| 158 |
+
})
|
| 159 |
+
return
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
func UpdateToken(c *gin.Context) {
|
| 163 |
+
userId := c.GetInt("id")
|
| 164 |
+
statusOnly := c.Query("status_only")
|
| 165 |
+
token := model.Token{}
|
| 166 |
+
err := c.ShouldBindJSON(&token)
|
| 167 |
+
if err != nil {
|
| 168 |
+
c.JSON(http.StatusOK, gin.H{
|
| 169 |
+
"success": false,
|
| 170 |
+
"message": err.Error(),
|
| 171 |
+
})
|
| 172 |
+
return
|
| 173 |
+
}
|
| 174 |
+
if len(token.Name) > 30 {
|
| 175 |
+
c.JSON(http.StatusOK, gin.H{
|
| 176 |
+
"success": false,
|
| 177 |
+
"message": "令牌名称过长",
|
| 178 |
+
})
|
| 179 |
+
return
|
| 180 |
+
}
|
| 181 |
+
cleanToken, err := model.GetTokenByIds(token.Id, userId)
|
| 182 |
+
if err != nil {
|
| 183 |
+
c.JSON(http.StatusOK, gin.H{
|
| 184 |
+
"success": false,
|
| 185 |
+
"message": err.Error(),
|
| 186 |
+
})
|
| 187 |
+
return
|
| 188 |
+
}
|
| 189 |
+
if token.Status == common.TokenStatusEnabled {
|
| 190 |
+
if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() && cleanToken.ExpiredTime != -1 {
|
| 191 |
+
c.JSON(http.StatusOK, gin.H{
|
| 192 |
+
"success": false,
|
| 193 |
+
"message": "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期",
|
| 194 |
+
})
|
| 195 |
+
return
|
| 196 |
+
}
|
| 197 |
+
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
|
| 198 |
+
c.JSON(http.StatusOK, gin.H{
|
| 199 |
+
"success": false,
|
| 200 |
+
"message": "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度",
|
| 201 |
+
})
|
| 202 |
+
return
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
if statusOnly != "" {
|
| 206 |
+
cleanToken.Status = token.Status
|
| 207 |
+
} else {
|
| 208 |
+
// If you add more fields, please also update token.Update()
|
| 209 |
+
cleanToken.Name = token.Name
|
| 210 |
+
cleanToken.ExpiredTime = token.ExpiredTime
|
| 211 |
+
cleanToken.RemainQuota = token.RemainQuota
|
| 212 |
+
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
| 213 |
+
}
|
| 214 |
+
err = cleanToken.Update()
|
| 215 |
+
if err != nil {
|
| 216 |
+
c.JSON(http.StatusOK, gin.H{
|
| 217 |
+
"success": false,
|
| 218 |
+
"message": err.Error(),
|
| 219 |
+
})
|
| 220 |
+
return
|
| 221 |
+
}
|
| 222 |
+
c.JSON(http.StatusOK, gin.H{
|
| 223 |
+
"success": true,
|
| 224 |
+
"message": "",
|
| 225 |
+
"data": cleanToken,
|
| 226 |
+
})
|
| 227 |
+
return
|
| 228 |
+
}
|
controller/user.go
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"net/http"
|
| 7 |
+
"one-api/common"
|
| 8 |
+
"one-api/model"
|
| 9 |
+
"strconv"
|
| 10 |
+
|
| 11 |
+
"github.com/gin-contrib/sessions"
|
| 12 |
+
"github.com/gin-gonic/gin"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
type LoginRequest struct {
|
| 16 |
+
Username string `json:"username"`
|
| 17 |
+
Password string `json:"password"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func Login(c *gin.Context) {
|
| 21 |
+
if !common.PasswordLoginEnabled {
|
| 22 |
+
c.JSON(http.StatusOK, gin.H{
|
| 23 |
+
"message": "管理员关闭了密码登录",
|
| 24 |
+
"success": false,
|
| 25 |
+
})
|
| 26 |
+
return
|
| 27 |
+
}
|
| 28 |
+
var loginRequest LoginRequest
|
| 29 |
+
err := json.NewDecoder(c.Request.Body).Decode(&loginRequest)
|
| 30 |
+
if err != nil {
|
| 31 |
+
c.JSON(http.StatusOK, gin.H{
|
| 32 |
+
"message": "无效的参数",
|
| 33 |
+
"success": false,
|
| 34 |
+
})
|
| 35 |
+
return
|
| 36 |
+
}
|
| 37 |
+
username := loginRequest.Username
|
| 38 |
+
password := loginRequest.Password
|
| 39 |
+
if username == "" || password == "" {
|
| 40 |
+
c.JSON(http.StatusOK, gin.H{
|
| 41 |
+
"message": "无效的参数",
|
| 42 |
+
"success": false,
|
| 43 |
+
})
|
| 44 |
+
return
|
| 45 |
+
}
|
| 46 |
+
user := model.User{
|
| 47 |
+
Username: username,
|
| 48 |
+
Password: password,
|
| 49 |
+
}
|
| 50 |
+
err = user.ValidateAndFill()
|
| 51 |
+
if err != nil {
|
| 52 |
+
c.JSON(http.StatusOK, gin.H{
|
| 53 |
+
"message": err.Error(),
|
| 54 |
+
"success": false,
|
| 55 |
+
})
|
| 56 |
+
return
|
| 57 |
+
}
|
| 58 |
+
setupLogin(&user, c)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// setup session & cookies and then return user info
|
| 62 |
+
func setupLogin(user *model.User, c *gin.Context) {
|
| 63 |
+
session := sessions.Default(c)
|
| 64 |
+
session.Set("id", user.Id)
|
| 65 |
+
session.Set("username", user.Username)
|
| 66 |
+
session.Set("role", user.Role)
|
| 67 |
+
session.Set("status", user.Status)
|
| 68 |
+
err := session.Save()
|
| 69 |
+
if err != nil {
|
| 70 |
+
c.JSON(http.StatusOK, gin.H{
|
| 71 |
+
"message": "无法保存会话信息,请重试",
|
| 72 |
+
"success": false,
|
| 73 |
+
})
|
| 74 |
+
return
|
| 75 |
+
}
|
| 76 |
+
cleanUser := model.User{
|
| 77 |
+
Id: user.Id,
|
| 78 |
+
Username: user.Username,
|
| 79 |
+
DisplayName: user.DisplayName,
|
| 80 |
+
Role: user.Role,
|
| 81 |
+
Status: user.Status,
|
| 82 |
+
}
|
| 83 |
+
c.JSON(http.StatusOK, gin.H{
|
| 84 |
+
"message": "",
|
| 85 |
+
"success": true,
|
| 86 |
+
"data": cleanUser,
|
| 87 |
+
})
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
func Logout(c *gin.Context) {
|
| 91 |
+
session := sessions.Default(c)
|
| 92 |
+
session.Clear()
|
| 93 |
+
err := session.Save()
|
| 94 |
+
if err != nil {
|
| 95 |
+
c.JSON(http.StatusOK, gin.H{
|
| 96 |
+
"message": err.Error(),
|
| 97 |
+
"success": false,
|
| 98 |
+
})
|
| 99 |
+
return
|
| 100 |
+
}
|
| 101 |
+
c.JSON(http.StatusOK, gin.H{
|
| 102 |
+
"message": "",
|
| 103 |
+
"success": true,
|
| 104 |
+
})
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
func Register(c *gin.Context) {
|
| 108 |
+
if !common.RegisterEnabled {
|
| 109 |
+
c.JSON(http.StatusOK, gin.H{
|
| 110 |
+
"message": "管理员关闭了新用户注册",
|
| 111 |
+
"success": false,
|
| 112 |
+
})
|
| 113 |
+
return
|
| 114 |
+
}
|
| 115 |
+
if !common.PasswordRegisterEnabled {
|
| 116 |
+
c.JSON(http.StatusOK, gin.H{
|
| 117 |
+
"message": "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册",
|
| 118 |
+
"success": false,
|
| 119 |
+
})
|
| 120 |
+
return
|
| 121 |
+
}
|
| 122 |
+
var user model.User
|
| 123 |
+
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
| 124 |
+
if err != nil {
|
| 125 |
+
c.JSON(http.StatusOK, gin.H{
|
| 126 |
+
"success": false,
|
| 127 |
+
"message": "无效的参数",
|
| 128 |
+
})
|
| 129 |
+
return
|
| 130 |
+
}
|
| 131 |
+
if err := common.Validate.Struct(&user); err != nil {
|
| 132 |
+
c.JSON(http.StatusOK, gin.H{
|
| 133 |
+
"success": false,
|
| 134 |
+
"message": "输入不合法 " + err.Error(),
|
| 135 |
+
})
|
| 136 |
+
return
|
| 137 |
+
}
|
| 138 |
+
if common.EmailVerificationEnabled {
|
| 139 |
+
if user.Email == "" || user.VerificationCode == "" {
|
| 140 |
+
c.JSON(http.StatusOK, gin.H{
|
| 141 |
+
"success": false,
|
| 142 |
+
"message": "管理员开启了邮箱验证,请输入邮箱地址和验证码",
|
| 143 |
+
})
|
| 144 |
+
return
|
| 145 |
+
}
|
| 146 |
+
if !common.VerifyCodeWithKey(user.Email, user.VerificationCode, common.EmailVerificationPurpose) {
|
| 147 |
+
c.JSON(http.StatusOK, gin.H{
|
| 148 |
+
"success": false,
|
| 149 |
+
"message": "验证码错误或已过期",
|
| 150 |
+
})
|
| 151 |
+
return
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
affCode := user.AffCode // this code is the inviter's code, not the user's own code
|
| 155 |
+
inviterId, _ := model.GetUserIdByAffCode(affCode)
|
| 156 |
+
cleanUser := model.User{
|
| 157 |
+
Username: user.Username,
|
| 158 |
+
Password: user.Password,
|
| 159 |
+
DisplayName: user.Username,
|
| 160 |
+
InviterId: inviterId,
|
| 161 |
+
}
|
| 162 |
+
if common.EmailVerificationEnabled {
|
| 163 |
+
cleanUser.Email = user.Email
|
| 164 |
+
}
|
| 165 |
+
if err := cleanUser.Insert(inviterId); err != nil {
|
| 166 |
+
c.JSON(http.StatusOK, gin.H{
|
| 167 |
+
"success": false,
|
| 168 |
+
"message": err.Error(),
|
| 169 |
+
})
|
| 170 |
+
return
|
| 171 |
+
}
|
| 172 |
+
c.JSON(http.StatusOK, gin.H{
|
| 173 |
+
"success": true,
|
| 174 |
+
"message": "",
|
| 175 |
+
})
|
| 176 |
+
return
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
func GetAllUsers(c *gin.Context) {
|
| 180 |
+
p, _ := strconv.Atoi(c.Query("p"))
|
| 181 |
+
if p < 0 {
|
| 182 |
+
p = 0
|
| 183 |
+
}
|
| 184 |
+
users, err := model.GetAllUsers(p*common.ItemsPerPage, common.ItemsPerPage)
|
| 185 |
+
if err != nil {
|
| 186 |
+
c.JSON(http.StatusOK, gin.H{
|
| 187 |
+
"success": false,
|
| 188 |
+
"message": err.Error(),
|
| 189 |
+
})
|
| 190 |
+
return
|
| 191 |
+
}
|
| 192 |
+
c.JSON(http.StatusOK, gin.H{
|
| 193 |
+
"success": true,
|
| 194 |
+
"message": "",
|
| 195 |
+
"data": users,
|
| 196 |
+
})
|
| 197 |
+
return
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
func SearchUsers(c *gin.Context) {
|
| 201 |
+
keyword := c.Query("keyword")
|
| 202 |
+
users, err := model.SearchUsers(keyword)
|
| 203 |
+
if err != nil {
|
| 204 |
+
c.JSON(http.StatusOK, gin.H{
|
| 205 |
+
"success": false,
|
| 206 |
+
"message": err.Error(),
|
| 207 |
+
})
|
| 208 |
+
return
|
| 209 |
+
}
|
| 210 |
+
c.JSON(http.StatusOK, gin.H{
|
| 211 |
+
"success": true,
|
| 212 |
+
"message": "",
|
| 213 |
+
"data": users,
|
| 214 |
+
})
|
| 215 |
+
return
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
func GetUser(c *gin.Context) {
|
| 219 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 220 |
+
if err != nil {
|
| 221 |
+
c.JSON(http.StatusOK, gin.H{
|
| 222 |
+
"success": false,
|
| 223 |
+
"message": err.Error(),
|
| 224 |
+
})
|
| 225 |
+
return
|
| 226 |
+
}
|
| 227 |
+
user, err := model.GetUserById(id, false)
|
| 228 |
+
if err != nil {
|
| 229 |
+
c.JSON(http.StatusOK, gin.H{
|
| 230 |
+
"success": false,
|
| 231 |
+
"message": err.Error(),
|
| 232 |
+
})
|
| 233 |
+
return
|
| 234 |
+
}
|
| 235 |
+
myRole := c.GetInt("role")
|
| 236 |
+
if myRole <= user.Role && myRole != common.RoleRootUser {
|
| 237 |
+
c.JSON(http.StatusOK, gin.H{
|
| 238 |
+
"success": false,
|
| 239 |
+
"message": "无权获取同级或更高等级用户的信息",
|
| 240 |
+
})
|
| 241 |
+
return
|
| 242 |
+
}
|
| 243 |
+
c.JSON(http.StatusOK, gin.H{
|
| 244 |
+
"success": true,
|
| 245 |
+
"message": "",
|
| 246 |
+
"data": user,
|
| 247 |
+
})
|
| 248 |
+
return
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
func GenerateAccessToken(c *gin.Context) {
|
| 252 |
+
id := c.GetInt("id")
|
| 253 |
+
user, err := model.GetUserById(id, true)
|
| 254 |
+
if err != nil {
|
| 255 |
+
c.JSON(http.StatusOK, gin.H{
|
| 256 |
+
"success": false,
|
| 257 |
+
"message": err.Error(),
|
| 258 |
+
})
|
| 259 |
+
return
|
| 260 |
+
}
|
| 261 |
+
user.AccessToken = common.GetUUID()
|
| 262 |
+
|
| 263 |
+
if model.DB.Where("access_token = ?", user.AccessToken).First(user).RowsAffected != 0 {
|
| 264 |
+
c.JSON(http.StatusOK, gin.H{
|
| 265 |
+
"success": false,
|
| 266 |
+
"message": "请重试,系统生成的 UUID 竟然重复了!",
|
| 267 |
+
})
|
| 268 |
+
return
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
if err := user.Update(false); err != nil {
|
| 272 |
+
c.JSON(http.StatusOK, gin.H{
|
| 273 |
+
"success": false,
|
| 274 |
+
"message": err.Error(),
|
| 275 |
+
})
|
| 276 |
+
return
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
c.JSON(http.StatusOK, gin.H{
|
| 280 |
+
"success": true,
|
| 281 |
+
"message": "",
|
| 282 |
+
"data": user.AccessToken,
|
| 283 |
+
})
|
| 284 |
+
return
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
func GetAffCode(c *gin.Context) {
|
| 288 |
+
id := c.GetInt("id")
|
| 289 |
+
user, err := model.GetUserById(id, true)
|
| 290 |
+
if err != nil {
|
| 291 |
+
c.JSON(http.StatusOK, gin.H{
|
| 292 |
+
"success": false,
|
| 293 |
+
"message": err.Error(),
|
| 294 |
+
})
|
| 295 |
+
return
|
| 296 |
+
}
|
| 297 |
+
if user.AffCode == "" {
|
| 298 |
+
user.AffCode = common.GetRandomString(4)
|
| 299 |
+
if err := user.Update(false); err != nil {
|
| 300 |
+
c.JSON(http.StatusOK, gin.H{
|
| 301 |
+
"success": false,
|
| 302 |
+
"message": err.Error(),
|
| 303 |
+
})
|
| 304 |
+
return
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
c.JSON(http.StatusOK, gin.H{
|
| 308 |
+
"success": true,
|
| 309 |
+
"message": "",
|
| 310 |
+
"data": user.AffCode,
|
| 311 |
+
})
|
| 312 |
+
return
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
func GetSelf(c *gin.Context) {
|
| 316 |
+
id := c.GetInt("id")
|
| 317 |
+
user, err := model.GetUserById(id, false)
|
| 318 |
+
if err != nil {
|
| 319 |
+
c.JSON(http.StatusOK, gin.H{
|
| 320 |
+
"success": false,
|
| 321 |
+
"message": err.Error(),
|
| 322 |
+
})
|
| 323 |
+
return
|
| 324 |
+
}
|
| 325 |
+
c.JSON(http.StatusOK, gin.H{
|
| 326 |
+
"success": true,
|
| 327 |
+
"message": "",
|
| 328 |
+
"data": user,
|
| 329 |
+
})
|
| 330 |
+
return
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
func UpdateUser(c *gin.Context) {
|
| 334 |
+
var updatedUser model.User
|
| 335 |
+
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
|
| 336 |
+
if err != nil || updatedUser.Id == 0 {
|
| 337 |
+
c.JSON(http.StatusOK, gin.H{
|
| 338 |
+
"success": false,
|
| 339 |
+
"message": "无效的参数",
|
| 340 |
+
})
|
| 341 |
+
return
|
| 342 |
+
}
|
| 343 |
+
if updatedUser.Password == "" {
|
| 344 |
+
updatedUser.Password = "$I_LOVE_U" // make Validator happy :)
|
| 345 |
+
}
|
| 346 |
+
if err := common.Validate.Struct(&updatedUser); err != nil {
|
| 347 |
+
c.JSON(http.StatusOK, gin.H{
|
| 348 |
+
"success": false,
|
| 349 |
+
"message": "输入不合法 " + err.Error(),
|
| 350 |
+
})
|
| 351 |
+
return
|
| 352 |
+
}
|
| 353 |
+
originUser, err := model.GetUserById(updatedUser.Id, false)
|
| 354 |
+
if err != nil {
|
| 355 |
+
c.JSON(http.StatusOK, gin.H{
|
| 356 |
+
"success": false,
|
| 357 |
+
"message": err.Error(),
|
| 358 |
+
})
|
| 359 |
+
return
|
| 360 |
+
}
|
| 361 |
+
myRole := c.GetInt("role")
|
| 362 |
+
if myRole <= originUser.Role && myRole != common.RoleRootUser {
|
| 363 |
+
c.JSON(http.StatusOK, gin.H{
|
| 364 |
+
"success": false,
|
| 365 |
+
"message": "无权更新同权限等级或更高权限等级的用户信息",
|
| 366 |
+
})
|
| 367 |
+
return
|
| 368 |
+
}
|
| 369 |
+
if myRole <= updatedUser.Role && myRole != common.RoleRootUser {
|
| 370 |
+
c.JSON(http.StatusOK, gin.H{
|
| 371 |
+
"success": false,
|
| 372 |
+
"message": "无权将其他用户权限等级提升到大于等于自己的权限等级",
|
| 373 |
+
})
|
| 374 |
+
return
|
| 375 |
+
}
|
| 376 |
+
if updatedUser.Password == "$I_LOVE_U" {
|
| 377 |
+
updatedUser.Password = "" // rollback to what it should be
|
| 378 |
+
}
|
| 379 |
+
updatePassword := updatedUser.Password != ""
|
| 380 |
+
if err := updatedUser.Update(updatePassword); err != nil {
|
| 381 |
+
c.JSON(http.StatusOK, gin.H{
|
| 382 |
+
"success": false,
|
| 383 |
+
"message": err.Error(),
|
| 384 |
+
})
|
| 385 |
+
return
|
| 386 |
+
}
|
| 387 |
+
if originUser.Quota != updatedUser.Quota {
|
| 388 |
+
model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
|
| 389 |
+
}
|
| 390 |
+
c.JSON(http.StatusOK, gin.H{
|
| 391 |
+
"success": true,
|
| 392 |
+
"message": "",
|
| 393 |
+
})
|
| 394 |
+
return
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
func UpdateSelf(c *gin.Context) {
|
| 398 |
+
var user model.User
|
| 399 |
+
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
| 400 |
+
if err != nil {
|
| 401 |
+
c.JSON(http.StatusOK, gin.H{
|
| 402 |
+
"success": false,
|
| 403 |
+
"message": "无效的参数",
|
| 404 |
+
})
|
| 405 |
+
return
|
| 406 |
+
}
|
| 407 |
+
if user.Password == "" {
|
| 408 |
+
user.Password = "$I_LOVE_U" // make Validator happy :)
|
| 409 |
+
}
|
| 410 |
+
if err := common.Validate.Struct(&user); err != nil {
|
| 411 |
+
c.JSON(http.StatusOK, gin.H{
|
| 412 |
+
"success": false,
|
| 413 |
+
"message": "输入不合法 " + err.Error(),
|
| 414 |
+
})
|
| 415 |
+
return
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
cleanUser := model.User{
|
| 419 |
+
Id: c.GetInt("id"),
|
| 420 |
+
Username: user.Username,
|
| 421 |
+
Password: user.Password,
|
| 422 |
+
DisplayName: user.DisplayName,
|
| 423 |
+
}
|
| 424 |
+
if user.Password == "$I_LOVE_U" {
|
| 425 |
+
user.Password = "" // rollback to what it should be
|
| 426 |
+
cleanUser.Password = ""
|
| 427 |
+
}
|
| 428 |
+
updatePassword := user.Password != ""
|
| 429 |
+
if err := cleanUser.Update(updatePassword); err != nil {
|
| 430 |
+
c.JSON(http.StatusOK, gin.H{
|
| 431 |
+
"success": false,
|
| 432 |
+
"message": err.Error(),
|
| 433 |
+
})
|
| 434 |
+
return
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
c.JSON(http.StatusOK, gin.H{
|
| 438 |
+
"success": true,
|
| 439 |
+
"message": "",
|
| 440 |
+
})
|
| 441 |
+
return
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
func DeleteUser(c *gin.Context) {
|
| 445 |
+
id, err := strconv.Atoi(c.Param("id"))
|
| 446 |
+
if err != nil {
|
| 447 |
+
c.JSON(http.StatusOK, gin.H{
|
| 448 |
+
"success": false,
|
| 449 |
+
"message": err.Error(),
|
| 450 |
+
})
|
| 451 |
+
return
|
| 452 |
+
}
|
| 453 |
+
originUser, err := model.GetUserById(id, false)
|
| 454 |
+
if err != nil {
|
| 455 |
+
c.JSON(http.StatusOK, gin.H{
|
| 456 |
+
"success": false,
|
| 457 |
+
"message": err.Error(),
|
| 458 |
+
})
|
| 459 |
+
return
|
| 460 |
+
}
|
| 461 |
+
myRole := c.GetInt("role")
|
| 462 |
+
if myRole <= originUser.Role {
|
| 463 |
+
c.JSON(http.StatusOK, gin.H{
|
| 464 |
+
"success": false,
|
| 465 |
+
"message": "无权删除同权限等级或更高权限等级的用户",
|
| 466 |
+
})
|
| 467 |
+
return
|
| 468 |
+
}
|
| 469 |
+
err = model.DeleteUserById(id)
|
| 470 |
+
if err != nil {
|
| 471 |
+
c.JSON(http.StatusOK, gin.H{
|
| 472 |
+
"success": true,
|
| 473 |
+
"message": "",
|
| 474 |
+
})
|
| 475 |
+
return
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
func DeleteSelf(c *gin.Context) {
|
| 480 |
+
id := c.GetInt("id")
|
| 481 |
+
user, _ := model.GetUserById(id, false)
|
| 482 |
+
|
| 483 |
+
if user.Role == common.RoleRootUser {
|
| 484 |
+
c.JSON(http.StatusOK, gin.H{
|
| 485 |
+
"success": false,
|
| 486 |
+
"message": "不能删除超级管理员账户",
|
| 487 |
+
})
|
| 488 |
+
return
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
err := model.DeleteUserById(id)
|
| 492 |
+
if err != nil {
|
| 493 |
+
c.JSON(http.StatusOK, gin.H{
|
| 494 |
+
"success": false,
|
| 495 |
+
"message": err.Error(),
|
| 496 |
+
})
|
| 497 |
+
return
|
| 498 |
+
}
|
| 499 |
+
c.JSON(http.StatusOK, gin.H{
|
| 500 |
+
"success": true,
|
| 501 |
+
"message": "",
|
| 502 |
+
})
|
| 503 |
+
return
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
func CreateUser(c *gin.Context) {
|
| 507 |
+
var user model.User
|
| 508 |
+
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
| 509 |
+
if err != nil || user.Username == "" || user.Password == "" {
|
| 510 |
+
c.JSON(http.StatusOK, gin.H{
|
| 511 |
+
"success": false,
|
| 512 |
+
"message": "无效的参数",
|
| 513 |
+
})
|
| 514 |
+
return
|
| 515 |
+
}
|
| 516 |
+
if err := common.Validate.Struct(&user); err != nil {
|
| 517 |
+
c.JSON(http.StatusOK, gin.H{
|
| 518 |
+
"success": false,
|
| 519 |
+
"message": "输入不合法 " + err.Error(),
|
| 520 |
+
})
|
| 521 |
+
return
|
| 522 |
+
}
|
| 523 |
+
if user.DisplayName == "" {
|
| 524 |
+
user.DisplayName = user.Username
|
| 525 |
+
}
|
| 526 |
+
myRole := c.GetInt("role")
|
| 527 |
+
if user.Role >= myRole {
|
| 528 |
+
c.JSON(http.StatusOK, gin.H{
|
| 529 |
+
"success": false,
|
| 530 |
+
"message": "无法创建权限大于等于自己的用户",
|
| 531 |
+
})
|
| 532 |
+
return
|
| 533 |
+
}
|
| 534 |
+
// Even for admin users, we cannot fully trust them!
|
| 535 |
+
cleanUser := model.User{
|
| 536 |
+
Username: user.Username,
|
| 537 |
+
Password: user.Password,
|
| 538 |
+
DisplayName: user.DisplayName,
|
| 539 |
+
}
|
| 540 |
+
if err := cleanUser.Insert(0); err != nil {
|
| 541 |
+
c.JSON(http.StatusOK, gin.H{
|
| 542 |
+
"success": false,
|
| 543 |
+
"message": err.Error(),
|
| 544 |
+
})
|
| 545 |
+
return
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
c.JSON(http.StatusOK, gin.H{
|
| 549 |
+
"success": true,
|
| 550 |
+
"message": "",
|
| 551 |
+
})
|
| 552 |
+
return
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
type ManageRequest struct {
|
| 556 |
+
Username string `json:"username"`
|
| 557 |
+
Action string `json:"action"`
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
// ManageUser Only admin user can do this
|
| 561 |
+
func ManageUser(c *gin.Context) {
|
| 562 |
+
var req ManageRequest
|
| 563 |
+
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
| 564 |
+
|
| 565 |
+
if err != nil {
|
| 566 |
+
c.JSON(http.StatusOK, gin.H{
|
| 567 |
+
"success": false,
|
| 568 |
+
"message": "无效的参数",
|
| 569 |
+
})
|
| 570 |
+
return
|
| 571 |
+
}
|
| 572 |
+
user := model.User{
|
| 573 |
+
Username: req.Username,
|
| 574 |
+
}
|
| 575 |
+
// Fill attributes
|
| 576 |
+
model.DB.Where(&user).First(&user)
|
| 577 |
+
if user.Id == 0 {
|
| 578 |
+
c.JSON(http.StatusOK, gin.H{
|
| 579 |
+
"success": false,
|
| 580 |
+
"message": "用户不存在",
|
| 581 |
+
})
|
| 582 |
+
return
|
| 583 |
+
}
|
| 584 |
+
myRole := c.GetInt("role")
|
| 585 |
+
if myRole <= user.Role && myRole != common.RoleRootUser {
|
| 586 |
+
c.JSON(http.StatusOK, gin.H{
|
| 587 |
+
"success": false,
|
| 588 |
+
"message": "无权更新同权限等级或更高权限等级的用户信息",
|
| 589 |
+
})
|
| 590 |
+
return
|
| 591 |
+
}
|
| 592 |
+
switch req.Action {
|
| 593 |
+
case "disable":
|
| 594 |
+
user.Status = common.UserStatusDisabled
|
| 595 |
+
if user.Role == common.RoleRootUser {
|
| 596 |
+
c.JSON(http.StatusOK, gin.H{
|
| 597 |
+
"success": false,
|
| 598 |
+
"message": "无法禁用超级管理员用户",
|
| 599 |
+
})
|
| 600 |
+
return
|
| 601 |
+
}
|
| 602 |
+
case "enable":
|
| 603 |
+
user.Status = common.UserStatusEnabled
|
| 604 |
+
case "delete":
|
| 605 |
+
if user.Role == common.RoleRootUser {
|
| 606 |
+
c.JSON(http.StatusOK, gin.H{
|
| 607 |
+
"success": false,
|
| 608 |
+
"message": "无法删除超级管理员用户",
|
| 609 |
+
})
|
| 610 |
+
return
|
| 611 |
+
}
|
| 612 |
+
if err := user.Delete(); err != nil {
|
| 613 |
+
c.JSON(http.StatusOK, gin.H{
|
| 614 |
+
"success": false,
|
| 615 |
+
"message": err.Error(),
|
| 616 |
+
})
|
| 617 |
+
return
|
| 618 |
+
}
|
| 619 |
+
case "promote":
|
| 620 |
+
if myRole != common.RoleRootUser {
|
| 621 |
+
c.JSON(http.StatusOK, gin.H{
|
| 622 |
+
"success": false,
|
| 623 |
+
"message": "普通管理员用户无法提升其他用户为管理员",
|
| 624 |
+
})
|
| 625 |
+
return
|
| 626 |
+
}
|
| 627 |
+
if user.Role >= common.RoleAdminUser {
|
| 628 |
+
c.JSON(http.StatusOK, gin.H{
|
| 629 |
+
"success": false,
|
| 630 |
+
"message": "该用户已经是管理员",
|
| 631 |
+
})
|
| 632 |
+
return
|
| 633 |
+
}
|
| 634 |
+
user.Role = common.RoleAdminUser
|
| 635 |
+
case "demote":
|
| 636 |
+
if user.Role == common.RoleRootUser {
|
| 637 |
+
c.JSON(http.StatusOK, gin.H{
|
| 638 |
+
"success": false,
|
| 639 |
+
"message": "无法降级超级管理员用户",
|
| 640 |
+
})
|
| 641 |
+
return
|
| 642 |
+
}
|
| 643 |
+
if user.Role == common.RoleCommonUser {
|
| 644 |
+
c.JSON(http.StatusOK, gin.H{
|
| 645 |
+
"success": false,
|
| 646 |
+
"message": "该用户已经是普通用户",
|
| 647 |
+
})
|
| 648 |
+
return
|
| 649 |
+
}
|
| 650 |
+
user.Role = common.RoleCommonUser
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
if err := user.Update(false); err != nil {
|
| 654 |
+
c.JSON(http.StatusOK, gin.H{
|
| 655 |
+
"success": false,
|
| 656 |
+
"message": err.Error(),
|
| 657 |
+
})
|
| 658 |
+
return
|
| 659 |
+
}
|
| 660 |
+
clearUser := model.User{
|
| 661 |
+
Role: user.Role,
|
| 662 |
+
Status: user.Status,
|
| 663 |
+
}
|
| 664 |
+
c.JSON(http.StatusOK, gin.H{
|
| 665 |
+
"success": true,
|
| 666 |
+
"message": "",
|
| 667 |
+
"data": clearUser,
|
| 668 |
+
})
|
| 669 |
+
return
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
func EmailBind(c *gin.Context) {
|
| 673 |
+
email := c.Query("email")
|
| 674 |
+
code := c.Query("code")
|
| 675 |
+
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
|
| 676 |
+
c.JSON(http.StatusOK, gin.H{
|
| 677 |
+
"success": false,
|
| 678 |
+
"message": "验证码错误或已过期",
|
| 679 |
+
})
|
| 680 |
+
return
|
| 681 |
+
}
|
| 682 |
+
id := c.GetInt("id")
|
| 683 |
+
user := model.User{
|
| 684 |
+
Id: id,
|
| 685 |
+
}
|
| 686 |
+
err := user.FillUserById()
|
| 687 |
+
if err != nil {
|
| 688 |
+
c.JSON(http.StatusOK, gin.H{
|
| 689 |
+
"success": false,
|
| 690 |
+
"message": err.Error(),
|
| 691 |
+
})
|
| 692 |
+
return
|
| 693 |
+
}
|
| 694 |
+
user.Email = email
|
| 695 |
+
// no need to check if this email already taken, because we have used verification code to check it
|
| 696 |
+
err = user.Update(false)
|
| 697 |
+
if err != nil {
|
| 698 |
+
c.JSON(http.StatusOK, gin.H{
|
| 699 |
+
"success": false,
|
| 700 |
+
"message": err.Error(),
|
| 701 |
+
})
|
| 702 |
+
return
|
| 703 |
+
}
|
| 704 |
+
if user.Role == common.RoleRootUser {
|
| 705 |
+
common.RootUserEmail = email
|
| 706 |
+
}
|
| 707 |
+
c.JSON(http.StatusOK, gin.H{
|
| 708 |
+
"success": true,
|
| 709 |
+
"message": "",
|
| 710 |
+
})
|
| 711 |
+
return
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
type topUpRequest struct {
|
| 715 |
+
Key string `json:"key"`
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
func TopUp(c *gin.Context) {
|
| 719 |
+
req := topUpRequest{}
|
| 720 |
+
err := c.ShouldBindJSON(&req)
|
| 721 |
+
if err != nil {
|
| 722 |
+
c.JSON(http.StatusOK, gin.H{
|
| 723 |
+
"success": false,
|
| 724 |
+
"message": err.Error(),
|
| 725 |
+
})
|
| 726 |
+
return
|
| 727 |
+
}
|
| 728 |
+
id := c.GetInt("id")
|
| 729 |
+
quota, err := model.Redeem(req.Key, id)
|
| 730 |
+
if err != nil {
|
| 731 |
+
c.JSON(http.StatusOK, gin.H{
|
| 732 |
+
"success": false,
|
| 733 |
+
"message": err.Error(),
|
| 734 |
+
})
|
| 735 |
+
return
|
| 736 |
+
}
|
| 737 |
+
c.JSON(http.StatusOK, gin.H{
|
| 738 |
+
"success": true,
|
| 739 |
+
"message": "",
|
| 740 |
+
"data": quota,
|
| 741 |
+
})
|
| 742 |
+
return
|
| 743 |
+
}
|
controller/wechat.go
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
"net/http"
|
| 9 |
+
"one-api/common"
|
| 10 |
+
"one-api/model"
|
| 11 |
+
"strconv"
|
| 12 |
+
"time"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
type wechatLoginResponse struct {
|
| 16 |
+
Success bool `json:"success"`
|
| 17 |
+
Message string `json:"message"`
|
| 18 |
+
Data string `json:"data"`
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func getWeChatIdByCode(code string) (string, error) {
|
| 22 |
+
if code == "" {
|
| 23 |
+
return "", errors.New("无效的参数")
|
| 24 |
+
}
|
| 25 |
+
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, code), nil)
|
| 26 |
+
if err != nil {
|
| 27 |
+
return "", err
|
| 28 |
+
}
|
| 29 |
+
req.Header.Set("Authorization", common.WeChatServerToken)
|
| 30 |
+
client := http.Client{
|
| 31 |
+
Timeout: 5 * time.Second,
|
| 32 |
+
}
|
| 33 |
+
httpResponse, err := client.Do(req)
|
| 34 |
+
if err != nil {
|
| 35 |
+
return "", err
|
| 36 |
+
}
|
| 37 |
+
defer httpResponse.Body.Close()
|
| 38 |
+
var res wechatLoginResponse
|
| 39 |
+
err = json.NewDecoder(httpResponse.Body).Decode(&res)
|
| 40 |
+
if err != nil {
|
| 41 |
+
return "", err
|
| 42 |
+
}
|
| 43 |
+
if !res.Success {
|
| 44 |
+
return "", errors.New(res.Message)
|
| 45 |
+
}
|
| 46 |
+
if res.Data == "" {
|
| 47 |
+
return "", errors.New("验证码错误或已过期")
|
| 48 |
+
}
|
| 49 |
+
return res.Data, nil
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func WeChatAuth(c *gin.Context) {
|
| 53 |
+
if !common.WeChatAuthEnabled {
|
| 54 |
+
c.JSON(http.StatusOK, gin.H{
|
| 55 |
+
"message": "管理员未开启通过微信登录以及注册",
|
| 56 |
+
"success": false,
|
| 57 |
+
})
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
code := c.Query("code")
|
| 61 |
+
wechatId, err := getWeChatIdByCode(code)
|
| 62 |
+
if err != nil {
|
| 63 |
+
c.JSON(http.StatusOK, gin.H{
|
| 64 |
+
"message": err.Error(),
|
| 65 |
+
"success": false,
|
| 66 |
+
})
|
| 67 |
+
return
|
| 68 |
+
}
|
| 69 |
+
user := model.User{
|
| 70 |
+
WeChatId: wechatId,
|
| 71 |
+
}
|
| 72 |
+
if model.IsWeChatIdAlreadyTaken(wechatId) {
|
| 73 |
+
err := user.FillUserByWeChatId()
|
| 74 |
+
if err != nil {
|
| 75 |
+
c.JSON(http.StatusOK, gin.H{
|
| 76 |
+
"success": false,
|
| 77 |
+
"message": err.Error(),
|
| 78 |
+
})
|
| 79 |
+
return
|
| 80 |
+
}
|
| 81 |
+
} else {
|
| 82 |
+
if common.RegisterEnabled {
|
| 83 |
+
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
|
| 84 |
+
user.DisplayName = "WeChat User"
|
| 85 |
+
user.Role = common.RoleCommonUser
|
| 86 |
+
user.Status = common.UserStatusEnabled
|
| 87 |
+
|
| 88 |
+
if err := user.Insert(0); err != nil {
|
| 89 |
+
c.JSON(http.StatusOK, gin.H{
|
| 90 |
+
"success": false,
|
| 91 |
+
"message": err.Error(),
|
| 92 |
+
})
|
| 93 |
+
return
|
| 94 |
+
}
|
| 95 |
+
} else {
|
| 96 |
+
c.JSON(http.StatusOK, gin.H{
|
| 97 |
+
"success": false,
|
| 98 |
+
"message": "管理员关闭了新用户注册",
|
| 99 |
+
})
|
| 100 |
+
return
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if user.Status != common.UserStatusEnabled {
|
| 105 |
+
c.JSON(http.StatusOK, gin.H{
|
| 106 |
+
"message": "用户已被封禁",
|
| 107 |
+
"success": false,
|
| 108 |
+
})
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
setupLogin(&user, c)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
func WeChatBind(c *gin.Context) {
|
| 115 |
+
if !common.WeChatAuthEnabled {
|
| 116 |
+
c.JSON(http.StatusOK, gin.H{
|
| 117 |
+
"message": "管理员未开启通过微信登录以及注册",
|
| 118 |
+
"success": false,
|
| 119 |
+
})
|
| 120 |
+
return
|
| 121 |
+
}
|
| 122 |
+
code := c.Query("code")
|
| 123 |
+
wechatId, err := getWeChatIdByCode(code)
|
| 124 |
+
if err != nil {
|
| 125 |
+
c.JSON(http.StatusOK, gin.H{
|
| 126 |
+
"message": err.Error(),
|
| 127 |
+
"success": false,
|
| 128 |
+
})
|
| 129 |
+
return
|
| 130 |
+
}
|
| 131 |
+
if model.IsWeChatIdAlreadyTaken(wechatId) {
|
| 132 |
+
c.JSON(http.StatusOK, gin.H{
|
| 133 |
+
"success": false,
|
| 134 |
+
"message": "该微信账号已被绑定",
|
| 135 |
+
})
|
| 136 |
+
return
|
| 137 |
+
}
|
| 138 |
+
id := c.GetInt("id")
|
| 139 |
+
user := model.User{
|
| 140 |
+
Id: id,
|
| 141 |
+
}
|
| 142 |
+
err = user.FillUserById()
|
| 143 |
+
if err != nil {
|
| 144 |
+
c.JSON(http.StatusOK, gin.H{
|
| 145 |
+
"success": false,
|
| 146 |
+
"message": err.Error(),
|
| 147 |
+
})
|
| 148 |
+
return
|
| 149 |
+
}
|
| 150 |
+
user.WeChatId = wechatId
|
| 151 |
+
err = user.Update(false)
|
| 152 |
+
if err != nil {
|
| 153 |
+
c.JSON(http.StatusOK, gin.H{
|
| 154 |
+
"success": false,
|
| 155 |
+
"message": err.Error(),
|
| 156 |
+
})
|
| 157 |
+
return
|
| 158 |
+
}
|
| 159 |
+
c.JSON(http.StatusOK, gin.H{
|
| 160 |
+
"success": true,
|
| 161 |
+
"message": "",
|
| 162 |
+
})
|
| 163 |
+
return
|
| 164 |
+
}
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.4'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
one-api:
|
| 5 |
+
image: justsong/one-api:latest
|
| 6 |
+
container_name: one-api
|
| 7 |
+
restart: always
|
| 8 |
+
command: --log-dir /app/logs
|
| 9 |
+
ports:
|
| 10 |
+
- "3000:3000"
|
| 11 |
+
volumes:
|
| 12 |
+
- ./data:/data
|
| 13 |
+
- ./logs:/app/logs
|
| 14 |
+
environment:
|
| 15 |
+
- SQL_DSN=root:123456@tcp(host.docker.internal:3306)/one-api # 修改此行,或注释掉以使用 SQLite 作为数据库
|
| 16 |
+
- REDIS_CONN_STRING=redis://redis
|
| 17 |
+
- SESSION_SECRET=random_string # 修改为随机字符串
|
| 18 |
+
- TZ=Asia/Shanghai
|
| 19 |
+
# - NODE_TYPE=slave # 多机部署时从节点取消注释该行
|
| 20 |
+
# - SYNC_FREQUENCY=60 # 需要定期从数据库加载数据时取消注释该行
|
| 21 |
+
# - FRONTEND_BASE_URL=https://openai.justsong.cn # 多机部署时从节点取消注释该行
|
| 22 |
+
|
| 23 |
+
depends_on:
|
| 24 |
+
- redis
|
| 25 |
+
healthcheck:
|
| 26 |
+
test: [ "CMD-SHELL", "curl -s http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk '{print $2}' | grep 'true'" ]
|
| 27 |
+
interval: 30s
|
| 28 |
+
timeout: 10s
|
| 29 |
+
retries: 3
|
| 30 |
+
|
| 31 |
+
redis:
|
| 32 |
+
image: redis:latest
|
| 33 |
+
container_name: redis
|
| 34 |
+
restart: always
|
go.mod
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module one-api
|
| 2 |
+
|
| 3 |
+
// +heroku goVersion go1.18
|
| 4 |
+
go 1.18
|
| 5 |
+
|
| 6 |
+
require (
|
| 7 |
+
github.com/gin-contrib/cors v1.4.0
|
| 8 |
+
github.com/gin-contrib/gzip v0.0.6
|
| 9 |
+
github.com/gin-contrib/sessions v0.0.5
|
| 10 |
+
github.com/gin-contrib/static v0.0.1
|
| 11 |
+
github.com/gin-gonic/gin v1.9.1
|
| 12 |
+
github.com/go-playground/validator/v10 v10.14.0
|
| 13 |
+
github.com/go-redis/redis/v8 v8.11.5
|
| 14 |
+
github.com/golang-jwt/jwt v3.2.2+incompatible
|
| 15 |
+
github.com/google/uuid v1.3.0
|
| 16 |
+
github.com/gorilla/websocket v1.5.0
|
| 17 |
+
github.com/pkoukk/tiktoken-go v0.1.5
|
| 18 |
+
golang.org/x/crypto v0.9.0
|
| 19 |
+
gorm.io/driver/mysql v1.4.3
|
| 20 |
+
gorm.io/driver/sqlite v1.4.3
|
| 21 |
+
gorm.io/gorm v1.25.0
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
require (
|
| 25 |
+
github.com/bytedance/sonic v1.9.1 // indirect
|
| 26 |
+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
| 27 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
| 28 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
| 29 |
+
github.com/dlclark/regexp2 v1.10.0 // indirect
|
| 30 |
+
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
| 31 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
| 32 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
| 33 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 34 |
+
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
| 35 |
+
github.com/goccy/go-json v0.10.2 // indirect
|
| 36 |
+
github.com/gorilla/context v1.1.1 // indirect
|
| 37 |
+
github.com/gorilla/securecookie v1.1.1 // indirect
|
| 38 |
+
github.com/gorilla/sessions v1.2.1 // indirect
|
| 39 |
+
github.com/jackc/pgpassfile v1.0.0 // indirect
|
| 40 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
| 41 |
+
github.com/jackc/pgx/v5 v5.3.1 // indirect
|
| 42 |
+
github.com/jinzhu/inflection v1.0.0 // indirect
|
| 43 |
+
github.com/jinzhu/now v1.1.5 // indirect
|
| 44 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 45 |
+
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
| 46 |
+
github.com/leodido/go-urn v1.2.4 // indirect
|
| 47 |
+
github.com/mattn/go-isatty v0.0.19 // indirect
|
| 48 |
+
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
| 49 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 50 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 51 |
+
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
| 52 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 53 |
+
github.com/ugorji/go/codec v1.2.11 // indirect
|
| 54 |
+
golang.org/x/arch v0.3.0 // indirect
|
| 55 |
+
golang.org/x/net v0.10.0 // indirect
|
| 56 |
+
golang.org/x/sys v0.8.0 // indirect
|
| 57 |
+
golang.org/x/text v0.9.0 // indirect
|
| 58 |
+
google.golang.org/protobuf v1.30.0 // indirect
|
| 59 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 60 |
+
gorm.io/driver/postgres v1.5.2 // indirect
|
| 61 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
| 2 |
+
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
| 3 |
+
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
| 4 |
+
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
| 5 |
+
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
| 6 |
+
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
| 7 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
| 8 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
| 9 |
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
| 10 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 11 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 12 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 13 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
| 14 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
| 15 |
+
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
| 16 |
+
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
| 17 |
+
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
| 18 |
+
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
| 19 |
+
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
| 20 |
+
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
| 21 |
+
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
| 22 |
+
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
| 23 |
+
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
| 24 |
+
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
| 25 |
+
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
| 26 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
| 27 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
| 28 |
+
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
| 29 |
+
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
| 30 |
+
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
| 31 |
+
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
| 32 |
+
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
| 33 |
+
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
| 34 |
+
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 35 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
| 36 |
+
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
| 37 |
+
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
| 38 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 39 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 40 |
+
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
| 41 |
+
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
| 42 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 43 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 44 |
+
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
| 45 |
+
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
| 46 |
+
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
| 47 |
+
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
| 48 |
+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
| 49 |
+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
| 50 |
+
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
| 51 |
+
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
| 52 |
+
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
| 53 |
+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
| 54 |
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
| 55 |
+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
| 56 |
+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
| 57 |
+
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
| 58 |
+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
| 59 |
+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
| 60 |
+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 61 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 62 |
+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
| 63 |
+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 64 |
+
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
| 65 |
+
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
| 66 |
+
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
| 67 |
+
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
| 68 |
+
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
| 69 |
+
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
| 70 |
+
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
| 71 |
+
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
| 72 |
+
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
| 73 |
+
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
| 74 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
| 75 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
| 76 |
+
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
| 77 |
+
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
| 78 |
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
| 79 |
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
| 80 |
+
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 81 |
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
| 82 |
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 83 |
+
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
| 84 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 85 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 86 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 87 |
+
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
| 88 |
+
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
| 89 |
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
| 90 |
+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
| 91 |
+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
| 92 |
+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
| 93 |
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
| 94 |
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
| 95 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 96 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 97 |
+
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
| 98 |
+
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
| 99 |
+
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
| 100 |
+
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
| 101 |
+
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
| 102 |
+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
| 103 |
+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
| 104 |
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 105 |
+
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
| 106 |
+
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
| 107 |
+
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
| 108 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 109 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 110 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 111 |
+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
| 112 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 113 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 114 |
+
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
| 115 |
+
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
| 116 |
+
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
| 117 |
+
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
| 118 |
+
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
| 119 |
+
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
| 120 |
+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
| 121 |
+
github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4=
|
| 122 |
+
github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
| 123 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 124 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 125 |
+
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
| 126 |
+
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
| 127 |
+
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
| 128 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 129 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 130 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 131 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 132 |
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
| 133 |
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 134 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 135 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 136 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 137 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 138 |
+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 139 |
+
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
| 140 |
+
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 141 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 142 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 143 |
+
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
| 144 |
+
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
| 145 |
+
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
| 146 |
+
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
| 147 |
+
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
| 148 |
+
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
| 149 |
+
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 150 |
+
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
| 151 |
+
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 152 |
+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
| 153 |
+
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
| 154 |
+
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
| 155 |
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
| 156 |
+
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
| 157 |
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
| 158 |
+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 159 |
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 160 |
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 161 |
+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 162 |
+
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 163 |
+
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 164 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 165 |
+
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
| 166 |
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 167 |
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
| 168 |
+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
| 169 |
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 170 |
+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 171 |
+
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
| 172 |
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
| 173 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 174 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
| 175 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 176 |
+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
| 177 |
+
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
| 178 |
+
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
| 179 |
+
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
| 180 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 181 |
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 182 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 183 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 184 |
+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
| 185 |
+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
| 186 |
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 187 |
+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 188 |
+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
| 189 |
+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
| 190 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 191 |
+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 192 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 193 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 194 |
+
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
| 195 |
+
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
| 196 |
+
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
| 197 |
+
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
| 198 |
+
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
| 199 |
+
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
| 200 |
+
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
| 201 |
+
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
| 202 |
+
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
| 203 |
+
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
| 204 |
+
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
| 205 |
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|