mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-12-17 13:35:57 +08:00
Compare commits
6 Commits
v0.5.6-alp
...
v0.5.7-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d34b7a77e | ||
|
|
cbd62011b8 | ||
|
|
4701897e2e | ||
|
|
0f6c132a80 | ||
|
|
3cac45dc85 | ||
|
|
47c08c72ce |
28
README.md
28
README.md
@@ -106,11 +106,17 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
|
```shell
|
||||||
|
# 使用 SQLite 的部署命令:
|
||||||
|
docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
|
||||||
|
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数,不清楚如何修改请参见下面环境变量一节。
|
||||||
|
# 例如:
|
||||||
|
docker run --name one-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
|
||||||
|
```
|
||||||
|
|
||||||
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
|
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
|
||||||
|
|
||||||
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
|
数据和日志将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
|
||||||
|
|
||||||
如果启动失败,请添加 `--privileged=true`,具体参考 https://github.com/songquanpeng/one-api/issues/482 。
|
如果启动失败,请添加 `--privileged=true`,具体参考 https://github.com/songquanpeng/one-api/issues/482 。
|
||||||
|
|
||||||
@@ -239,7 +245,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
|||||||
<summary><strong>部署到 Zeabur</strong></summary>
|
<summary><strong>部署到 Zeabur</strong></summary>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
|
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用
|
||||||
|
|
||||||
1. 首先 fork 一份代码。
|
1. 首先 fork 一份代码。
|
||||||
2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
|
2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
|
||||||
@@ -254,6 +260,17 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>部署到 Render</strong></summary>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
> Render 提供免费额度,绑卡后可以进一步提升额度
|
||||||
|
|
||||||
|
Render 可以直接部署 docker 镜像,不需要 fork 仓库:https://dashboard.render.com
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
系统本身开箱即用。
|
系统本身开箱即用。
|
||||||
|
|
||||||
@@ -281,10 +298,11 @@ OPENAI_API_BASE="https://<HOST>:<PORT>/v1"
|
|||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
A(用户)
|
A(用户)
|
||||||
A --->|请求| B(One API)
|
A --->|使用 One API 分发的 key 进行请求| B(One API)
|
||||||
B -->|中继请求| C(OpenAI)
|
B -->|中继请求| C(OpenAI)
|
||||||
B -->|中继请求| D(Azure)
|
B -->|中继请求| D(Azure)
|
||||||
B -->|中继请求| E(其他下游渠道)
|
B -->|中继请求| E(其他 OpenAI API 格式下游渠道)
|
||||||
|
B -->|中继并修改请求体和返回体| F(非 OpenAI API 格式下游渠道)
|
||||||
```
|
```
|
||||||
|
|
||||||
可以通过在令牌后面添加渠道 ID 的方式指定使用哪一个渠道处理本次请求,例如:`Authorization: Bearer ONE_API_KEY-CHANNEL_ID`。
|
可以通过在令牌后面添加渠道 ID 的方式指定使用哪一个渠道处理本次请求,例如:`Authorization: Bearer ONE_API_KEY-CHANNEL_ID`。
|
||||||
|
|||||||
@@ -156,9 +156,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelStatusUnknown = 0
|
ChannelStatusUnknown = 0
|
||||||
ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
|
ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
|
||||||
ChannelStatusDisabled = 2 // also don't use 0
|
ChannelStatusManuallyDisabled = 2 // also don't use 0
|
||||||
|
ChannelStatusAutoDisabled = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func disableChannel(channelId int, channelName string, reason string) {
|
|||||||
if common.RootUserEmail == "" {
|
if common.RootUserEmail == "" {
|
||||||
common.RootUserEmail = model.GetRootUserEmail()
|
common.RootUserEmail = model.GetRootUserEmail()
|
||||||
}
|
}
|
||||||
model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled)
|
model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
|
||||||
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
|
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
|
||||||
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
|
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
|
||||||
err := common.SendEmail(subject, common.RootUserEmail, content)
|
err := common.SendEmail(subject, common.RootUserEmail, content)
|
||||||
|
|||||||
@@ -127,6 +127,23 @@ func DeleteChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteManuallyDisabledChannel(c *gin.Context) {
|
||||||
|
rows, err := model.DeleteChannelByStatus(common.ChannelStatusManuallyDisabled)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": rows,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateChannel(c *gin.Context) {
|
func UpdateChannel(c *gin.Context) {
|
||||||
channel := model.Channel{}
|
channel := model.Channel{}
|
||||||
err := c.ShouldBindJSON(&channel)
|
err := c.ShouldBindJSON(&channel)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -31,6 +32,9 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
if userQuota-preConsumedQuota < 0 {
|
||||||
|
return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
||||||
|
}
|
||||||
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
quota := int(ratio*sizeRatio*1000) * imageRequest.N
|
quota := int(ratio*sizeRatio*1000) * imageRequest.N
|
||||||
|
|
||||||
if consumeQuota && userQuota-quota < 0 {
|
if consumeQuota && userQuota-quota < 0 {
|
||||||
return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
|
return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||||
|
|||||||
@@ -204,6 +204,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
if userQuota-preConsumedQuota < 0 {
|
||||||
|
return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
||||||
|
}
|
||||||
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -176,3 +176,8 @@ func updateChannelUsedQuota(id int, quota int) {
|
|||||||
common.SysError("failed to update channel used quota: " + err.Error())
|
common.SysError("failed to update channel used quota: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteChannelByStatus(status int64) (int64, error) {
|
||||||
|
result := DB.Where("status = ?", status).Delete(&Channel{})
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|||||||
10
model/log.go
10
model/log.go
@@ -8,14 +8,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Log struct {
|
type Log struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id;index:idx_created_at_id,priority:1"`
|
||||||
UserId int `json:"user_id" gorm:"index"`
|
UserId int `json:"user_id" gorm:"index"`
|
||||||
CreatedAt int64 `json:"created_at" gorm:"bigint;index"`
|
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;index:idx_created_at_type"`
|
||||||
Type int `json:"type" gorm:"index"`
|
Type int `json:"type" gorm:"index:idx_created_at_type"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Username string `json:"username" gorm:"index;default:''"`
|
Username string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"`
|
||||||
TokenName string `json:"token_name" gorm:"index;default:''"`
|
TokenName string `json:"token_name" gorm:"index;default:''"`
|
||||||
ModelName string `json:"model_name" gorm:"index;default:''"`
|
ModelName string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
|
||||||
Quota int `json:"quota" gorm:"default:0"`
|
Quota int `json:"quota" gorm:"default:0"`
|
||||||
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
|
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
|
||||||
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
|
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func InitDB() (err error) {
|
|||||||
if !common.IsMasterNode {
|
if !common.IsMasterNode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
common.SysLog("database migration started")
|
||||||
err = db.AutoMigrate(&Channel{})
|
err = db.AutoMigrate(&Channel{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
|
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
|
||||||
channelRoute.POST("/", controller.AddChannel)
|
channelRoute.POST("/", controller.AddChannel)
|
||||||
channelRoute.PUT("/", controller.UpdateChannel)
|
channelRoute.PUT("/", controller.UpdateChannel)
|
||||||
|
channelRoute.DELETE("/manually_disabled", controller.DeleteManuallyDisabledChannel)
|
||||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
||||||
}
|
}
|
||||||
tokenRoute := apiRouter.Group("/token")
|
tokenRoute := apiRouter.Group("/token")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {Button, Form, Input, Label, Pagination, Popup, Table} from 'semantic-ui-react';
|
import { Button, Form, Input, Label, Pagination, Popup, Table } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showNotice, showSuccess, timestamp2string } from '../helpers';
|
import { API, showError, showInfo, showNotice, showSuccess, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ const ChannelsTable = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const manageChannel = async (id, action, idx, priority) => {
|
const manageChannel = async (id, action, idx, value) => {
|
||||||
let data = { id };
|
let data = { id };
|
||||||
let res;
|
let res;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -112,10 +112,20 @@ const ChannelsTable = () => {
|
|||||||
res = await API.put('/api/channel/', data);
|
res = await API.put('/api/channel/', data);
|
||||||
break;
|
break;
|
||||||
case 'priority':
|
case 'priority':
|
||||||
if (priority === '') {
|
if (value === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.priority = parseInt(priority);
|
data.priority = parseInt(value);
|
||||||
|
res = await API.put('/api/channel/', data);
|
||||||
|
break;
|
||||||
|
case 'weight':
|
||||||
|
if (value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.weight = parseInt(value);
|
||||||
|
if (data.weight < 0) {
|
||||||
|
data.weight = 0;
|
||||||
|
}
|
||||||
res = await API.put('/api/channel/', data);
|
res = await API.put('/api/channel/', data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -142,9 +152,23 @@ const ChannelsTable = () => {
|
|||||||
return <Label basic color='green'>已启用</Label>;
|
return <Label basic color='green'>已启用</Label>;
|
||||||
case 2:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<Label basic color='red'>
|
<Popup
|
||||||
已禁用
|
trigger={<Label basic color='red'>
|
||||||
</Label>
|
已禁用
|
||||||
|
</Label>}
|
||||||
|
content='本渠道被手动禁用'
|
||||||
|
basic
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
trigger={<Label basic color='yellow'>
|
||||||
|
已禁用
|
||||||
|
</Label>}
|
||||||
|
content='本渠道被程序自动禁用'
|
||||||
|
basic
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
@@ -202,7 +226,7 @@ const ChannelsTable = () => {
|
|||||||
showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
|
showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
showNotice("当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。")
|
showNotice('当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,6 +240,17 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteAllManuallyDisabledChannels = async () => {
|
||||||
|
const res = await API.delete(`/api/channel/manually_disabled`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(`已删除所有手动禁用渠道,共计 ${data} 个`);
|
||||||
|
await refresh();
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateChannelBalance = async (id, name, idx) => {
|
const updateChannelBalance = async (id, name, idx) => {
|
||||||
const res = await API.get(`/api/channel/update_balance/${id}/`);
|
const res = await API.get(`/api/channel/update_balance/${id}/`);
|
||||||
const { success, message, balance } = res.data;
|
const { success, message, balance } = res.data;
|
||||||
@@ -343,10 +378,10 @@ const ChannelsTable = () => {
|
|||||||
余额
|
余额
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortChannel('priority');
|
sortChannel('priority');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
优先级
|
优先级
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
@@ -390,18 +425,18 @@ const ChannelsTable = () => {
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Popup
|
<Popup
|
||||||
trigger={<Input type="number" defaultValue={channel.priority} onBlur={(event) => {
|
trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => {
|
||||||
manageChannel(
|
manageChannel(
|
||||||
channel.id,
|
channel.id,
|
||||||
'priority',
|
'priority',
|
||||||
idx,
|
idx,
|
||||||
event.target.value,
|
event.target.value
|
||||||
);
|
);
|
||||||
}}>
|
}}>
|
||||||
<input style={{maxWidth:'60px'}} />
|
<input style={{ maxWidth: '60px' }} />
|
||||||
</Input>}
|
</Input>}
|
||||||
content='渠道选择优先级,越高越优先'
|
content='渠道选择优先级,越高越优先'
|
||||||
basic
|
basic
|
||||||
/>
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@@ -481,6 +516,20 @@ const ChannelsTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button size='small' onClick={updateAllChannelsBalance}
|
<Button size='small' onClick={updateAllChannelsBalance}
|
||||||
loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
|
loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
|
||||||
|
<Popup
|
||||||
|
trigger={
|
||||||
|
<Button size='small' loading={loading}>
|
||||||
|
删除所有手动禁用渠道
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Button size='small' loading={loading} negative onClick={deleteAllManuallyDisabledChannels}>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Pagination
|
<Pagination
|
||||||
floated='right'
|
floated='right'
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
|
|||||||
Reference in New Issue
Block a user