mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-10-23 09:53:42 +08:00
Compare commits
8 Commits
v0.4.0-alp
...
v0.4.1-alp
Author | SHA1 | Date | |
---|---|---|---|
|
145bb14cb2 | ||
|
8901f03864 | ||
|
813bf0bd66 | ||
|
45e9fd66e7 | ||
|
e0d0674f81 | ||
|
4b6adaec0b | ||
|
9301b3fed3 | ||
|
c6edb78ac9 |
23
README.md
23
README.md
@@ -44,12 +44,13 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
||||
<a href="https://iamazing.cn/page/reward">赞赏支持</a>
|
||||
</p>
|
||||
|
||||
> **Warning**:从 `v0.2` 版本升级到 `v0.3` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.2-v0.3.sql)。
|
||||
> **Warning**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
|
||||
|
||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
||||
|
||||
## 功能
|
||||
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
||||
+ [x] OpenAI 官方通道
|
||||
+ [x] OpenAI 官方通道(支持配置代理)
|
||||
+ [x] **Azure OpenAI API**
|
||||
+ [x] [API2D](https://api2d.com/r/197971)
|
||||
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
|
||||
@@ -58,23 +59,25 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
||||
+ [x] [OpenAI Max](https://openaimax.com)
|
||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||
+ [x] [CloseAI](https://console.openai-asia.com/r/2412)
|
||||
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
||||
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
||||
2. 支持通过**负载均衡**的方式访问多个渠道。
|
||||
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||
4. 支持**多机部署**,[详见此处](#多机部署)。
|
||||
5. 支持**令牌管理**,设置令牌的过期时间和使用次数。
|
||||
6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
|
||||
7. 支持**通道管理**,批量创建通道。
|
||||
8. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||
9. 支持丰富的**自定义**设置,
|
||||
1. 支持自定义系统名称,logo 以及页脚。
|
||||
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||
10. 支持通过系统访问令牌访问管理 API。
|
||||
11. 支持用户管理,支持**多种用户登录注册方式**:
|
||||
8. 支持**用户分组**以及**渠道分组**。
|
||||
9. 支持渠道**设置模型列表**。
|
||||
10. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||
11. 支持丰富的**自定义**设置,
|
||||
1. 支持自定义系统名称,logo 以及页脚。
|
||||
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||
12. 支持通过系统访问令牌访问管理 API。
|
||||
13. 支持用户管理,支持**多种用户登录注册方式**:
|
||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||
12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
||||
14. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
||||
|
||||
## 部署
|
||||
### 基于 Docker 进行部署
|
||||
|
17
bin/migration_v0.3-v0.4.sql
Normal file
17
bin/migration_v0.3-v0.4.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
INSERT INTO abilities (`group`, model, channel_id, enabled)
|
||||
SELECT c.`group`, m.model, c.id, 1
|
||||
FROM channels c
|
||||
CROSS JOIN (
|
||||
SELECT 'gpt-3.5-turbo' AS model UNION ALL
|
||||
SELECT 'gpt-3.5-turbo-0301' AS model UNION ALL
|
||||
SELECT 'gpt-4' AS model UNION ALL
|
||||
SELECT 'gpt-4-0314' AS model
|
||||
) AS m
|
||||
WHERE c.status = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM abilities a
|
||||
WHERE a.`group` = c.`group`
|
||||
AND a.model = m.model
|
||||
AND a.channel_id = c.id
|
||||
);
|
@@ -25,6 +25,7 @@ var OptionMap map[string]string
|
||||
var OptionMapRWMutex sync.RWMutex
|
||||
|
||||
var ItemsPerPage = 10
|
||||
var MaxRecentItems = 100
|
||||
|
||||
var PasswordLoginEnabled = true
|
||||
var PasswordRegisterEnabled = true
|
||||
|
@@ -10,7 +10,7 @@ var ModelRatio = map[string]float64{
|
||||
"gpt-4-0314": 15,
|
||||
"gpt-4-32k": 30,
|
||||
"gpt-4-32k-0314": 30,
|
||||
"gpt-3.5-turbo": 1,
|
||||
"gpt-3.5-turbo": 1, // $0.002 / 1K tokens
|
||||
"gpt-3.5-turbo-0301": 1,
|
||||
"text-ada-001": 0.2,
|
||||
"text-babbage-001": 0.25,
|
||||
|
@@ -41,7 +41,9 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
||||
switch channel.Type {
|
||||
case common.ChannelTypeOpenAI:
|
||||
// do nothing
|
||||
if channel.BaseURL != "" {
|
||||
baseURL = channel.BaseURL
|
||||
}
|
||||
case common.ChannelTypeAzure:
|
||||
return 0, errors.New("尚未实现")
|
||||
case common.ChannelTypeCustom:
|
||||
|
@@ -27,6 +27,8 @@ func testChannel(channel *model.Channel, request *ChatRequest) error {
|
||||
} else {
|
||||
if channel.Type == common.ChannelTypeCustom {
|
||||
requestURL = channel.BaseURL
|
||||
} else if channel.Type == common.ChannelTypeOpenAI && channel.BaseURL != "" {
|
||||
requestURL = channel.BaseURL
|
||||
}
|
||||
requestURL += "/v1/chat/completions"
|
||||
}
|
||||
@@ -56,7 +58,7 @@ func testChannel(channel *model.Channel, request *ChatRequest) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.Error.Message != "" || response.Error.Code != "" {
|
||||
if response.Usage.CompletionTokens == 0 {
|
||||
return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
|
||||
}
|
||||
return nil
|
||||
|
86
controller/log.go
Normal file
86
controller/log.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetAllLogs(c *gin.Context) {
|
||||
p, _ := strconv.Atoi(c.Query("p"))
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
logType, _ := strconv.Atoi(c.Query("type"))
|
||||
logs, err := model.GetAllLogs(logType, p*common.ItemsPerPage, common.ItemsPerPage)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserLogs(c *gin.Context) {
|
||||
p, _ := strconv.Atoi(c.Query("p"))
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
userId := c.GetInt("id")
|
||||
logType, _ := strconv.Atoi(c.Query("type"))
|
||||
logs, err := model.GetUserLogs(userId, logType, p*common.ItemsPerPage, common.ItemsPerPage)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
func SearchAllLogs(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
logs, err := model.SearchAllLogs(keyword)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
func SearchUserLogs(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
userId := c.GetInt("id")
|
||||
logs, err := model.SearchUserLogs(userId, keyword)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": logs,
|
||||
})
|
||||
}
|
@@ -116,6 +116,51 @@ func init() {
|
||||
Root: "text-embedding-ada-002",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-davinci-003",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-davinci-003",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-davinci-002",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-davinci-002",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-curie-001",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-curie-001",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-babbage-001",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-babbage-001",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-ada-001",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-ada-001",
|
||||
Parent: nil,
|
||||
},
|
||||
}
|
||||
openAIModelsMap = make(map[string]OpenAIModels)
|
||||
for _, model := range openAIModels {
|
||||
|
@@ -19,6 +19,13 @@ type Message struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
RelayModeUnknown = iota
|
||||
RelayModeChatCompletions
|
||||
RelayModeCompletions
|
||||
RelayModeEmbeddings
|
||||
)
|
||||
|
||||
// https://platform.openai.com/docs/api-reference/chat
|
||||
|
||||
type GeneralOpenAIRequest struct {
|
||||
@@ -69,7 +76,7 @@ type TextResponse struct {
|
||||
Error OpenAIError `json:"error"`
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
type ChatCompletionsStreamResponse struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
@@ -78,8 +85,23 @@ type StreamResponse struct {
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
type CompletionsStreamResponse struct {
|
||||
Choices []struct {
|
||||
Text string `json:"text"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
func Relay(c *gin.Context) {
|
||||
err := relayHelper(c)
|
||||
relayMode := RelayModeUnknown
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") {
|
||||
relayMode = RelayModeChatCompletions
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") {
|
||||
relayMode = RelayModeCompletions
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
||||
relayMode = RelayModeEmbeddings
|
||||
}
|
||||
err := relayHelper(c, relayMode)
|
||||
if err != nil {
|
||||
if err.StatusCode == http.StatusTooManyRequests {
|
||||
err.OpenAIError.Message = "负载已满,请稍后再试,或升级账户以提升服务质量。"
|
||||
@@ -110,7 +132,7 @@ func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatus
|
||||
}
|
||||
}
|
||||
|
||||
func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode {
|
||||
func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
channelType := c.GetInt("channel")
|
||||
tokenId := c.GetInt("token_id")
|
||||
consumeQuota := c.GetBool("consume_quota")
|
||||
@@ -125,6 +147,10 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode {
|
||||
requestURL := c.Request.URL.String()
|
||||
if channelType == common.ChannelTypeCustom {
|
||||
baseURL = c.GetString("base_url")
|
||||
} else if channelType == common.ChannelTypeOpenAI {
|
||||
if c.GetString("base_url") != "" {
|
||||
baseURL = c.GetString("base_url")
|
||||
}
|
||||
}
|
||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||
if channelType == common.ChannelTypeAzure {
|
||||
@@ -148,8 +174,13 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode {
|
||||
err := relayPaLM(textRequest, c)
|
||||
return err
|
||||
}
|
||||
|
||||
promptTokens := countTokenMessages(textRequest.Messages, textRequest.Model)
|
||||
var promptTokens int
|
||||
switch relayMode {
|
||||
case RelayModeChatCompletions:
|
||||
promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model)
|
||||
case RelayModeCompletions:
|
||||
promptTokens = countTokenText(textRequest.Prompt, textRequest.Model)
|
||||
}
|
||||
preConsumedTokens := common.PreConsumedQuota
|
||||
if textRequest.MaxTokens != 0 {
|
||||
preConsumedTokens = promptTokens + textRequest.MaxTokens
|
||||
@@ -245,14 +276,27 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode {
|
||||
dataChan <- data
|
||||
data = data[6:]
|
||||
if !strings.HasPrefix(data, "[DONE]") {
|
||||
var streamResponse StreamResponse
|
||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||
if err != nil {
|
||||
common.SysError("Error unmarshalling stream response: " + err.Error())
|
||||
return
|
||||
}
|
||||
for _, choice := range streamResponse.Choices {
|
||||
streamResponseText += choice.Delta.Content
|
||||
switch relayMode {
|
||||
case RelayModeChatCompletions:
|
||||
var streamResponse ChatCompletionsStreamResponse
|
||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||
if err != nil {
|
||||
common.SysError("Error unmarshalling stream response: " + err.Error())
|
||||
return
|
||||
}
|
||||
for _, choice := range streamResponse.Choices {
|
||||
streamResponseText += choice.Delta.Content
|
||||
}
|
||||
case RelayModeCompletions:
|
||||
var streamResponse CompletionsStreamResponse
|
||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||
if err != nil {
|
||||
common.SysError("Error unmarshalling stream response: " + err.Error())
|
||||
return
|
||||
}
|
||||
for _, choice := range streamResponse.Choices {
|
||||
streamResponseText += choice.Text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -82,11 +82,9 @@ func Distribute() func(c *gin.Context) {
|
||||
c.Set("channel_id", channel.Id)
|
||||
c.Set("channel_name", channel.Name)
|
||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||
if channel.Type == common.ChannelTypeCustom || channel.Type == common.ChannelTypeAzure {
|
||||
c.Set("base_url", channel.BaseURL)
|
||||
if channel.Type == common.ChannelTypeAzure {
|
||||
c.Set("api_version", channel.Other)
|
||||
}
|
||||
c.Set("base_url", channel.BaseURL)
|
||||
if channel.Type == common.ChannelTypeAzure {
|
||||
c.Set("api_version", channel.Other)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
@@ -13,9 +13,6 @@ type Ability struct {
|
||||
}
|
||||
|
||||
func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
|
||||
if group == "default" {
|
||||
return GetRandomChannel()
|
||||
}
|
||||
ability := Ability{}
|
||||
var err error = nil
|
||||
if common.UsingSQLite {
|
||||
|
44
model/log.go
Normal file
44
model/log.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import "one-api/common"
|
||||
|
||||
type Log struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"bigint"`
|
||||
Type int `json:"type" gorm:"index"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func RecordLog(userId int, logType int, content string) {
|
||||
log := &Log{
|
||||
UserId: userId,
|
||||
CreatedAt: common.GetTimestamp(),
|
||||
Type: logType,
|
||||
Content: content,
|
||||
}
|
||||
err := DB.Create(log).Error
|
||||
if err != nil {
|
||||
common.SysError("failed to record log: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) {
|
||||
err = DB.Where("type = ?", logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
||||
|
||||
func GetUserLogs(userId int, logType int, startIdx int, num int) (logs []*Log, err error) {
|
||||
err = DB.Where("user_id = ? and type = ?", userId, logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
||||
|
||||
func SearchAllLogs(keyword string) (logs []*Log, err error) {
|
||||
err = DB.Where("type = ? or content LIKE ?", keyword, keyword+"%").Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
||||
|
||||
func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) {
|
||||
err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
@@ -83,7 +83,7 @@ func (redemption *Redemption) SelectUpdate() error {
|
||||
// Update Make sure your token's fields is completed, because this will update non-zero values
|
||||
func (redemption *Redemption) Update() error {
|
||||
var err error
|
||||
err = DB.Model(redemption).Select("name", "status", "redeemed_time").Updates(redemption).Error
|
||||
err = DB.Model(redemption).Select("name", "status", "quota", "redeemed_time").Updates(redemption).Error
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -93,5 +93,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
redemptionRoute.PUT("/", controller.UpdateRedemption)
|
||||
redemptionRoute.DELETE("/:id", controller.DeleteRedemption)
|
||||
}
|
||||
logRoute := apiRouter.Group("/log")
|
||||
logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs)
|
||||
logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs)
|
||||
logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs)
|
||||
logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs)
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ func SetRelayRouter(router *gin.Engine) {
|
||||
relayV1Router := router.Group("/v1")
|
||||
relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
|
||||
{
|
||||
relayV1Router.POST("/completions", controller.RelayNotImplemented)
|
||||
relayV1Router.POST("/completions", controller.Relay)
|
||||
relayV1Router.POST("/chat/completions", controller.Relay)
|
||||
relayV1Router.POST("/edits", controller.RelayNotImplemented)
|
||||
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
||||
|
@@ -198,6 +198,20 @@ const EditChannel = () => {
|
||||
handleInputChange(null, { name: 'models', value: fullModels });
|
||||
}}>填入所有模型</Button>
|
||||
</div>
|
||||
{
|
||||
inputs.type === 1 && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='代理'
|
||||
name='base_url'
|
||||
placeholder={'请输入 OpenAI API 代理地址,如果不需要请留空,格式为:https://api.openai.com'}
|
||||
onChange={handleInputChange}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
)
|
||||
}
|
||||
{
|
||||
batch ? <Form.Field>
|
||||
<Form.TextArea
|
||||
|
Reference in New Issue
Block a user