Compare commits

...

112 Commits

Author SHA1 Message Date
wozulong
333849429b merge upstream
Signed-off-by: wozulong <>
2024-09-23 11:15:45 +08:00
1808837298@qq.com
50eab6b4e4 chore: 更新令牌分组描述 2024-09-22 19:43:06 +08:00
1808837298@qq.com
ed972eef06 feat: pricing page support multi groups #487 2024-09-22 17:44:57 +08:00
CalciumIon
c6ff785a83 feat: 无可选分组时关闭令牌分组功能 #485 2024-09-19 03:01:33 +08:00
CalciumIon
2e734e0c37 chore: 令牌分组描述歧义 2024-09-19 02:52:25 +08:00
CalciumIon
af33f36c7b feat: update gemini flash completion ratio #479 2024-09-18 20:39:06 +08:00
CalciumIon
3aa86a8cd9 feat: update gemini completion ratio #479 2024-09-18 20:37:22 +08:00
CalciumIon
af7fecbfa7 fix: 使用令牌分组时 "/v1/models" 返回模型不正确 #481 2024-09-18 19:19:37 +08:00
CalciumIon
3fbdd502b6 fix: token group #477 2024-09-18 18:55:11 +08:00
CalciumIon
052bc2075b feat: 令牌分组 2024-09-18 05:19:49 +08:00
Calcium-Ion
5f3798053f Create FUNDING.yml 2024-09-18 01:41:31 +08:00
CalciumIon
e31022c676 Update logo 2024-09-18 01:25:00 +08:00
Calcium-Ion
fff7609f06 Merge pull request #439 from guoruqiang/main
改进了聊天页面,增加了初始令牌,方便用户注册后即可使用聊天功能。
2024-09-17 23:14:19 +08:00
CalciumIon
9032b5cfbf fix: 初始令牌 2024-09-17 23:07:16 +08:00
CalciumIon
131453dac8 Update README.md 2024-09-17 23:01:34 +08:00
CalciumIon
ed948c121a Merge branch 'main' into g-main
# Conflicts:
#	web/src/App.js
2024-09-17 22:50:59 +08:00
CalciumIon
a03cd15505 fix: '/v1/models' #474 2024-09-17 22:41:54 +08:00
CalciumIon
02f5137781 fix: '/v1/models' #474 2024-09-17 22:39:58 +08:00
CalciumIon
e6df0ed20c fix: '/vi/models' #474 2024-09-17 22:36:20 +08:00
CalciumIon
f505afdc10 feat: 添加令牌ip白名单功能 2024-09-17 20:49:51 +08:00
CalciumIon
feb1d76942 feat: 优化界面显示 2024-09-17 19:55:18 +08:00
CalciumIon
6263616cd9 Update README.md 2024-09-17 03:18:12 +08:00
wozulong
9b14c5da63 merge upstream
Signed-off-by: wozulong <>
2024-09-15 17:30:59 +08:00
GuoRuqiang
6bbf1d4843 Merge branch 'Calcium-Ion:main' into main 2024-09-14 19:00:03 +08:00
1808837298@qq.com
13c993d87e feat: format o1 model max tokens param 2024-09-14 16:11:38 +08:00
CalciumIon
cb73889353 feat: support o1 channel test 2024-09-13 03:17:04 +08:00
CalciumIon
804aad3f37 feat: support o1 channel test 2024-09-13 03:15:32 +08:00
CalciumIon
3af62a3efa feat: support OpenAI o1-preview and o1-mini 2024-09-13 01:22:27 +08:00
CalciumIon
be54369c12 chore: update footer 2024-09-12 18:43:01 +08:00
CalciumIon
0cbf8e07e7 feat: support ollama multi-text embedding 2024-09-12 18:29:45 +08:00
Calcium-Ion
1675679be9 Merge pull request #464 from Yan-Zero/main
fix: tool use in claude and add gemini mapping
2024-09-12 05:04:19 +08:00
Yan
0b5f2a7089 add gemini exp 2024-09-11 19:37:03 +08:00
Yan Tau
b5bb708072 Merge branch 'Calcium-Ion:main' into main 2024-09-11 19:29:50 +08:00
CalciumIon
2650ec9b59 feat: claude response return model name 2024-09-11 19:12:55 +08:00
CalciumIon
d168a685c1 fix: cohere SafetyMode 2024-09-11 19:12:32 +08:00
wozulong
a9e3555cac update
Signed-off-by: wozulong <>
2024-09-10 17:38:30 +08:00
wozulong
8d83630d85 merge upstream
Signed-off-by: wozulong <>
2024-09-10 17:28:04 +08:00
wozulong
a60f209c85 optimized blocking issue during bulk log data deletion
Signed-off-by: wozulong <>
2024-09-10 17:24:44 +08:00
GuoRuqiang
a0d20896b3 Merge branch 'Calcium-Ion:main' into main 2024-09-08 15:56:54 +08:00
Calcium-Ion
5cab06d1ce Merge pull request #459 from HynoR/main
chore: 适配cohere的safety参数
2024-09-05 18:37:47 +08:00
CalciumIon
e3b3fdec48 feat: update chatgpt-4o token encoder 2024-09-05 18:35:34 +08:00
CalciumIon
5863aa8061 feat: remove lobe chat link #457 2024-09-05 18:34:04 +08:00
wozulong
208bc5e794 merge upstream
Signed-off-by: wozulong <>
2024-09-05 17:36:17 +08:00
Yan
0ada2371b6 fix: tool use in claude 2024-09-05 00:53:00 +08:00
CalciumIon
8bc1e956cf fix: email 2024-09-04 19:44:29 +08:00
GuoRuqiang
a0673ef2b6 Merge branch 'Calcium-Ion:main' into main 2024-09-02 21:53:54 +08:00
HynoR
416f831a6c Merge remote-tracking branch 'origin/main' 2024-09-02 06:47:58 +07:00
HynoR
0b4317ce28 Update Cohere Safety Setting 2024-09-02 06:47:49 +07:00
Calcium-Ion
12e2481acb Merge pull request #451 from Nana7mi1/main
feat: support more zhipu models
2024-09-02 01:12:10 +08:00
Calcium-Ion
270709064d Merge pull request #455 from HynoR/feat/cohere-update
Feat: 更新Cohere新模型和定价
2024-09-02 01:11:55 +08:00
CalciumIon
0830ef3305 feat: support jina embedding 2024-09-02 01:11:19 +08:00
HynoR
722cc174b7 Cohere Update 2024-09-01 15:21:05 +07:00
Nanami
97c18d0c7f feat: support more zhipu models 2024-08-31 10:20:22 +08:00
GuoRuqiang
2223aeb022 Merge branch 'Calcium-Ion:main' into main 2024-08-29 19:42:03 +08:00
CalciumIon
4b1e83c42d feat: support siliconflow embedding #447 2024-08-29 00:19:30 +08:00
GuoRuqiang
ecf2f7f212 Merge branch 'Calcium-Ion:main' into main 2024-08-28 21:44:54 +08:00
CalciumIon
01fd8b53a6 feat: 检测vertex渠道部署地区是否填写 2024-08-28 18:47:27 +08:00
CalciumIon
e60f200192 feat: 支持vertex ai渠道多个部署地区 2024-08-28 18:43:40 +08:00
GuoRuqiang
033359e93c Merge branch 'Calcium-Ion:main' into main 2024-08-28 10:44:14 +08:00
CalciumIon
c41820541d Update go.mod 2024-08-27 20:30:46 +08:00
CalciumIon
228f0c5ee5 Update README.md 2024-08-27 20:25:55 +08:00
Calcium-Ion
8a5e074f14 Merge pull request #448 from Calcium-Ion/vertex
feat: support vertex ai
2024-08-27 20:21:01 +08:00
CalciumIon
ac4262c542 feat: support vertex ai #377 2024-08-27 20:19:51 +08:00
wozulong
400b2b0ed0 merge upstream
Signed-off-by: wozulong <>
2024-08-26 18:50:07 +08:00
wozulong
27b034674d add encoding_format
Signed-off-by: wozulong <>
2024-08-26 18:39:13 +08:00
GuoRuqiang
1379d7f184 Merge pull request #2 from j471782517/main
增加环境变量GENERATE_DEFAULT_TOKEN 设置之后将生成初始令牌,默认关闭。
2024-08-25 02:53:47 +08:00
Jin Weihan
716bf6f48a 增加环境变量GENERATE_DEFAULT_TOKEN 设置之后将生成初始令牌,默认关闭。 2024-08-24 18:44:37 +00:00
GuoRuqiang
2422eb2820 Merge branch 'Calcium-Ion:main' into main 2024-08-25 01:55:23 +08:00
CalciumIon
46e03683ce fix: channel auto ban 2024-08-24 17:27:14 +08:00
CalciumIon
ff0985f06e fix: channel auto ban #443 2024-08-24 17:23:24 +08:00
CalciumIon
a8ac8a25d5 feat: format claude messages when first role is not user 2024-08-24 17:15:55 +08:00
Xyfacai
5b2082ba58 Merge branch 'main' of https://github.com/Calcium-Ion/new-api 2024-08-24 13:36:44 +08:00
Xyfacai
967ccabb56 fix: 修复 dall-e-2 请求报错 2024-08-24 13:36:41 +08:00
CalciumIon
144513f1d8 feat: rerank model mapping (close #444) 2024-08-23 23:21:37 +08:00
Calcium-Ion
e3087e9bea Merge pull request #445 from OswinWu/fix-outlook-ofb
fix: 多地区outlook邮箱和ofb邮箱Auth
2024-08-23 23:16:37 +08:00
OswinWu
484a8595e4 fix: 多地区outlook邮箱和ofb邮箱Auth 2024-08-23 17:16:09 +08:00
GuoRuqiang
c97e2875b4 增加注册自动生成初始令牌。 2024-08-18 15:12:59 +00:00
GuoRuqiang
64794630c8 修改提示时间。 2024-08-17 16:59:31 +00:00
GuoRuqiang
fc5055c766 update App.js 2024-08-17 16:20:41 +00:00
GuoRuqiang
27eb358497 重新修改了chat 2024-08-17 16:17:24 +00:00
GuoRuqiang
6810ee0a28 Update Chat
修改chat界面,配合nextChat等前端可以自动传入第一个已启用令牌,
2024-08-17 23:09:45 +08:00
CalciumIon
7c4d9d225e feat: support SiliconFlow (close #437, close #403) 2024-08-16 18:27:26 +08:00
CalciumIon
d0f76a5c61 feat: support gpt-4o-gizmo-* (close #436) 2024-08-16 17:25:03 +08:00
CalciumIon
a5ec11e463 fix: add email missing Message-ID 2024-08-16 16:16:38 +08:00
CalciumIon
b3d8e3e9ae fix: lobechat #430 2024-08-16 14:59:32 +08:00
wozulong
fefe5913e9 merge upstream
Signed-off-by: wozulong <>
2024-08-15 22:30:28 +08:00
CalciumIon
0c46d0c7af chore: remove useless code 2024-08-14 22:44:33 +08:00
CalciumIon
8cd8cc29bc fix: log page 'Cannot read properties of undefined (reading 'length')' 2024-08-14 22:43:57 +08:00
CalciumIon
748e34fd10 feat: update openai models list 2024-08-14 15:51:48 +08:00
CalciumIon
f9392ca904 feat: 避免暴露内部错误 2024-08-14 15:49:33 +08:00
CalciumIon
1988c41842 feat: update chatgpt-4o-latest model ratio 2024-08-14 15:47:08 +08:00
CalciumIon
6cb0eb4b39 feat: update claude tools calling 2024-08-13 17:54:24 +08:00
Calcium-Ion
59d06a5576 Merge pull request #427 from QuentinHsu/fix-log-pagination
fix log pagination
2024-08-13 17:50:12 +08:00
Calcium-Ion
1b900e3917 Merge pull request #426 from OswinWu/fix-log-page
Fix log page
2024-08-13 17:50:03 +08:00
Calcium-Ion
accbae3904 Merge pull request #432 from xixingya/feat-add-logdb
Feature: Support Log DB
2024-08-13 17:48:25 +08:00
liuzhifei
d82bd20354 support log db 2024-08-13 10:29:55 +08:00
liuzhifei
0c01f49bc5 add log db 2024-08-13 10:28:35 +08:00
QuentinHsu
9edb7c4ade fix: log pagination 2024-08-11 11:25:32 +08:00
Nothing.
228104e848 Merge branch 'Calcium-Ion:main' into fix-log-page 2024-08-11 11:22:34 +08:00
OswinWu
a2af637e7f fix: log分页问题 2024-08-11 11:21:34 +08:00
QuentinHsu
d6f6403fd3 chore: update @so1ve/prettier-config to version 3.1.0 2024-08-11 11:18:08 +08:00
CalciumIon
4b5303a77b feat: 区分额度不足和预扣费失败提示 2024-08-09 18:48:13 +08:00
CalciumIon
6eab0cc370 feat: 区分额度不足和预扣费失败提示 2024-08-09 18:34:51 +08:00
CalciumIon
9e45dbe964 fix: close #422 2024-08-09 16:14:05 +08:00
Calcium-Ion
e495354823 Merge pull request #425 from dalefengs/fix_group
fix: 渠道多分组查询 sqlite 查询兼容
2024-08-09 15:44:23 +08:00
FENG
9452be51b9 fix: sqlite group 查询兼容 2024-08-09 11:39:19 +08:00
Calcium-Ion
43076c2f33 Merge pull request #415 from dalefengs/fix_group
fix: 渠道多分组,优化分组 like 查询
2024-08-08 20:47:51 +08:00
CalciumIon
04f0084d97 fix: 修复mysql兼容问题 2024-08-08 20:45:41 +08:00
CalciumIon
2e3c266bd6 fix: response format 2024-08-07 15:43:01 +08:00
FENG
e614ca370a fix: optionList bug 2024-08-06 21:30:20 +08:00
FENG
c152b4de08 chore: indent recovery 2024-08-06 15:40:44 +08:00
FENG
190316f66e fix: 渠道多分组,优化分组 like 查询 2024-08-05 22:35:16 +08:00
92 changed files with 2265 additions and 763 deletions

View File

@@ -1,6 +1,13 @@
<div align="center">
![new-api](/web/public/logo.png)
# New API # New API
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
> [!NOTE] > [!NOTE]
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发 > 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
@@ -54,10 +61,12 @@
8. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md) 8. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
9. Rerank模型目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)[对接文档](Rerank.md) 9. Rerank模型目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)[对接文档](Rerank.md)
10. Dify 10. Dify
11. Vertex AI目前兼容ClaudeGeminiLlama3.1
您可以在渠道中添加自定义模型gpt-4-gizmo-*或g-*此模型并非OpenAI官方模型而是第三方模型使用官方key无法调用。 您可以在渠道中添加自定义模型gpt-4-gizmo-*或g-*此模型并非OpenAI官方模型而是第三方模型使用官方key无法调用。
## 比原版One API多出的配置 ## 比原版One API多出的配置
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。 - `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true` - `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`
- `FORCE_STREAM_OPTION`是否覆盖客户端stream_options参数请求上游返回流模式usage默认为 `true`建议开启不影响客户端传入stream_options参数返回结果。 - `FORCE_STREAM_OPTION`是否覆盖客户端stream_options参数请求上游返回流模式usage默认为 `true`建议开启不影响客户端传入stream_options参数返回结果。
@@ -65,7 +74,7 @@
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`情况下统计图片token默认为 `true` - `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`情况下统计图片token默认为 `true`
- `UPDATE_TASK`是否更新异步任务Midjourney、Suno默认为 `true`,关闭后将不会更新任务进度。 - `UPDATE_TASK`是否更新异步任务Midjourney、Suno默认为 `true`,关闭后将不会更新任务进度。
- `GEMINI_MODEL_MAP`Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置 - `GEMINI_MODEL_MAP`Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置
- `COHERE_SAFETY_SETTING`Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL``STRICT`,默认为 `NONE`
## 部署 ## 部署
### 部署要求 ### 部署要求
- 本地数据库默认SQLiteDocker 部署默认使用 SQLite必须挂载 `/data` 目录到宿主机) - 本地数据库默认SQLiteDocker 部署默认使用 SQLite必须挂载 `/data` 目录到宿主机)
@@ -114,22 +123,13 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
## Suno接口设置文档 ## Suno接口设置文档
[对接文档](Suno.md) [对接文档](Suno.md)
## 交流群
<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="300">
## 界面截图 ## 界面截图
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/d1ac216e-0804-4105-9fdc-66b35022d861)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/f4f40ed4-8ccb-43d7-a580-90677827646d)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/90d7d763-6a77-4b36-9f76-2bb30f18583d) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/90d7d763-6a77-4b36-9f76-2bb30f18583d)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/e414228a-3c35-429a-b298-6451d76d9032)
夜间模式 夜间模式
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/1c66b593-bb9e-4757-9720-ff2759539242) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/1c66b593-bb9e-4757-9720-ff2759539242)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/5b3228e8-2556-44f7-97d6-4f8d8ee6effa)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e)
## 相关项目 ## 相关项目

View File

@@ -129,6 +129,9 @@ var RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0) // unit is second
var GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE") var GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
// https://docs.cohere.com/docs/safety-modes Type; NONE/CONTEXTUAL/STRICT
var CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE")
const ( const (
RequestIdKey = "X-Oneapi-Request-Id" RequestIdKey = "X-Oneapi-Request-Id"
) )
@@ -236,6 +239,8 @@ const (
ChannelTypeDify = 37 ChannelTypeDify = 37
ChannelTypeJina = 38 ChannelTypeJina = 38
ChannelCloudflare = 39 ChannelCloudflare = 39
ChannelTypeSiliconFlow = 40
ChannelTypeVertexAi = 41
ChannelTypeDummy // this one is only for count, do not add any channel after this ChannelTypeDummy // this one is only for count, do not add any channel after this
@@ -282,4 +287,6 @@ var ChannelBaseURLs = []string{
"", //37 "", //37
"https://api.jina.ai", //38 "https://api.jina.ai", //38
"https://api.cloudflare.com", //39 "https://api.cloudflare.com", //39
"https://api.siliconflow.cn", //40
"", //41
} }

View File

@@ -3,6 +3,7 @@ package common
import ( import (
"errors" "errors"
"net/smtp" "net/smtp"
"strings"
) )
type outlookAuth struct { type outlookAuth struct {
@@ -30,3 +31,10 @@ func (a *outlookAuth) Next(fromServer []byte, more bool) ([]byte, error) {
} }
return nil, nil return nil, nil
} }
func isOutlookServer(server string) bool {
// 兼容多地区的outlook邮箱和ofb邮箱
// 其实应该加一个Option来区分是否用LOGIN的方式登录
// 先临时兼容一下
return strings.Contains(server, "outlook") || strings.Contains(server, "onmicrosoft")
}

View File

@@ -9,17 +9,26 @@ import (
"time" "time"
) )
func generateMessageID() string {
domain := strings.Split(SMTPAccount, "@")[1]
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain)
}
func SendEmail(subject string, receiver string, content string) error { func SendEmail(subject string, receiver string, content string) error {
if SMTPFrom == "" { // for compatibility if SMTPFrom == "" { // for compatibility
SMTPFrom = SMTPAccount SMTPFrom = SMTPAccount
} }
if SMTPServer == "" && SMTPAccount == "" {
return fmt.Errorf("SMTP 服务器未配置")
}
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject))) encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
mail := []byte(fmt.Sprintf("To: %s\r\n"+ mail := []byte(fmt.Sprintf("To: %s\r\n"+
"From: %s<%s>\r\n"+ "From: %s<%s>\r\n"+
"Subject: %s\r\n"+ "Subject: %s\r\n"+
"Date: %s\r\n"+ "Date: %s\r\n"+
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", "Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), content)) receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), generateMessageID(), content))
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer) auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort) addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
to := strings.Split(receiver, ";") to := strings.Split(receiver, ";")
@@ -62,7 +71,7 @@ func SendEmail(subject string, receiver string, content string) error {
if err != nil { if err != nil {
return err return err
} }
} else if strings.HasSuffix(SMTPAccount, "outlook.com") { } else if isOutlookServer(SMTPAccount) {
auth = LoginAuth(SMTPAccount, SMTPToken) auth = LoginAuth(SMTPAccount, SMTPToken)
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail) err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
} else { } else {

View File

@@ -33,9 +33,14 @@ var defaultModelRatio = map[string]float64{
"gpt-4-32k-0613": 30, "gpt-4-32k-0613": 30,
"gpt-4o-mini": 0.075, // $0.00015 / 1K tokens "gpt-4o-mini": 0.075, // $0.00015 / 1K tokens
"gpt-4o-mini-2024-07-18": 0.075, "gpt-4o-mini-2024-07-18": 0.075,
"chatgpt-4o-latest": 2.5, // $0.01 / 1K tokens
"gpt-4o": 2.5, // $0.005 / 1K tokens "gpt-4o": 2.5, // $0.005 / 1K tokens
"gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens "gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens
"gpt-4o-2024-08-06": 1.25, // $0.01 / 1K tokens "gpt-4o-2024-08-06": 1.25, // $0.01 / 1K tokens
"o1-preview": 7.5,
"o1-preview-2024-09-12": 7.5,
"o1-mini": 1.5,
"o1-mini-2024-09-12": 1.5,
"gpt-4-turbo": 5, // $0.01 / 1K tokens "gpt-4-turbo": 5, // $0.01 / 1K tokens
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens "gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens "gpt-4-1106-preview": 5, // $0.01 / 1K tokens
@@ -103,8 +108,10 @@ var defaultModelRatio = map[string]float64{
"gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
"gemini-1.0-pro-vision-001": 1, "gemini-1.0-pro-vision-001": 1,
"gemini-1.0-pro-001": 1, "gemini-1.0-pro-001": 1,
"gemini-1.5-pro-latest": 1, "gemini-1.5-pro-latest": 1.75, // $3.5 / 1M tokens
"gemini-1.5-pro-exp-0827": 1.75, // $3.5 / 1M tokens
"gemini-1.5-flash-latest": 1, "gemini-1.5-flash-latest": 1,
"gemini-1.5-flash-exp-0827": 1,
"gemini-1.0-pro-latest": 1, "gemini-1.0-pro-latest": 1,
"gemini-1.0-pro-vision-latest": 1, "gemini-1.0-pro-vision-latest": 1,
"gemini-ultra": 1, "gemini-ultra": 1,
@@ -116,6 +123,13 @@ var defaultModelRatio = map[string]float64{
"glm-4v": 0.05 * RMB, // ¥0.05 / 1k tokens "glm-4v": 0.05 * RMB, // ¥0.05 / 1k tokens
"glm-4-alltools": 0.1 * RMB, // ¥0.1 / 1k tokens "glm-4-alltools": 0.1 * RMB, // ¥0.1 / 1k tokens
"glm-3-turbo": 0.3572, "glm-3-turbo": 0.3572,
"glm-4-plus": 0.05 * RMB,
"glm-4-0520": 0.1 * RMB,
"glm-4-air": 0.001 * RMB,
"glm-4-airx": 0.01 * RMB,
"glm-4-long": 0.001 * RMB,
"glm-4-flash": 0,
"glm-4v-plus": 0.01 * RMB,
"qwen-turbo": 0.8572, // ¥0.012 / 1k tokens "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
"qwen-plus": 10, // ¥0.14 / 1k tokens "qwen-plus": 10, // ¥0.14 / 1k tokens
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens "text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
@@ -134,26 +148,28 @@ var defaultModelRatio = map[string]float64{
"hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0 "hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
// https://platform.lingyiwanwu.com/docs#-计费单元 // https://platform.lingyiwanwu.com/docs#-计费单元
// 已经按照 7.2 来换算美元价格 // 已经按照 7.2 来换算美元价格
"yi-34b-chat-0205": 0.18, "yi-34b-chat-0205": 0.18,
"yi-34b-chat-200k": 0.864, "yi-34b-chat-200k": 0.864,
"yi-vl-plus": 0.432, "yi-vl-plus": 0.432,
"yi-large": 20.0 / 1000 * RMB, "yi-large": 20.0 / 1000 * RMB,
"yi-medium": 2.5 / 1000 * RMB, "yi-medium": 2.5 / 1000 * RMB,
"yi-vision": 6.0 / 1000 * RMB, "yi-vision": 6.0 / 1000 * RMB,
"yi-medium-200k": 12.0 / 1000 * RMB, "yi-medium-200k": 12.0 / 1000 * RMB,
"yi-spark": 1.0 / 1000 * RMB, "yi-spark": 1.0 / 1000 * RMB,
"yi-large-rag": 25.0 / 1000 * RMB, "yi-large-rag": 25.0 / 1000 * RMB,
"yi-large-turbo": 12.0 / 1000 * RMB, "yi-large-turbo": 12.0 / 1000 * RMB,
"yi-large-preview": 20.0 / 1000 * RMB, "yi-large-preview": 20.0 / 1000 * RMB,
"yi-large-rag-preview": 25.0 / 1000 * RMB, "yi-large-rag-preview": 25.0 / 1000 * RMB,
"command": 0.5, "command": 0.5,
"command-nightly": 0.5, "command-nightly": 0.5,
"command-light": 0.5, "command-light": 0.5,
"command-light-nightly": 0.5, "command-light-nightly": 0.5,
"command-r": 0.25, "command-r": 0.25,
"command-r-plus ": 1.5, "command-r-plus": 1.5,
"deepseek-chat": 0.07, "command-r-08-2024": 0.075,
"deepseek-coder": 0.07, "command-r-plus-08-2024": 1.25,
"deepseek-chat": 0.07,
"deepseek-coder": 0.07,
// Perplexity online 模型对搜索额外收费,有需要应自行调整,此处不计入搜索费用 // Perplexity online 模型对搜索额外收费,有需要应自行调整,此处不计入搜索费用
"llama-3-sonar-small-32k-chat": 0.2 / 1000 * USD, "llama-3-sonar-small-32k-chat": 0.2 / 1000 * USD,
"llama-3-sonar-small-32k-online": 0.2 / 1000 * USD, "llama-3-sonar-small-32k-online": 0.2 / 1000 * USD,
@@ -187,8 +203,8 @@ var defaultModelPrice = map[string]float64{
} }
var ( var (
modelPriceMap = make(map[string]float64) modelPriceMap map[string]float64 = nil
modelPriceMapMutex = sync.RWMutex{} modelPriceMapMutex = sync.RWMutex{}
) )
var ( var (
modelRatioMap map[string]float64 = nil modelRatioMap map[string]float64 = nil
@@ -343,6 +359,12 @@ func GetCompletionRatio(name string) float64 {
} }
return 2 return 2
} }
if "o1" == name || strings.HasPrefix(name, "o1-") {
return 4
}
if name == "chatgpt-4o-latest" {
return 3
}
if strings.HasPrefix(name, "claude-instant-1") { if strings.HasPrefix(name, "claude-instant-1") {
return 3 return 3
} else if strings.HasPrefix(name, "claude-2") { } else if strings.HasPrefix(name, "claude-2") {
@@ -354,6 +376,9 @@ func GetCompletionRatio(name string) float64 {
return 3 return 3
} }
if strings.HasPrefix(name, "gemini-") { if strings.HasPrefix(name, "gemini-") {
if strings.Contains(name, "flash") {
return 4
}
return 3 return 3
} }
if strings.HasPrefix(name, "command") { if strings.HasPrefix(name, "command") {
@@ -362,6 +387,10 @@ func GetCompletionRatio(name string) float64 {
return 3 return 3
case "command-r-plus": case "command-r-plus":
return 5 return 5
case "command-r-08-2024":
return 4
case "command-r-plus-08-2024":
return 4
default: default:
return 2 return 2
} }

View File

@@ -31,14 +31,6 @@ func MapToJsonStr(m map[string]interface{}) string {
return string(bytes) return string(bytes)
} }
func MapToJsonStrFloat(m map[string]float64) string {
bytes, err := json.Marshal(m)
if err != nil {
return ""
}
return string(bytes)
}
func StrToMap(str string) map[string]interface{} { func StrToMap(str string) map[string]interface{} {
m := make(map[string]interface{}) m := make(map[string]interface{})
err := json.Unmarshal([]byte(str), &m) err := json.Unmarshal([]byte(str), &m)
@@ -48,6 +40,11 @@ func StrToMap(str string) map[string]interface{} {
return m return m
} }
func IsJsonStr(str string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(str), &js) == nil
}
func String2Int(str string) int { func String2Int(str string) int {
num, err := strconv.Atoi(str) num, err := strconv.Atoi(str)
if err != nil { if err != nil {

23
common/user_groups.go Normal file
View File

@@ -0,0 +1,23 @@
package common
import (
"encoding/json"
)
var UserUsableGroups = map[string]string{
"default": "默认分组",
"vip": "vip分组",
}
func UserUsableGroups2JSONString() string {
jsonBytes, err := json.Marshal(UserUsableGroups)
if err != nil {
SysError("error marshalling user groups: " + err.Error())
}
return string(jsonBytes)
}
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
UserUsableGroups = make(map[string]string)
return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
}

View File

@@ -133,6 +133,11 @@ func IntMax(a int, b int) int {
} }
} }
func IsIP(s string) bool {
ip := net.ParseIP(s)
return ip != nil
}
func GetUUID() string { func GetUUID() string {
code := uuid.New().String() code := uuid.New().String()
code = strings.Replace(code, "-", "", -1) code = strings.Replace(code, "-", "", -1)

View File

@@ -20,14 +20,16 @@ var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STR
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true) var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
var GeminiModelMap = map[string]string{ var GeminiModelMap = map[string]string{
"gemini-1.5-pro-latest": "v1beta", "gemini-1.5-pro-latest": "v1beta",
"gemini-1.5-pro-001": "v1beta", "gemini-1.5-pro-001": "v1beta",
"gemini-1.5-pro": "v1beta", "gemini-1.5-pro": "v1beta",
"gemini-1.5-pro-exp-0801": "v1beta", "gemini-1.5-pro-exp-0801": "v1beta",
"gemini-1.5-flash-latest": "v1beta", "gemini-1.5-pro-exp-0827": "v1beta",
"gemini-1.5-flash-001": "v1beta", "gemini-1.5-flash-latest": "v1beta",
"gemini-1.5-flash": "v1beta", "gemini-1.5-flash-exp-0827": "v1beta",
"gemini-ultra": "v1beta", "gemini-1.5-flash-001": "v1beta",
"gemini-1.5-flash": "v1beta",
"gemini-ultra": "v1beta",
} }
func InitEnv() { func InitEnv() {
@@ -44,3 +46,6 @@ func InitEnv() {
} }
} }
} }
// 是否生成初始令牌,默认关闭。
var GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)

View File

@@ -20,6 +20,7 @@ import (
"one-api/relay/constant" "one-api/relay/constant"
"one-api/service" "one-api/service"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@@ -81,8 +82,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
} }
request := buildTestRequest() request := buildTestRequest(testModel)
request.Model = testModel
meta.UpstreamModelName = testModel meta.UpstreamModelName = testModel
common.SysLog(fmt.Sprintf("testing channel %d with model %s", channel.Id, testModel)) common.SysLog(fmt.Sprintf("testing channel %d with model %s", channel.Id, testModel))
@@ -141,17 +141,22 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
return nil, nil return nil, nil
} }
func buildTestRequest() *dto.GeneralOpenAIRequest { func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
testRequest := &dto.GeneralOpenAIRequest{ testRequest := &dto.GeneralOpenAIRequest{
Model: "", // this will be set later Model: "", // this will be set later
MaxTokens: 1, Stream: false,
Stream: false, }
if "o1" == model || strings.HasPrefix(model, "o1-") {
testRequest.MaxCompletionTokens = 1
} else {
testRequest.MaxTokens = 1
} }
content, _ := json.Marshal("hi") content, _ := json.Marshal("hi")
testMessage := dto.Message{ testMessage := dto.Message{
Role: "user", Role: "user",
Content: content, Content: content,
} }
testRequest.Model = model
testRequest.Messages = append(testRequest.Messages, testMessage) testRequest.Messages = append(testRequest.Messages, testMessage)
return testRequest return testRequest
} }
@@ -226,26 +231,22 @@ func testAllChannels(notify bool) error {
tok := time.Now() tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds() milliseconds := tok.Sub(tik).Milliseconds()
ban := false shouldBanChannel := false
if milliseconds > disableThreshold {
err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
ban = true
}
// request error disables the channel // request error disables the channel
if openaiWithStatusErr != nil { if openaiWithStatusErr != nil {
oaiErr := openaiWithStatusErr.Error oaiErr := openaiWithStatusErr.Error
err = errors.New(fmt.Sprintf("type %s, httpCode %d, code %v, message %s", oaiErr.Type, openaiWithStatusErr.StatusCode, oaiErr.Code, oaiErr.Message)) err = errors.New(fmt.Sprintf("type %s, httpCode %d, code %v, message %s", oaiErr.Type, openaiWithStatusErr.StatusCode, oaiErr.Code, oaiErr.Message))
ban = service.ShouldDisableChannel(channel.Type, openaiWithStatusErr) shouldBanChannel = service.ShouldDisableChannel(channel.Type, openaiWithStatusErr)
} }
// parse *int to bool if milliseconds > disableThreshold {
if !channel.GetAutoBan() { err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
ban = false shouldBanChannel = true
} }
// disable channel // disable channel
if ban && isChannelEnabled { if isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
service.DisableChannel(channel.Id, channel.Name, err.Error()) service.DisableChannel(channel.Id, channel.Name, err.Error())
} }

View File

@@ -198,6 +198,28 @@ func AddChannel(c *gin.Context) {
} }
channel.CreatedTime = common.GetTimestamp() channel.CreatedTime = common.GetTimestamp()
keys := strings.Split(channel.Key, "\n") keys := strings.Split(channel.Key, "\n")
if channel.Type == common.ChannelTypeVertexAi {
if channel.Other == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "部署地区不能为空",
})
return
} else {
if common.IsJsonStr(channel.Other) {
// must have default
regionMap := common.StrToMap(channel.Other)
if regionMap["default"] == nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "部署地区必须包含default字段",
})
return
}
}
}
keys = []string{channel.Key}
}
channels := make([]model.Channel, 0, len(keys)) channels := make([]model.Channel, 0, len(keys))
for _, key := range keys { for _, key := range keys {
if key == "" { if key == "" {
@@ -297,6 +319,27 @@ func UpdateChannel(c *gin.Context) {
}) })
return return
} }
if channel.Type == common.ChannelTypeVertexAi {
if channel.Other == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "部署地区不能为空",
})
return
} else {
if common.IsJsonStr(channel.Other) {
// must have default
regionMap := common.StrToMap(channel.Other)
if regionMap["default"] == nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "部署地区必须包含default字段",
})
return
}
}
}
}
err = channel.Update() err = channel.Update()
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@@ -17,3 +17,18 @@ func GetGroups(c *gin.Context) {
"data": groupNames, "data": groupNames,
}) })
} }
func GetUserGroups(c *gin.Context) {
usableGroups := make(map[string]string)
for groupName, _ := range common.GroupRatio {
// UserUsableGroups contains the groups that the user can use
if _, ok := common.UserUsableGroups[groupName]; ok {
usableGroups[groupName] = common.UserUsableGroups[groupName]
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": usableGroups,
})
}

View File

@@ -1,18 +1,19 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"net/http" "net/http"
"one-api/common" "one-api/common"
"one-api/model" "one-api/model"
"strconv" "strconv"
"github.com/gin-gonic/gin"
) )
func GetAllLogs(c *gin.Context) { func GetAllLogs(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p")) p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size")) pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 0 { if p < 1 {
p = 0 p = 1
} }
if pageSize < 0 { if pageSize < 0 {
pageSize = common.ItemsPerPage pageSize = common.ItemsPerPage
@@ -24,7 +25,7 @@ func GetAllLogs(c *gin.Context) {
tokenName := c.Query("token_name") tokenName := c.Query("token_name")
modelName := c.Query("model_name") modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel")) channel, _ := strconv.Atoi(c.Query("channel"))
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*pageSize, pageSize, channel) logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
@@ -35,17 +36,20 @@ func GetAllLogs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
"total": total, "data": map[string]any{
"data": logs, "items": logs,
"total": total,
"page": p,
"page_size": pageSize,
},
}) })
return
} }
func GetUserLogs(c *gin.Context) { func GetUserLogs(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p")) p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size")) pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 0 { if p < 1 {
p = 0 p = 1
} }
if pageSize < 0 { if pageSize < 0 {
pageSize = common.ItemsPerPage pageSize = common.ItemsPerPage
@@ -59,7 +63,7 @@ func GetUserLogs(c *gin.Context) {
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
tokenName := c.Query("token_name") tokenName := c.Query("token_name")
modelName := c.Query("model_name") modelName := c.Query("model_name")
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*pageSize, pageSize) logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
@@ -70,8 +74,12 @@ func GetUserLogs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
"total": total, "data": map[string]any{
"data": logs, "items": logs,
"total": total,
"page": p,
"page_size": pageSize,
},
}) })
return return
} }
@@ -184,7 +192,7 @@ func DeleteHistoryLogs(c *gin.Context) {
}) })
return return
} }
count, err := model.DeleteOldLog(targetTimestamp) count, err := model.DeleteOldLog(c.Request.Context(), targetTimestamp, 100)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,

View File

@@ -137,31 +137,63 @@ func init() {
} }
func ListModels(c *gin.Context) { func ListModels(c *gin.Context) {
userId := c.GetInt("id")
user, err := model.GetUserById(userId, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
models := model.GetGroupModels(user.Group)
userOpenAiModels := make([]dto.OpenAIModels, 0) userOpenAiModels := make([]dto.OpenAIModels, 0)
permission := getPermission() permission := getPermission()
for _, s := range models {
if _, ok := openAIModelsMap[s]; ok { modelLimitEnable := c.GetBool("token_model_limit_enabled")
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s]) if modelLimitEnable {
s, ok := c.Get("token_model_limit")
var tokenModelLimit map[string]bool
if ok {
tokenModelLimit = s.(map[string]bool)
} else { } else {
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{ tokenModelLimit = map[string]bool{}
Id: s, }
Object: "model", for allowModel, _ := range tokenModelLimit {
Created: 1626777600, if _, ok := openAIModelsMap[allowModel]; ok {
OwnedBy: "custom", userOpenAiModels = append(userOpenAiModels, openAIModelsMap[allowModel])
Permission: permission, } else {
Root: s, userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
Parent: nil, Id: allowModel,
Object: "model",
Created: 1626777600,
OwnedBy: "custom",
Permission: permission,
Root: allowModel,
Parent: nil,
})
}
}
} else {
userId := c.GetInt("id")
userGroup, err := model.GetUserGroup(userId)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "get user group failed",
}) })
return
}
group := userGroup
tokenGroup := c.GetString("token_group")
if tokenGroup != "" {
group = tokenGroup
}
models := model.GetGroupModels(group)
for _, s := range models {
if _, ok := openAIModelsMap[s]; ok {
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
} else {
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
Id: s,
Object: "model",
Created: 1626777600,
OwnedBy: "custom",
Permission: permission,
Root: s,
Parent: nil,
})
}
} }
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{

View File

@@ -7,18 +7,11 @@ import (
) )
func GetPricing(c *gin.Context) { func GetPricing(c *gin.Context) {
userId := c.GetInt("id") pricing := model.GetPricing()
// if no login, get default group ratio
groupRatio := common.GetGroupRatio("default")
group, err := model.CacheGetUserGroup(userId)
if err == nil {
groupRatio = common.GetGroupRatio(group)
}
pricing := model.GetPricing(group)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"success": true, "success": true,
"data": pricing, "data": pricing,
"group_ratio": groupRatio, "group_ratio": common.GroupRatio,
}) })
} }

View File

@@ -121,6 +121,9 @@ func shouldRetry(c *gin.Context, openaiErr *dto.OpenAIErrorWithStatusCode, retry
if openaiErr == nil { if openaiErr == nil {
return false return false
} }
if openaiErr.LocalError {
return false
}
if retryTimes <= 0 { if retryTimes <= 0 {
return false return false
} }
@@ -151,9 +154,6 @@ func shouldRetry(c *gin.Context, openaiErr *dto.OpenAIErrorWithStatusCode, retry
// azure处理超时不重试 // azure处理超时不重试
return false return false
} }
if openaiErr.LocalError {
return false
}
if openaiErr.StatusCode/100 == 2 { if openaiErr.StatusCode/100 == 2 {
return false return false
} }

View File

@@ -134,6 +134,8 @@ func AddToken(c *gin.Context) {
UnlimitedQuota: token.UnlimitedQuota, UnlimitedQuota: token.UnlimitedQuota,
ModelLimitsEnabled: token.ModelLimitsEnabled, ModelLimitsEnabled: token.ModelLimitsEnabled,
ModelLimits: token.ModelLimits, ModelLimits: token.ModelLimits,
AllowIps: token.AllowIps,
Group: token.Group,
} }
err = cleanToken.Insert() err = cleanToken.Insert()
if err != nil { if err != nil {
@@ -221,6 +223,8 @@ func UpdateToken(c *gin.Context) {
cleanToken.UnlimitedQuota = token.UnlimitedQuota cleanToken.UnlimitedQuota = token.UnlimitedQuota
cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
cleanToken.ModelLimits = token.ModelLimits cleanToken.ModelLimits = token.ModelLimits
cleanToken.AllowIps = token.AllowIps
cleanToken.Group = token.Group
} }
err = cleanToken.Update() err = cleanToken.Update()
if err != nil { if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"one-api/constant"
) )
type LoginRequest struct { type LoginRequest struct {
@@ -187,6 +188,39 @@ func Register(c *gin.Context) {
}) })
return return
} }
// 获取插入后的用户ID
var insertedUser model.User
if err := model.DB.Where("username = ?", cleanUser.Username).First(&insertedUser).Error; err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户注册失败或用户ID获取失败",
})
return
}
// 生成默认令牌
if constant.GenerateDefaultToken {
// 生成默认令牌
token := model.Token{
UserId: insertedUser.Id, // 使用插入后的用户ID
Name: cleanUser.Username + "的初始令牌",
Key: common.GenerateKey(),
CreatedTime: common.GetTimestamp(),
AccessedTime: common.GetTimestamp(),
ExpiredTime: -1, // 永不过期
RemainQuota: 500000, // 示例额度
UnlimitedQuota: true,
ModelLimitsEnabled: false,
}
if err := token.Insert(); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "创建默认令牌失败",
})
return
}
}
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",

View File

@@ -1,14 +1,17 @@
package dto package dto
type RerankRequest struct { type RerankRequest struct {
Documents []any `json:"documents"` Documents []any `json:"documents"`
Query string `json:"query"` Query string `json:"query"`
Model string `json:"model"` Model string `json:"model"`
TopN int `json:"top_n"` TopN int `json:"top_n"`
ReturnDocuments bool `json:"return_documents,omitempty"`
MaxChunkPerDoc int `json:"max_chunk_per_doc,omitempty"`
OverLapTokens int `json:"overlap_tokens,omitempty"`
} }
type RerankResponseDocument struct { type RerankResponseDocument struct {
Document any `json:"document"` Document any `json:"document,omitempty"`
Index int `json:"index"` Index int `json:"index"`
RelevanceScore float64 `json:"relevance_score"` RelevanceScore float64 `json:"relevance_score"`
} }

View File

@@ -12,6 +12,7 @@ type GeneralOpenAIRequest struct {
StreamOptions *StreamOptions `json:"stream_options,omitempty"` StreamOptions *StreamOptions `json:"stream_options,omitempty"`
Suffix string `json:"suffix,omitempty"` Suffix string `json:"suffix,omitempty"`
MaxTokens uint `json:"max_tokens,omitempty"` MaxTokens uint `json:"max_tokens,omitempty"`
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"` Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"` TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"` TopK int `json:"top_k,omitempty"`
@@ -33,6 +34,7 @@ type GeneralOpenAIRequest struct {
TopLogProbs int `json:"top_logprobs,omitempty"` TopLogProbs int `json:"top_logprobs,omitempty"`
Dimensions int `json:"dimensions,omitempty"` Dimensions int `json:"dimensions,omitempty"`
ParallelToolCalls bool `json:"parallel_Tool_Calls,omitempty"` ParallelToolCalls bool `json:"parallel_Tool_Calls,omitempty"`
EncodingFormat string `json:"encoding_format,omitempty"`
} }
type OpenAITools struct { type OpenAITools struct {

View File

@@ -34,6 +34,7 @@ type OpenAITextResponseChoice struct {
type OpenAITextResponse struct { type OpenAITextResponse struct {
Id string `json:"id"` Id string `json:"id"`
Model string `json:"model"`
Object string `json:"object"` Object string `json:"object"`
Created int64 `json:"created"` Created int64 `json:"created"`
Choices []OpenAITextResponseChoice `json:"choices"` Choices []OpenAITextResponseChoice `json:"choices"`
@@ -41,9 +42,9 @@ type OpenAITextResponse struct {
} }
type OpenAIEmbeddingResponseItem struct { type OpenAIEmbeddingResponseItem struct {
Object string `json:"object"` Object string `json:"object"`
Index int `json:"index"` Index int `json:"index"`
Embedding []float64 `json:"embedding"` Embedding any `json:"embedding"`
} }
type OpenAIEmbeddingResponse struct { type OpenAIEmbeddingResponse struct {

22
go.mod
View File

@@ -1,13 +1,16 @@
module one-api module one-api
// +heroku goVersion go1.18 // +heroku goVersion go1.18
go 1.18 go 1.21
toolchain go1.22.4
require ( require (
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
github.com/aws/aws-sdk-go-v2 v1.26.1 github.com/aws/aws-sdk-go-v2 v1.26.1
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 github.com/aws/aws-sdk-go-v2/credentials v1.17.11
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/cors v1.4.0
github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/sessions v0.0.5
@@ -24,8 +27,9 @@ require (
github.com/samber/lo v1.39.0 github.com/samber/lo v1.39.0
github.com/shirou/gopsutil v3.21.11+incompatible github.com/shirou/gopsutil v3.21.11+incompatible
github.com/stripe/stripe-go/v76 v76.21.0 github.com/stripe/stripe-go/v76 v76.21.0
golang.org/x/crypto v0.21.0 golang.org/x/crypto v0.26.0
golang.org/x/image v0.15.0 golang.org/x/image v0.15.0
golang.org/x/net v0.28.0
gorm.io/driver/mysql v1.4.3 gorm.io/driver/mysql v1.4.3
gorm.io/driver/postgres v1.5.2 gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.4.3 gorm.io/driver/sqlite v1.4.3
@@ -38,9 +42,8 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/smithy-go v1.20.2 // indirect github.com/aws/smithy-go v1.20.2 // indirect
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b // indirect
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect
@@ -51,6 +54,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect
@@ -68,6 +72,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@@ -75,10 +80,9 @@ require (
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.24.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

48
go.sum
View File

@@ -21,8 +21,8 @@ github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaU
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -32,11 +32,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
@@ -57,6 +56,7 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -81,10 +81,9 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@@ -128,8 +127,6 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linux-do/tiktoken-go v0.7.0 h1:Kcm/miJ5gp77srtF8GQWnfq7W9kTaXEuHZg/g9IVEu8=
github.com/linux-do/tiktoken-go v0.7.0/go.mod h1:9Vkdtp0ngi4USmrdSx984iuIQ5IMr0hnUdz4jZZTJb8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -144,16 +141,17 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -176,7 +174,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stripe/stripe-go/v76 v76.21.0 h1:O3GHImHS4oUI3qWMOClHN3zAQF5/oswS/NB7leV1fsU= github.com/stripe/stripe-go/v76 v76.21.0 h1:O3GHImHS4oUI3qWMOClHN3zAQF5/oswS/NB7leV1fsU=
github.com/stripe/stripe-go/v76 v76.21.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4= github.com/stripe/stripe-go/v76 v76.21.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -197,19 +196,19 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -222,26 +221,27 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@@ -42,6 +42,11 @@ func main() {
if err != nil { if err != nil {
common.FatalLog("failed to initialize database: " + err.Error()) common.FatalLog("failed to initialize database: " + err.Error())
} }
// Initialize SQL Database
err = model.InitLogDB()
if err != nil {
common.FatalLog("failed to initialize database: " + err.Error())
}
defer func() { defer func() {
err := model.CloseDB() err := model.CloseDB()
if err != nil { if err != nil {

View File

@@ -194,6 +194,8 @@ func TokenAuth() func(c *gin.Context) {
} else { } else {
c.Set("token_model_limit_enabled", false) c.Set("token_model_limit_enabled", false)
} }
c.Set("allow_ips", token.GetIpLimitsMap())
c.Set("token_group", token.Group)
if len(parts) > 1 { if len(parts) > 1 {
if model.IsAdmin(token.UserId) { if model.IsAdmin(token.UserId) {
c.Set("specific_channel_id", parts[1]) c.Set("specific_channel_id", parts[1])

View File

@@ -22,6 +22,14 @@ type ModelRequest struct {
func Distribute() func(c *gin.Context) { func Distribute() func(c *gin.Context) {
return func(c *gin.Context) { return func(c *gin.Context) {
allowIpsMap := c.GetStringMap("allow_ips")
if len(allowIpsMap) != 0 {
clientIp := c.ClientIP()
if _, ok := allowIpsMap[clientIp]; !ok {
abortWithOpenAiMessage(c, http.StatusForbidden, "您的 IP 不在令牌允许访问的列表中")
return
}
}
userId := c.GetInt("id") userId := c.GetInt("id")
var channel *model.Channel var channel *model.Channel
channelId, ok := c.Get("specific_channel_id") channelId, ok := c.Get("specific_channel_id")
@@ -31,6 +39,20 @@ func Distribute() func(c *gin.Context) {
return return
} }
userGroup, _ := model.CacheGetUserGroup(userId) userGroup, _ := model.CacheGetUserGroup(userId)
tokenGroup := c.GetString("token_group")
if tokenGroup != "" {
// check common.UserUsableGroups[userGroup]
if _, ok := common.UserUsableGroups[tokenGroup]; !ok {
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
return
}
// check group in common.GroupRatio
if _, ok := common.GroupRatio[tokenGroup]; !ok {
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
return
}
userGroup = tokenGroup
}
c.Set("group", userGroup) c.Set("group", userGroup)
if ok { if ok {
id, err := strconv.Atoi(channelId.(string)) id, err := strconv.Atoi(channelId.(string))
@@ -199,6 +221,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
switch channel.Type { switch channel.Type {
case common.ChannelTypeAzure: case common.ChannelTypeAzure:
c.Set("api_version", channel.Other) c.Set("api_version", channel.Other)
case common.ChannelTypeVertexAi:
c.Set("region", channel.Other)
case common.ChannelTypeXunfei: case common.ChannelTypeXunfei:
c.Set("api_version", channel.Other) c.Set("api_version", channel.Other)
case common.ChannelTypeGemini: case common.ChannelTypeGemini:

View File

@@ -36,6 +36,12 @@ func GetEnabledModels() []string {
return models return models
} }
func GetAllEnableAbilities() []Ability {
var abilities []Ability
DB.Find(&abilities, "enabled = ?", true)
return abilities
}
func getPriority(group string, model string, retry int) (int, error) { func getPriority(group string, model string, retry int) (int, error) {
groupCol := "`group`" groupCol := "`group`"
trueVal := "1" trueVal := "1"

View File

@@ -106,16 +106,23 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
// 构造WHERE子句 // 构造WHERE子句
var whereClause string var whereClause string
var args []interface{} var args []interface{}
if group != "" { if group != "" && group != "null" {
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + groupCol + " = ? AND " + modelsCol + " LIKE ?" var groupCondition string
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, group, "%"+model+"%") if common.UsingMySQL {
groupCondition = `CONCAT(',', ` + groupCol + `, ',') LIKE ?`
} else {
// sqlite, PostgreSQL
groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
}
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%", "%,"+group+",%")
} else { } else {
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?" whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%") args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
} }
// 执行查询 // 执行查询
err := baseQuery.Where(whereClause, args...).Find(&channels).Error err := baseQuery.Where(whereClause, args...).Order("priority desc").Find(&channels).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,11 +3,12 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/bytedance/gopkg/util/gopool"
"gorm.io/gorm"
"one-api/common" "one-api/common"
"strings" "strings"
"time" "time"
"github.com/bytedance/gopkg/util/gopool"
"gorm.io/gorm"
) )
type Log struct { type Log struct {
@@ -38,7 +39,7 @@ const (
) )
func GetLogByKey(key string) (logs []*Log, err error) { func GetLogByKey(key string) (logs []*Log, err error) {
err = DB.Joins("left join tokens on tokens.id = logs.token_id").Where("tokens.key = ?", strings.TrimPrefix(key, "sk-")).Find(&logs).Error err = LOG_DB.Joins("left join tokens on tokens.id = logs.token_id").Where("tokens.key = ?", strings.TrimPrefix(key, "sk-")).Find(&logs).Error
return logs, err return logs, err
} }
@@ -54,7 +55,7 @@ func RecordLog(userId int, logType int, content string) {
Type: logType, Type: logType,
Content: content, Content: content,
} }
err := DB.Create(log).Error err := LOG_DB.Create(log).Error
if err != nil { if err != nil {
common.SysError("failed to record log: " + err.Error()) common.SysError("failed to record log: " + err.Error())
} }
@@ -84,7 +85,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
IsStream: isStream, IsStream: isStream,
Other: otherStr, Other: otherStr,
} }
err := DB.Create(log).Error err := LOG_DB.Create(log).Error
if err != nil { if err != nil {
common.LogError(ctx, "failed to record log: "+err.Error()) common.LogError(ctx, "failed to record log: "+err.Error())
} }
@@ -98,9 +99,9 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, total int64, err error) { func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, total int64, err error) {
var tx *gorm.DB var tx *gorm.DB
if logType == LogTypeUnknown { if logType == LogTypeUnknown {
tx = DB tx = LOG_DB
} else { } else {
tx = DB.Where("type = ?", logType) tx = LOG_DB.Where("type = ?", logType)
} }
if modelName != "" { if modelName != "" {
tx = tx.Where("model_name like ?", modelName) tx = tx.Where("model_name like ?", modelName)
@@ -120,22 +121,23 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
if channel != 0 { if channel != 0 {
tx = tx.Where("channel_id = ?", channel) tx = tx.Where("channel_id = ?", channel)
} }
err = tx.Model(&Log{}).Count(&total).Error err = tx.Model(&Log{}).Count(&total).Error
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
if err != nil {
return nil, 0, err
}
return logs, total, err return logs, total, err
} }
func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, total int64, err error) { func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, total int64, err error) {
var tx *gorm.DB var tx *gorm.DB
if logType == LogTypeUnknown { if logType == LogTypeUnknown {
tx = DB.Where("user_id = ?", userId) tx = LOG_DB.Where("user_id = ?", userId)
} else { } else {
tx = DB.Where("user_id = ? and type = ?", userId, logType) tx = LOG_DB.Where("user_id = ? and type = ?", userId, logType)
} }
if modelName != "" { if modelName != "" {
tx = tx.Where("model_name like ?", modelName) tx = tx.Where("model_name like ?", modelName)
@@ -149,14 +151,11 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
if endTimestamp != 0 { if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp) tx = tx.Where("created_at <= ?", endTimestamp)
} }
err = tx.Model(&Log{}).Count(&total).Error err = tx.Model(&Log{}).Count(&total).Error
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error
return logs, total, err
for i := range logs { for i := range logs {
var otherMap map[string]interface{} var otherMap map[string]interface{}
otherMap = common.StrToMap(logs[i].Other) otherMap = common.StrToMap(logs[i].Other)
@@ -170,12 +169,12 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
} }
func SearchAllLogs(keyword string) (logs []*Log, err error) { 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 err = LOG_DB.Where("type = ? or content LIKE ?", keyword, keyword+"%").Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error
return logs, err return logs, err
} }
func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { 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).Omit("id").Find(&logs).Error err = LOG_DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error
return logs, err return logs, err
} }
@@ -186,10 +185,10 @@ type Stat struct {
} }
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int) (stat Stat) { func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int) (stat Stat) {
tx := DB.Table("logs").Select("sum(quota) quota") tx := LOG_DB.Table("logs").Select("sum(quota) quota")
// 为rpm和tpm创建单独的查询 // 为rpm和tpm创建单独的查询
rpmTpmQuery := DB.Table("logs").Select("count(*) rpm, sum(prompt_tokens) + sum(completion_tokens) tpm") rpmTpmQuery := LOG_DB.Table("logs").Select("count(*) rpm, sum(prompt_tokens) + sum(completion_tokens) tpm")
if username != "" { if username != "" {
tx = tx.Where("username = ?", username) tx = tx.Where("username = ?", username)
@@ -206,8 +205,8 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa
tx = tx.Where("created_at <= ?", endTimestamp) tx = tx.Where("created_at <= ?", endTimestamp)
} }
if modelName != "" { if modelName != "" {
tx = tx.Where("model_name = ?", modelName) tx = tx.Where("model_name like ?", modelName)
rpmTpmQuery = rpmTpmQuery.Where("model_name = ?", modelName) rpmTpmQuery = rpmTpmQuery.Where("model_name like ?", modelName)
} }
if channel != 0 { if channel != 0 {
tx = tx.Where("channel_id = ?", channel) tx = tx.Where("channel_id = ?", channel)
@@ -228,7 +227,7 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa
} }
func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) { func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) {
tx := DB.Table("logs").Select("ifnull(sum(prompt_tokens),0) + ifnull(sum(completion_tokens),0)") tx := LOG_DB.Table("logs").Select("ifnull(sum(prompt_tokens),0) + ifnull(sum(completion_tokens),0)")
if username != "" { if username != "" {
tx = tx.Where("username = ?", username) tx = tx.Where("username = ?", username)
} }
@@ -248,7 +247,25 @@ func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelNa
return token return token
} }
func DeleteOldLog(targetTimestamp int64) (int64, error) { func DeleteOldLog(ctx context.Context, targetTimestamp int64, limit int) (int64, error) {
result := DB.Where("created_at < ?", targetTimestamp).Delete(&Log{}) var total int64 = 0
return result.RowsAffected, result.Error
for {
if nil != ctx.Err() {
return total, ctx.Err()
}
result := LOG_DB.Where("created_at < ?", targetTimestamp).Limit(limit).Delete(&Log{})
if nil != result.Error {
return total, result.Error
}
total += result.RowsAffected
if result.RowsAffected < int64(limit) {
break
}
}
return total, nil
} }

View File

@@ -15,6 +15,8 @@ import (
var DB *gorm.DB var DB *gorm.DB
var LOG_DB *gorm.DB
func createRootAccountIfNeed() error { func createRootAccountIfNeed() error {
var user User var user User
//if user.Status != common.UserStatusEnabled { //if user.Status != common.UserStatusEnabled {
@@ -38,9 +40,9 @@ func createRootAccountIfNeed() error {
return nil return nil
} }
func chooseDB() (*gorm.DB, error) { func chooseDB(envName string) (*gorm.DB, error) {
if os.Getenv("SQL_DSN") != "" { dsn := os.Getenv(envName)
dsn := os.Getenv("SQL_DSN") if dsn != "" {
if strings.HasPrefix(dsn, "postgres://") { if strings.HasPrefix(dsn, "postgres://") {
// Use PostgreSQL // Use PostgreSQL
common.SysLog("using PostgreSQL as database") common.SysLog("using PostgreSQL as database")
@@ -52,6 +54,13 @@ func chooseDB() (*gorm.DB, error) {
PrepareStmt: true, // precompile SQL PrepareStmt: true, // precompile SQL
}) })
} }
if strings.HasPrefix(dsn, "local") {
common.SysLog("SQL_DSN not set, using SQLite as database")
common.UsingSQLite = true
return gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
PrepareStmt: true, // precompile SQL
})
}
// Use MySQL // Use MySQL
common.SysLog("using MySQL as database") common.SysLog("using MySQL as database")
// check parseTime // check parseTime
@@ -76,7 +85,7 @@ func chooseDB() (*gorm.DB, error) {
} }
func InitDB() (err error) { func InitDB() (err error) {
db, err := chooseDB() db, err := chooseDB("SQL_DSN")
if err == nil { if err == nil {
if common.DebugEnabled { if common.DebugEnabled {
db = db.Debug() db = db.Debug()
@@ -100,52 +109,7 @@ func InitDB() (err error) {
// _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY status VARCHAR(20);") // TODO: delete this line when most users have upgraded // _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY status VARCHAR(20);") // TODO: delete this line when most users have upgraded
//} //}
common.SysLog("database migration started") common.SysLog("database migration started")
err = db.AutoMigrate(&Channel{}) err = migrateDB()
if err != nil {
return err
}
err = db.AutoMigrate(&Token{})
if err != nil {
return err
}
err = db.AutoMigrate(&User{})
if err != nil {
return err
}
err = db.AutoMigrate(&Option{})
if err != nil {
return err
}
err = db.AutoMigrate(&Redemption{})
if err != nil {
return err
}
err = db.AutoMigrate(&Ability{})
if err != nil {
return err
}
err = db.AutoMigrate(&Log{})
if err != nil {
return err
}
err = db.AutoMigrate(&Midjourney{})
if err != nil {
return err
}
err = db.AutoMigrate(&TopUp{})
if err != nil {
return err
}
err = db.AutoMigrate(&QuotaData{})
if err != nil {
return err
}
err = db.AutoMigrate(&Task{})
if err != nil {
return err
}
common.SysLog("database migrated")
err = createRootAccountIfNeed()
return err return err
} else { } else {
common.FatalLog(err) common.FatalLog(err)
@@ -153,8 +117,103 @@ func InitDB() (err error) {
return err return err
} }
func CloseDB() error { func InitLogDB() (err error) {
sqlDB, err := DB.DB() if os.Getenv("LOG_SQL_DSN") == "" {
LOG_DB = DB
return
}
db, err := chooseDB("LOG_SQL_DSN")
if err == nil {
if common.DebugEnabled {
db = db.Debug()
}
LOG_DB = db
sqlDB, err := LOG_DB.DB()
if err != nil {
return err
}
sqlDB.SetMaxIdleConns(common.GetEnvOrDefault("SQL_MAX_IDLE_CONNS", 100))
sqlDB.SetMaxOpenConns(common.GetEnvOrDefault("SQL_MAX_OPEN_CONNS", 1000))
sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetEnvOrDefault("SQL_MAX_LIFETIME", 60)))
if !common.IsMasterNode {
return nil
}
//if common.UsingMySQL {
// _, _ = sqlDB.Exec("DROP INDEX idx_channels_key ON channels;") // TODO: delete this line when most users have upgraded
// _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY action VARCHAR(40);") // TODO: delete this line when most users have upgraded
// _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY progress VARCHAR(30);") // TODO: delete this line when most users have upgraded
// _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY status VARCHAR(20);") // TODO: delete this line when most users have upgraded
//}
common.SysLog("database migration started")
err = migrateLOGDB()
return err
} else {
common.FatalLog(err)
}
return err
}
func migrateDB() error {
err := DB.AutoMigrate(&Channel{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Token{})
if err != nil {
return err
}
err = DB.AutoMigrate(&User{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Option{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Redemption{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Ability{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Log{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Midjourney{})
if err != nil {
return err
}
err = DB.AutoMigrate(&TopUp{})
if err != nil {
return err
}
err = DB.AutoMigrate(&QuotaData{})
if err != nil {
return err
}
err = DB.AutoMigrate(&Task{})
if err != nil {
return err
}
common.SysLog("database migrated")
err = createRootAccountIfNeed()
return err
}
func migrateLOGDB() error {
var err error
if err = LOG_DB.AutoMigrate(&Log{}); err != nil {
return err
}
return nil
}
func closeDB(db *gorm.DB) error {
sqlDB, err := db.DB()
if err != nil { if err != nil {
return err return err
} }
@@ -162,6 +221,16 @@ func CloseDB() error {
return err return err
} }
func CloseDB() error {
if LOG_DB != DB {
err := closeDB(LOG_DB)
if err != nil {
return err
}
}
return closeDB(DB)
}
var ( var (
lastPingTime time.Time lastPingTime time.Time
pingMutex sync.Mutex pingMutex sync.Mutex

View File

@@ -90,6 +90,7 @@ func InitOptionMap() {
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString() common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString() common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString() common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
common.OptionMap["TopUpLink"] = common.TopUpLink common.OptionMap["TopUpLink"] = common.TopUpLink
common.OptionMap["ChatLink"] = common.ChatLink common.OptionMap["ChatLink"] = common.ChatLink
@@ -315,6 +316,8 @@ func updateOptionMap(key string, value string) (err error) {
err = common.UpdateModelRatioByJSONString(value) err = common.UpdateModelRatioByJSONString(value)
case "GroupRatio": case "GroupRatio":
err = common.UpdateGroupRatioByJSONString(value) err = common.UpdateGroupRatioByJSONString(value)
case "UserUsableGroups":
err = common.UpdateUserUsableGroupsByJSONString(value)
case "CompletionRatio": case "CompletionRatio":
err = common.UpdateCompletionRatioByJSONString(value) err = common.UpdateCompletionRatioByJSONString(value)
case "ModelPrice": case "ModelPrice":

View File

@@ -7,14 +7,13 @@ import (
) )
type Pricing struct { type Pricing struct {
Available bool `json:"available"`
ModelName string `json:"model_name"` ModelName string `json:"model_name"`
QuotaType int `json:"quota_type"` QuotaType int `json:"quota_type"`
ModelRatio float64 `json:"model_ratio"` ModelRatio float64 `json:"model_ratio"`
ModelPrice float64 `json:"model_price"` ModelPrice float64 `json:"model_price"`
OwnerBy string `json:"owner_by"` OwnerBy string `json:"owner_by"`
CompletionRatio float64 `json:"completion_ratio"` CompletionRatio float64 `json:"completion_ratio"`
EnableGroup []string `json:"enable_group,omitempty"` EnableGroup []string `json:"enable_groups,omitempty"`
} }
var ( var (
@@ -23,40 +22,47 @@ var (
updatePricingLock sync.Mutex updatePricingLock sync.Mutex
) )
func GetPricing(group string) []Pricing { func GetPricing() []Pricing {
updatePricingLock.Lock() updatePricingLock.Lock()
defer updatePricingLock.Unlock() defer updatePricingLock.Unlock()
if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 { if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
updatePricing() updatePricing()
} }
if group != "" { //if group != "" {
userPricingMap := make([]Pricing, 0) // userPricingMap := make([]Pricing, 0)
models := GetGroupModels(group) // models := GetGroupModels(group)
for _, pricing := range pricingMap { // for _, pricing := range pricingMap {
if !common.StringsContains(models, pricing.ModelName) { // if !common.StringsContains(models, pricing.ModelName) {
pricing.Available = false // pricing.Available = false
} // }
userPricingMap = append(userPricingMap, pricing) // userPricingMap = append(userPricingMap, pricing)
} // }
return userPricingMap // return userPricingMap
} //}
return pricingMap return pricingMap
} }
func updatePricing() { func updatePricing() {
//modelRatios := common.GetModelRatios() //modelRatios := common.GetModelRatios()
enabledModels := GetEnabledModels() enableAbilities := GetAllEnableAbilities()
allModels := make(map[string]int) modelGroupsMap := make(map[string][]string)
for i, model := range enabledModels { for _, ability := range enableAbilities {
allModels[model] = i groups := modelGroupsMap[ability.Model]
if groups == nil {
groups = make([]string, 0)
}
if !common.StringsContains(groups, ability.Group) {
groups = append(groups, ability.Group)
}
modelGroupsMap[ability.Model] = groups
} }
pricingMap = make([]Pricing, 0) pricingMap = make([]Pricing, 0)
for model, _ := range allModels { for model, groups := range modelGroupsMap {
pricing := Pricing{ pricing := Pricing{
Available: true, ModelName: model,
ModelName: model, EnableGroup: groups,
} }
modelPrice, findPrice := common.GetModelPrice(model, false) modelPrice, findPrice := common.GetModelPrice(model, false)
if findPrice { if findPrice {

View File

@@ -22,10 +22,34 @@ type Token struct {
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"` UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"` ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"`
ModelLimits string `json:"model_limits" gorm:"type:varchar(1024);default:''"` ModelLimits string `json:"model_limits" gorm:"type:varchar(1024);default:''"`
AllowIps *string `json:"allow_ips" gorm:"default:''"`
UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota
Group string `json:"group" gorm:"default:''"`
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
} }
func (token *Token) GetIpLimitsMap() map[string]any {
// delete empty spaces
//split with \n
ipLimitsMap := make(map[string]any)
if token.AllowIps == nil {
return ipLimitsMap
}
cleanIps := strings.ReplaceAll(*token.AllowIps, " ", "")
if cleanIps == "" {
return ipLimitsMap
}
ips := strings.Split(cleanIps, "\n")
for _, ip := range ips {
ip = strings.TrimSpace(ip)
ip = strings.ReplaceAll(ip, ",", "")
if common.IsIP(ip) {
ipLimitsMap[ip] = true
}
}
return ipLimitsMap
}
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
var tokens []*Token var tokens []*Token
var err error var err error
@@ -129,7 +153,8 @@ func (token *Token) Insert() error {
// Update Make sure your token's fields is completed, because this will update non-zero values // Update Make sure your token's fields is completed, because this will update non-zero values
func (token *Token) Update() error { func (token *Token) Update() error {
var err error var err error
err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits").Updates(token).Error err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota",
"model_limits_enabled", "model_limits", "allow_ips", "group").Updates(token).Error
return err return err
} }

View File

@@ -36,8 +36,8 @@ type AliEmbeddingRequest struct {
} }
type AliEmbedding struct { type AliEmbedding struct {
Embedding []float64 `json:"embedding"` Embedding any `json:"embedding"`
TextIndex int `json:"text_index"` TextIndex int `json:"text_index"`
} }
type AliEmbeddingResponse struct { type AliEmbeddingResponse struct {

View File

@@ -50,9 +50,9 @@ type BaiduEmbeddingRequest struct {
} }
type BaiduEmbeddingData struct { type BaiduEmbeddingData struct {
Object string `json:"object"` Object string `json:"object"`
Embedding []float64 `json:"embedding"` Embedding any `json:"embedding"`
Index int `json:"index"` Index int `json:"index"`
} }
type BaiduEmbeddingResponse struct { type BaiduEmbeddingResponse struct {

View File

@@ -79,9 +79,9 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream { if info.IsStream {
err, usage = claudeStreamHandler(c, resp, info, a.RequestMode) err, usage = ClaudeStreamHandler(c, resp, info, a.RequestMode)
} else { } else {
err, usage = claudeHandler(a.RequestMode, c, resp, info.PromptTokens, info.UpstreamModelName) err, usage = ClaudeHandler(c, resp, a.RequestMode, info)
} }
return return
} }

View File

@@ -31,9 +31,9 @@ type ClaudeMessage struct {
} }
type Tool struct { type Tool struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
InputSchema InputSchema `json:"input_schema"` InputSchema map[string]interface{} `json:"input_schema"`
} }
type InputSchema struct { type InputSchema struct {

View File

@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"io" "io"
"net/http" "net/http"
"one-api/common" "one-api/common"
@@ -12,6 +11,8 @@ import (
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
"one-api/service" "one-api/service"
"strings" "strings"
"github.com/gin-gonic/gin"
) )
func stopReasonClaude2OpenAI(reason string) string { func stopReasonClaude2OpenAI(reason string) string {
@@ -63,15 +64,21 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
for _, tool := range textRequest.Tools { for _, tool := range textRequest.Tools {
if params, ok := tool.Function.Parameters.(map[string]any); ok { if params, ok := tool.Function.Parameters.(map[string]any); ok {
claudeTools = append(claudeTools, Tool{ claudeTool := Tool{
Name: tool.Function.Name, Name: tool.Function.Name,
Description: tool.Function.Description, Description: tool.Function.Description,
InputSchema: InputSchema{ }
Type: params["type"].(string), claudeTool.InputSchema = make(map[string]interface{})
Properties: params["properties"], claudeTool.InputSchema["type"] = params["type"].(string)
Required: params["required"], claudeTool.InputSchema["properties"] = params["properties"]
}, claudeTool.InputSchema["required"] = params["required"]
}) for s, a := range params {
if s == "type" || s == "properties" || s == "required" {
continue
}
claudeTool.InputSchema[s] = a
}
claudeTools = append(claudeTools, claudeTool)
} }
} }
@@ -102,13 +109,10 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
} }
} }
formatMessages := make([]dto.Message, 0) formatMessages := make([]dto.Message, 0)
var lastMessage *dto.Message lastMessage := dto.Message{
Role: "tool",
}
for i, message := range textRequest.Messages { for i, message := range textRequest.Messages {
//if message.Role == "system" {
// if i != 0 {
// message.Role = "user"
// }
//}
if message.Role == "" { if message.Role == "" {
textRequest.Messages[i].Role = "user" textRequest.Messages[i].Role = "user"
} }
@@ -116,7 +120,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
Role: message.Role, Role: message.Role,
Content: message.Content, Content: message.Content,
} }
if lastMessage != nil && lastMessage.Role == message.Role { if message.Role == "tool" {
fmtMessage.ToolCallId = message.ToolCallId
}
if message.Role == "assistant" && message.ToolCalls != nil {
fmtMessage.ToolCalls = message.ToolCalls
}
if lastMessage.Role == message.Role && lastMessage.Role != "tool" {
if lastMessage.IsStringContent() && message.IsStringContent() { if lastMessage.IsStringContent() && message.IsStringContent() {
content, _ := json.Marshal(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\"")) content, _ := json.Marshal(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\""))
fmtMessage.Content = content fmtMessage.Content = content
@@ -129,10 +139,11 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
fmtMessage.Content = content fmtMessage.Content = content
} }
formatMessages = append(formatMessages, fmtMessage) formatMessages = append(formatMessages, fmtMessage)
lastMessage = &textRequest.Messages[i] lastMessage = fmtMessage
} }
claudeMessages := make([]ClaudeMessage, 0) claudeMessages := make([]ClaudeMessage, 0)
isFirstMessage := true
for _, message := range formatMessages { for _, message := range formatMessages {
if message.Role == "system" { if message.Role == "system" {
if message.IsStringContent() { if message.IsStringContent() {
@@ -148,10 +159,54 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
claudeRequest.System = content claudeRequest.System = content
} }
} else { } else {
if isFirstMessage {
isFirstMessage = false
if message.Role != "user" {
// fix: first message is assistant, add user message
claudeMessage := ClaudeMessage{
Role: "user",
Content: []ClaudeMediaMessage{
{
Type: "text",
Text: "...",
},
},
}
claudeMessages = append(claudeMessages, claudeMessage)
}
}
claudeMessage := ClaudeMessage{ claudeMessage := ClaudeMessage{
Role: message.Role, Role: message.Role,
} }
if message.IsStringContent() { if message.Role == "tool" {
if len(claudeMessages) > 0 && claudeMessages[len(claudeMessages)-1].Role == "user" {
lastMessage := claudeMessages[len(claudeMessages)-1]
if content, ok := lastMessage.Content.(string); ok {
lastMessage.Content = []ClaudeMediaMessage{
{
Type: "text",
Text: content,
},
}
}
lastMessage.Content = append(lastMessage.Content.([]ClaudeMediaMessage), ClaudeMediaMessage{
Type: "tool_result",
ToolUseId: message.ToolCallId,
Content: message.StringContent(),
})
claudeMessages[len(claudeMessages)-1] = lastMessage
continue
} else {
claudeMessage.Role = "user"
claudeMessage.Content = []ClaudeMediaMessage{
{
Type: "tool_result",
ToolUseId: message.ToolCallId,
Content: message.StringContent(),
},
}
}
} else if message.IsStringContent() && message.ToolCalls == nil {
claudeMessage.Content = message.StringContent() claudeMessage.Content = message.StringContent()
} else { } else {
claudeMediaMessages := make([]ClaudeMediaMessage, 0) claudeMediaMessages := make([]ClaudeMediaMessage, 0)
@@ -184,6 +239,28 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
} }
claudeMediaMessages = append(claudeMediaMessages, claudeMediaMessage) claudeMediaMessages = append(claudeMediaMessages, claudeMediaMessage)
} }
if message.ToolCalls != nil {
for _, tc := range message.ToolCalls.([]interface{}) {
toolCallJSON, _ := json.Marshal(tc)
var toolCall dto.ToolCall
err := json.Unmarshal(toolCallJSON, &toolCall)
if err != nil {
common.SysError("tool call is not a dto.ToolCall: " + fmt.Sprintf("%v", tc))
continue
}
inputObj := make(map[string]any)
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
common.SysError("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments))
continue
}
claudeMediaMessages = append(claudeMediaMessages, ClaudeMediaMessage{
Type: "tool_use",
Id: toolCall.ID,
Name: toolCall.Function.Name,
Input: inputObj,
})
}
}
claudeMessage.Content = claudeMediaMessages claudeMessage.Content = claudeMediaMessages
} }
claudeMessages = append(claudeMessages, claudeMessage) claudeMessages = append(claudeMessages, claudeMessage)
@@ -318,12 +395,13 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope
if len(tools) > 0 { if len(tools) > 0 {
choice.Message.ToolCalls = tools choice.Message.ToolCalls = tools
} }
fullTextResponse.Model = claudeResponse.Model
choices = append(choices, choice) choices = append(choices, choice)
fullTextResponse.Choices = choices fullTextResponse.Choices = choices
return &fullTextResponse return &fullTextResponse
} }
func claudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID()) responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
var usage *dto.Usage var usage *dto.Usage
usage = &dto.Usage{} usage = &dto.Usage{}
@@ -405,7 +483,7 @@ func claudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.
return nil, usage return nil, usage
} }
func claudeHandler(requestMode int, c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body) responseBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
@@ -431,15 +509,15 @@ func claudeHandler(requestMode int, c *gin.Context, resp *http.Response, promptT
}, nil }, nil
} }
fullTextResponse := ResponseClaude2OpenAI(requestMode, &claudeResponse) fullTextResponse := ResponseClaude2OpenAI(requestMode, &claudeResponse)
completionTokens, err := service.CountTokenText(claudeResponse.Completion, model) completionTokens, err := service.CountTokenText(claudeResponse.Completion, info.OriginModelName)
if err != nil { if err != nil {
return service.OpenAIErrorWrapper(err, "count_token_text_failed", http.StatusInternalServerError), nil return service.OpenAIErrorWrapper(err, "count_token_text_failed", http.StatusInternalServerError), nil
} }
usage := dto.Usage{} usage := dto.Usage{}
if requestMode == RequestModeCompletion { if requestMode == RequestModeCompletion {
usage.PromptTokens = promptTokens usage.PromptTokens = info.PromptTokens
usage.CompletionTokens = completionTokens usage.CompletionTokens = completionTokens
usage.TotalTokens = promptTokens + completionTokens usage.TotalTokens = info.PromptTokens + completionTokens
} else { } else {
usage.PromptTokens = claudeResponse.Usage.InputTokens usage.PromptTokens = claudeResponse.Usage.InputTokens
usage.CompletionTokens = claudeResponse.Usage.OutputTokens usage.CompletionTokens = claudeResponse.Usage.OutputTokens

View File

@@ -1,7 +1,10 @@
package cohere package cohere
var ModelList = []string{ var ModelList = []string{
"command-r", "command-r-plus", "command-light", "command-light-nightly", "command", "command-nightly", "command-r", "command-r-plus",
"command-r-08-2024", "command-r-plus-08-2024",
"c4ai-aya-23-35b", "c4ai-aya-23-8b",
"command-light", "command-light-nightly", "command", "command-nightly",
"rerank-english-v3.0", "rerank-multilingual-v3.0", "rerank-english-v2.0", "rerank-multilingual-v2.0", "rerank-english-v3.0", "rerank-multilingual-v3.0", "rerank-english-v2.0", "rerank-multilingual-v2.0",
} }

View File

@@ -8,6 +8,7 @@ type CohereRequest struct {
Message string `json:"message"` Message string `json:"message"`
Stream bool `json:"stream"` Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens"` MaxTokens int `json:"max_tokens"`
SafetyMode string `json:"safety_mode,omitempty"`
} }
type ChatHistory struct { type ChatHistory struct {

View File

@@ -23,6 +23,9 @@ func requestOpenAI2Cohere(textRequest dto.GeneralOpenAIRequest) *CohereRequest {
Stream: textRequest.Stream, Stream: textRequest.Stream,
MaxTokens: textRequest.GetMaxTokens(), MaxTokens: textRequest.GetMaxTokens(),
} }
if common.CohereSafetySetting != "NONE" {
cohereReq.SafetyMode = common.CohereSafetySetting
}
if cohereReq.MaxTokens == 0 { if cohereReq.MaxTokens == 0 {
cohereReq.MaxTokens = 4000 cohereReq.MaxTokens = 4000
} }
@@ -44,6 +47,7 @@ func requestOpenAI2Cohere(textRequest dto.GeneralOpenAIRequest) *CohereRequest {
}) })
} }
} }
return &cohereReq return &cohereReq
} }

View File

@@ -70,9 +70,9 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream { if info.IsStream {
err, usage = geminiChatStreamHandler(c, resp, info) err, usage = GeminiChatStreamHandler(c, resp, info)
} else { } else {
err, usage = geminiChatHandler(c, resp, info.PromptTokens, info.UpstreamModelName) err, usage = GeminiChatHandler(c, resp)
} }
return return
} }

View File

@@ -6,7 +6,7 @@ const (
var ModelList = []string{ var ModelList = []string{
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra", "gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
"gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
} }
var ChannelName = "google gemini" var ChannelName = "google gemini"

View File

@@ -220,7 +220,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *dto.Ch
return &response return &response
} }
func geminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseText := "" responseText := ""
id := fmt.Sprintf("chatcmpl-%s", common.GetUUID()) id := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
createAt := common.GetTimestamp() createAt := common.GetTimestamp()
@@ -279,7 +279,7 @@ func geminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
return nil, usage return nil, usage
} }
func geminiChatHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func GeminiChatHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body) responseBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil

View File

@@ -32,7 +32,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayMode == constant.RelayModeRerank { if info.RelayMode == constant.RelayModeRerank {
return fmt.Sprintf("%s/v1/rerank", info.BaseUrl), nil return fmt.Sprintf("%s/v1/rerank", info.BaseUrl), nil
} else if info.RelayMode == constant.RelayModeEmbeddings { } else if info.RelayMode == constant.RelayModeEmbeddings {
return fmt.Sprintf("%s/v1/embeddings ", info.BaseUrl), nil return fmt.Sprintf("%s/v1/embeddings", info.BaseUrl), nil
} }
return "", errors.New("invalid relay mode") return "", errors.New("invalid relay mode")
} }
@@ -58,6 +58,8 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.RelayMode == constant.RelayModeRerank { if info.RelayMode == constant.RelayModeRerank {
err, usage = jinaRerankHandler(c, resp) err, usage = jinaRerankHandler(c, resp)
} else if info.RelayMode == constant.RelayModeEmbeddings {
err, usage = jinaEmbeddingHandler(c, resp)
} }
return return
} }

View File

@@ -33,3 +33,28 @@ func jinaRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWit
_, err = c.Writer.Write(jsonResponse) _, err = c.Writer.Write(jsonResponse)
return nil, &jinaResp.Usage return nil, &jinaResp.Usage
} }
func jinaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
var jinaResp dto.OpenAIEmbeddingResponse
err = json.Unmarshal(responseBody, &jinaResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
jsonResponse, err := json.Marshal(jinaResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, err = c.Writer.Write(jsonResponse)
return nil, &jinaResp.Usage
}

View File

@@ -17,11 +17,25 @@ type OllamaRequest struct {
PresencePenalty float64 `json:"presence_penalty,omitempty"` PresencePenalty float64 `json:"presence_penalty,omitempty"`
} }
type Options struct {
Seed int `json:"seed,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopK int `json:"top_k,omitempty"`
TopP float64 `json:"top_p,omitempty"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
PresencePenalty float64 `json:"presence_penalty,omitempty"`
NumPredict int `json:"num_predict,omitempty"`
NumCtx int `json:"num_ctx,omitempty"`
}
type OllamaEmbeddingRequest struct { type OllamaEmbeddingRequest struct {
Model string `json:"model,omitempty"` Model string `json:"model,omitempty"`
Prompt any `json:"prompt,omitempty"` Input []string `json:"input"`
Options *Options `json:"options,omitempty"`
} }
type OllamaEmbeddingResponse struct { type OllamaEmbeddingResponse struct {
Embedding []float64 `json:"embedding,omitempty"` Embedding any `json:"embedding,omitempty"`
Error string `json:"error,omitempty"`
Model string `json:"model"`
} }

View File

@@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"one-api/dto" "one-api/dto"
"one-api/service" "one-api/service"
"strings"
) )
func requestOpenAI2Ollama(request dto.GeneralOpenAIRequest) *OllamaRequest { func requestOpenAI2Ollama(request dto.GeneralOpenAIRequest) *OllamaRequest {
@@ -45,8 +44,15 @@ func requestOpenAI2Ollama(request dto.GeneralOpenAIRequest) *OllamaRequest {
func requestOpenAI2Embeddings(request dto.GeneralOpenAIRequest) *OllamaEmbeddingRequest { func requestOpenAI2Embeddings(request dto.GeneralOpenAIRequest) *OllamaEmbeddingRequest {
return &OllamaEmbeddingRequest{ return &OllamaEmbeddingRequest{
Model: request.Model, Model: request.Model,
Prompt: strings.Join(request.ParseInput(), " "), Input: request.ParseInput(),
Options: &Options{
Seed: int(request.Seed),
Temperature: request.Temperature,
TopP: request.TopP,
FrequencyPenalty: request.FrequencyPenalty,
PresencePenalty: request.PresencePenalty,
},
} }
} }
@@ -64,6 +70,9 @@ func ollamaEmbeddingHandler(c *gin.Context, resp *http.Response, promptTokens in
if err != nil { if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
} }
if ollamaEmbeddingResponse.Error != "" {
return service.OpenAIErrorWrapper(err, "ollama_error", resp.StatusCode), nil
}
data := make([]dto.OpenAIEmbeddingResponseItem, 0, 1) data := make([]dto.OpenAIEmbeddingResponseItem, 0, 1)
data = append(data, dto.OpenAIEmbeddingResponseItem{ data = append(data, dto.OpenAIEmbeddingResponseItem{
Embedding: ollamaEmbeddingResponse.Embedding, Embedding: ollamaEmbeddingResponse.Embedding,

View File

@@ -78,6 +78,12 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
if info.ChannelType != common.ChannelTypeOpenAI { if info.ChannelType != common.ChannelTypeOpenAI {
request.StreamOptions = nil request.StreamOptions = nil
} }
if "o1" == request.Model || strings.HasPrefix(request.Model, "o1-") {
if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 {
request.MaxCompletionTokens = request.MaxTokens
request.MaxTokens = 0
}
}
return request, nil return request, nil
} }

View File

@@ -8,8 +8,11 @@ var ModelList = []string{
"gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613",
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", "gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
"gpt-4-vision-preview", "gpt-4-vision-preview",
"chatgpt-4o-latest",
"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", "gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06",
"gpt-4o-mini", "gpt-4o-mini-2024-07-18", "gpt-4o-mini", "gpt-4o-mini-2024-07-18",
"o1-preview", "o1-preview-2024-09-12",
"o1-mini", "o1-mini-2024-09-12",
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large", "text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
"text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003", "text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003",
"text-moderation-latest", "text-moderation-stable", "text-moderation-latest", "text-moderation-stable",

View File

@@ -0,0 +1,83 @@
package siliconflow
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
)
type Adaptor struct {
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayMode == constant.RelayModeRerank {
return fmt.Sprintf("%s/v1/rerank", info.BaseUrl), nil
} else if info.RelayMode == constant.RelayModeEmbeddings {
return fmt.Sprintf("%s/v1/embeddings", info.BaseUrl), nil
} else if info.RelayMode == constant.RelayModeChatCompletions {
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
}
return "", errors.New("invalid relay mode")
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey))
return nil
}
func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
return request, nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
return channel.DoApiRequest(a, c, info, requestBody)
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
return request, nil
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
switch info.RelayMode {
case constant.RelayModeRerank:
err, usage = siliconflowRerankHandler(c, resp)
case constant.RelayModeChatCompletions:
if info.IsStream {
err, usage = openai.OaiStreamHandler(c, resp, info)
} else {
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
}
case constant.RelayModeEmbeddings:
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
}
return
}
func (a *Adaptor) GetModelList() []string {
return ModelList
}
func (a *Adaptor) GetChannelName() string {
return ChannelName
}

View File

@@ -0,0 +1,51 @@
package siliconflow
var ModelList = []string{
"THUDM/glm-4-9b-chat",
//"stabilityai/stable-diffusion-xl-base-1.0",
//"TencentARC/PhotoMaker",
"InstantX/InstantID",
//"stabilityai/stable-diffusion-2-1",
//"stabilityai/sd-turbo",
//"stabilityai/sdxl-turbo",
"ByteDance/SDXL-Lightning",
"deepseek-ai/deepseek-llm-67b-chat",
"Qwen/Qwen1.5-14B-Chat",
"Qwen/Qwen1.5-7B-Chat",
"Qwen/Qwen1.5-110B-Chat",
"Qwen/Qwen1.5-32B-Chat",
"01-ai/Yi-1.5-6B-Chat",
"01-ai/Yi-1.5-9B-Chat-16K",
"01-ai/Yi-1.5-34B-Chat-16K",
"THUDM/chatglm3-6b",
"deepseek-ai/DeepSeek-V2-Chat",
"Qwen/Qwen2-72B-Instruct",
"Qwen/Qwen2-7B-Instruct",
"Qwen/Qwen2-57B-A14B-Instruct",
//"stabilityai/stable-diffusion-3-medium",
"deepseek-ai/DeepSeek-Coder-V2-Instruct",
"Qwen/Qwen2-1.5B-Instruct",
"internlm/internlm2_5-7b-chat",
"BAAI/bge-large-en-v1.5",
"BAAI/bge-large-zh-v1.5",
"Pro/Qwen/Qwen2-7B-Instruct",
"Pro/Qwen/Qwen2-1.5B-Instruct",
"Pro/Qwen/Qwen1.5-7B-Chat",
"Pro/THUDM/glm-4-9b-chat",
"Pro/THUDM/chatglm3-6b",
"Pro/01-ai/Yi-1.5-9B-Chat-16K",
"Pro/01-ai/Yi-1.5-6B-Chat",
"Pro/google/gemma-2-9b-it",
"Pro/internlm/internlm2_5-7b-chat",
"Pro/meta-llama/Meta-Llama-3-8B-Instruct",
"Pro/mistralai/Mistral-7B-Instruct-v0.2",
"black-forest-labs/FLUX.1-schnell",
"iic/SenseVoiceSmall",
"netease-youdao/bce-embedding-base_v1",
"BAAI/bge-m3",
"internlm/internlm2_5-20b-chat",
"Qwen/Qwen2-Math-72B-Instruct",
"netease-youdao/bce-reranker-base_v1",
"BAAI/bge-reranker-v2-m3",
}
var ChannelName = "siliconflow"

View File

@@ -0,0 +1,17 @@
package siliconflow
import "one-api/dto"
type SFTokens struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
type SFMeta struct {
Tokens SFTokens `json:"tokens"`
}
type SFRerankResponse struct {
Results []dto.RerankResponseDocument `json:"results"`
Meta SFMeta `json:"meta"`
}

View File

@@ -0,0 +1,44 @@
package siliconflow
import (
"encoding/json"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/dto"
"one-api/service"
)
func siliconflowRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
var siliconflowResp SFRerankResponse
err = json.Unmarshal(responseBody, &siliconflowResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
usage := &dto.Usage{
PromptTokens: siliconflowResp.Meta.Tokens.InputTokens,
CompletionTokens: siliconflowResp.Meta.Tokens.OutputTokens,
TotalTokens: siliconflowResp.Meta.Tokens.InputTokens + siliconflowResp.Meta.Tokens.OutputTokens,
}
rerankResp := &dto.RerankResponse{
Results: siliconflowResp.Results,
Usage: *usage,
}
jsonResponse, err := json.Marshal(rerankResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, err = c.Writer.Write(jsonResponse)
return nil, usage
}

View File

@@ -0,0 +1,184 @@
package vertex
import (
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"io"
"net/http"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/claude"
"one-api/relay/channel/gemini"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"strings"
)
const (
RequestModeClaude = 1
RequestModeGemini = 2
RequestModeLlama = 3
)
var claudeModelMap = map[string]string{
"claude-3-sonnet-20240229": "claude-3-sonnet@20240229",
"claude-3-opus-20240229": "claude-3-opus@20240229",
"claude-3-haiku-20240307": "claude-3-haiku@20240307",
"claude-3-5-sonnet-20240620": "claude-3-5-sonnet@20240620",
}
const anthropicVersion = "vertex-2023-10-16"
type Adaptor struct {
RequestMode int
AccountCredentials Credentials
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
if strings.HasPrefix(info.UpstreamModelName, "claude") {
a.RequestMode = RequestModeClaude
} else if strings.HasPrefix(info.UpstreamModelName, "gemini") {
a.RequestMode = RequestModeGemini
} else if strings.Contains(info.UpstreamModelName, "llama") {
a.RequestMode = RequestModeLlama
}
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
adc := &Credentials{}
if err := json.Unmarshal([]byte(info.ApiKey), adc); err != nil {
return "", fmt.Errorf("failed to decode credentials file: %w", err)
}
region := GetModelRegion(info.ApiVersion, info.OriginModelName)
a.AccountCredentials = *adc
suffix := ""
if a.RequestMode == RequestModeGemini {
if info.IsStream {
suffix = "streamGenerateContent?alt=sse"
} else {
suffix = "generateContent"
}
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
region,
adc.ProjectID,
region,
info.UpstreamModelName,
suffix,
), nil
} else if a.RequestMode == RequestModeClaude {
if info.IsStream {
suffix = "streamRawPredict?alt=sse"
} else {
suffix = "rawPredict"
}
if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
info.UpstreamModelName = v
}
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s",
region,
adc.ProjectID,
region,
info.UpstreamModelName,
suffix,
), nil
} else if a.RequestMode == RequestModeLlama {
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
region,
adc.ProjectID,
region,
), nil
}
return "", errors.New("unsupported request mode")
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
accessToken, err := getAccessToken(a, info)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
return nil
}
func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
if request == nil {
return nil, errors.New("request is nil")
}
if a.RequestMode == RequestModeClaude {
claudeReq, err := claude.RequestOpenAI2ClaudeMessage(*request)
if err != nil {
return nil, err
}
vertexClaudeReq := &VertexAIClaudeRequest{
AnthropicVersion: anthropicVersion,
}
if err = copier.Copy(vertexClaudeReq, claudeReq); err != nil {
return nil, errors.New("failed to copy claude request")
}
c.Set("request_model", request.Model)
return vertexClaudeReq, nil
} else if a.RequestMode == RequestModeGemini {
geminiRequest := gemini.CovertGemini2OpenAI(*request)
c.Set("request_model", request.Model)
return geminiRequest, nil
} else if a.RequestMode == RequestModeLlama {
return request, nil
}
return nil, errors.New("unsupported request mode")
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
return nil, nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
return channel.DoApiRequest(a, c, info, requestBody)
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream {
switch a.RequestMode {
case RequestModeClaude:
err, usage = claude.ClaudeStreamHandler(c, resp, info, claude.RequestModeMessage)
case RequestModeGemini:
err, usage = gemini.GeminiChatStreamHandler(c, resp, info)
case RequestModeLlama:
err, usage = openai.OaiStreamHandler(c, resp, info)
}
} else {
switch a.RequestMode {
case RequestModeClaude:
err, usage = claude.ClaudeHandler(c, resp, claude.RequestModeMessage, info)
case RequestModeGemini:
err, usage = gemini.GeminiChatHandler(c, resp)
case RequestModeLlama:
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.OriginModelName)
}
}
return
}
func (a *Adaptor) GetModelList() []string {
return ModelList
}
func (a *Adaptor) GetChannelName() string {
return ChannelName
}

View File

@@ -0,0 +1,15 @@
package vertex
var ModelList = []string{
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-haiku-20240307",
"claude-3-5-sonnet-20240620",
//"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision",
"meta/llama3-405b-instruct-maas",
}
var ChannelName = "vertex-ai"

View File

@@ -0,0 +1,17 @@
package vertex
import "one-api/relay/channel/claude"
type VertexAIClaudeRequest struct {
AnthropicVersion string `json:"anthropic_version"`
Messages []claude.ClaudeMessage `json:"messages"`
System string `json:"system,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
StopSequences []string `json:"stop_sequences,omitempty"`
Stream bool `json:"stream,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
Tools []claude.Tool `json:"tools,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
}

View File

@@ -0,0 +1,16 @@
package vertex
import "one-api/common"
func GetModelRegion(other string, localModelName string) string {
// if other is json string
if common.IsJsonStr(other) {
m := common.StrToMap(other)
if m[localModelName] != nil {
return m[localModelName].(string)
} else {
return m["default"].(string)
}
}
return other
}

View File

@@ -0,0 +1,122 @@
package vertex
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"github.com/bytedance/gopkg/cache/asynccache"
"github.com/golang-jwt/jwt"
"net/http"
"net/url"
relaycommon "one-api/relay/common"
"strings"
"fmt"
"time"
)
type Credentials struct {
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
}
var Cache = asynccache.NewAsyncCache(asynccache.Options{
RefreshDuration: time.Minute * 35,
EnableExpire: true,
ExpireDuration: time.Minute * 30,
Fetcher: func(key string) (interface{}, error) {
return nil, errors.New("not found")
},
})
func getAccessToken(a *Adaptor, info *relaycommon.RelayInfo) (string, error) {
cacheKey := fmt.Sprintf("access-token-%d", info.ChannelId)
val, err := Cache.Get(cacheKey)
if err == nil {
return val.(string), nil
}
signedJWT, err := createSignedJWT(a.AccountCredentials.ClientEmail, a.AccountCredentials.PrivateKey)
if err != nil {
return "", fmt.Errorf("failed to create signed JWT: %w", err)
}
newToken, err := exchangeJwtForAccessToken(signedJWT)
if err != nil {
return "", fmt.Errorf("failed to exchange JWT for access token: %w", err)
}
if err := Cache.SetDefault(cacheKey, newToken); err {
return newToken, nil
}
return newToken, nil
}
func createSignedJWT(email, privateKeyPEM string) (string, error) {
privateKeyPEM = strings.ReplaceAll(privateKeyPEM, "-----BEGIN PRIVATE KEY-----", "")
privateKeyPEM = strings.ReplaceAll(privateKeyPEM, "-----END PRIVATE KEY-----", "")
privateKeyPEM = strings.ReplaceAll(privateKeyPEM, "\r", "")
privateKeyPEM = strings.ReplaceAll(privateKeyPEM, "\n", "")
privateKeyPEM = strings.ReplaceAll(privateKeyPEM, "\\n", "")
block, _ := pem.Decode([]byte("-----BEGIN PRIVATE KEY-----\n" + privateKeyPEM + "\n-----END PRIVATE KEY-----"))
if block == nil {
return "", fmt.Errorf("failed to parse PEM block containing the private key")
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", err
}
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return "", fmt.Errorf("not an RSA private key")
}
now := time.Now()
claims := jwt.MapClaims{
"iss": email,
"scope": "https://www.googleapis.com/auth/cloud-platform",
"aud": "https://www.googleapis.com/oauth2/v4/token",
"exp": now.Add(time.Minute * 35).Unix(),
"iat": now.Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedToken, err := token.SignedString(rsaPrivateKey)
if err != nil {
return "", err
}
return signedToken, nil
}
func exchangeJwtForAccessToken(signedJWT string) (string, error) {
authURL := "https://www.googleapis.com/oauth2/v4/token"
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
data.Set("assertion", signedJWT)
resp, err := http.PostForm(authURL, data)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if accessToken, ok := result["access_token"].(string); ok {
return accessToken, nil
}
return "", fmt.Errorf("failed to get access token: %v", result)
}

View File

@@ -1,7 +1,7 @@
package zhipu_4v package zhipu_4v
var ModelList = []string{ var ModelList = []string{
"glm-4", "glm-4v", "glm-3-turbo", "glm-4-alltools", "glm-4", "glm-4v", "glm-3-turbo", "glm-4-alltools", "glm-4-plus", "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-long", "glm-4-flash", "glm-4v-plus",
} }
var ChannelName = "zhipu_4v" var ChannelName = "zhipu_4v"

View File

@@ -22,6 +22,7 @@ type RelayInfo struct {
IsStream bool IsStream bool
RelayMode int RelayMode int
UpstreamModelName string UpstreamModelName string
OriginModelName string
RequestURLPath string RequestURLPath string
ApiVersion string ApiVersion string
PromptTokens int PromptTokens int
@@ -57,6 +58,8 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
TokenUnlimited: tokenUnlimited, TokenUnlimited: tokenUnlimited,
StartTime: startTime, StartTime: startTime,
FirstResponseTime: startTime.Add(-time.Second), FirstResponseTime: startTime.Add(-time.Second),
OriginModelName: c.GetString("original_model"),
UpstreamModelName: c.GetString("original_model"),
ApiType: apiType, ApiType: apiType,
ApiVersion: c.GetString("api_version"), ApiVersion: c.GetString("api_version"),
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
@@ -68,6 +71,9 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
if info.ChannelType == common.ChannelTypeAzure { if info.ChannelType == common.ChannelTypeAzure {
info.ApiVersion = GetAPIVersion(c) info.ApiVersion = GetAPIVersion(c)
} }
if info.ChannelType == common.ChannelTypeVertexAi {
info.ApiVersion = c.GetString("region")
}
if info.ChannelType == common.ChannelTypeOpenAI || info.ChannelType == common.ChannelTypeAnthropic || if info.ChannelType == common.ChannelTypeOpenAI || info.ChannelType == common.ChannelTypeAnthropic ||
info.ChannelType == common.ChannelTypeAws || info.ChannelType == common.ChannelTypeGemini || info.ChannelType == common.ChannelTypeAws || info.ChannelType == common.ChannelTypeGemini ||
info.ChannelType == common.ChannelCloudflare { info.ChannelType == common.ChannelCloudflare {

View File

@@ -23,6 +23,8 @@ const (
APITypeDify APITypeDify
APITypeJina APITypeJina
APITypeCloudflare APITypeCloudflare
APITypeSiliconFlow
APITypeVertexAi
APITypeDummy // this one is only for count, do not add any channel after this APITypeDummy // this one is only for count, do not add any channel after this
) )
@@ -66,6 +68,10 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = APITypeJina apiType = APITypeJina
case common.ChannelCloudflare: case common.ChannelCloudflare:
apiType = APITypeCloudflare apiType = APITypeCloudflare
case common.ChannelTypeSiliconFlow:
apiType = APITypeSiliconFlow
case common.ChannelTypeVertexAi:
apiType = APITypeVertexAi
} }
if apiType == -1 { if apiType == -1 {
return APITypeOpenAI, false return APITypeOpenAI, false

View File

@@ -75,7 +75,7 @@ func AudioHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
return service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError) return service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError)
} }
if userQuota-preConsumedQuota < 0 { if userQuota-preConsumedQuota < 0 {
return service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden) return service.OpenAIErrorWrapperLocal(errors.New(fmt.Sprintf("audio pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota)), "insufficient_user_quota", http.StatusBadRequest)
} }
err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota) err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil { if err != nil {

View File

@@ -38,9 +38,7 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if imageRequest.Model == "" { if imageRequest.Model == "" {
imageRequest.Model = "dall-e-2" imageRequest.Model = "dall-e-2"
} }
if imageRequest.Quality == "" {
imageRequest.Quality = "standard"
}
// Not "256x256", "512x512", or "1024x1024" // Not "256x256", "512x512", or "1024x1024"
if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" { if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" {
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" { if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
@@ -50,6 +48,9 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" { if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" {
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024") return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024")
} }
if imageRequest.Quality == "" {
imageRequest.Quality = "standard"
}
//if imageRequest.N != 1 { //if imageRequest.N != 1 {
// return nil, errors.New("n must be 1") // return nil, errors.New("n must be 1")
//} //}
@@ -125,7 +126,7 @@ func ImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
quota := int(imageRatio * groupRatio * common.QuotaPerUnit) quota := int(imageRatio * groupRatio * common.QuotaPerUnit)
if userQuota-quota < 0 { if userQuota-quota < 0 {
return service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden) return service.OpenAIErrorWrapperLocal(errors.New(fmt.Sprintf("image pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, quota)), "insufficient_user_quota", http.StatusBadRequest)
} }
adaptor := GetAdaptor(relayInfo.ApiType) adaptor := GetAdaptor(relayInfo.ApiType)

View File

@@ -52,7 +52,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
} }
case relayconstant.RelayModeEmbeddings: case relayconstant.RelayModeEmbeddings:
case relayconstant.RelayModeModerations: case relayconstant.RelayModeModerations:
if textRequest.Input == "" { if textRequest.Input == "" || textRequest.Input == nil {
return nil, errors.New("field input is required") return nil, errors.New("field input is required")
} }
case relayconstant.RelayModeEdits: case relayconstant.RelayModeEdits:
@@ -247,9 +247,12 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
if err != nil { if err != nil {
return 0, 0, service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError) return 0, 0, service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError)
} }
if userQuota <= 0 || userQuota-preConsumedQuota < 0 { if userQuota <= 0 {
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden) return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
} }
if userQuota-preConsumedQuota < 0 {
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New(fmt.Sprintf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota)), "insufficient_user_quota", http.StatusBadRequest)
}
err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota) err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil { if err != nil {
return 0, 0, service.OpenAIErrorWrapperLocal(err, "decrease_user_quota_failed", http.StatusInternalServerError) return 0, 0, service.OpenAIErrorWrapperLocal(err, "decrease_user_quota_failed", http.StatusInternalServerError)
@@ -323,7 +326,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelN
totalTokens := promptTokens + completionTokens totalTokens := promptTokens + completionTokens
var logContent string var logContent string
if !usePrice { if !usePrice {
logContent = fmt.Sprintf("模型倍率 %.2f分组倍率 %.2f补全倍率 %.2f", modelRatio, groupRatio, completionRatio) logContent = fmt.Sprintf("模型倍率 %.2f补全倍率 %.2f分组倍率 %.2f", modelRatio, completionRatio, groupRatio)
} else { } else {
logContent = fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f", modelPrice, groupRatio) logContent = fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f", modelPrice, groupRatio)
} }

View File

@@ -16,8 +16,10 @@ import (
"one-api/relay/channel/openai" "one-api/relay/channel/openai"
"one-api/relay/channel/palm" "one-api/relay/channel/palm"
"one-api/relay/channel/perplexity" "one-api/relay/channel/perplexity"
"one-api/relay/channel/siliconflow"
"one-api/relay/channel/task/suno" "one-api/relay/channel/task/suno"
"one-api/relay/channel/tencent" "one-api/relay/channel/tencent"
"one-api/relay/channel/vertex"
"one-api/relay/channel/xunfei" "one-api/relay/channel/xunfei"
"one-api/relay/channel/zhipu" "one-api/relay/channel/zhipu"
"one-api/relay/channel/zhipu_4v" "one-api/relay/channel/zhipu_4v"
@@ -62,6 +64,10 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &jina.Adaptor{} return &jina.Adaptor{}
case constant.APITypeCloudflare: case constant.APITypeCloudflare:
return &cloudflare.Adaptor{} return &cloudflare.Adaptor{}
case constant.APITypeSiliconFlow:
return &siliconflow.Adaptor{}
case constant.APITypeVertexAi:
return &vertex.Adaptor{}
} }
return nil return nil
} }

View File

@@ -38,6 +38,23 @@ func RerankHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
if len(rerankRequest.Documents) == 0 { if len(rerankRequest.Documents) == 0 {
return service.OpenAIErrorWrapperLocal(fmt.Errorf("documents is empty"), "invalid_documents", http.StatusBadRequest) return service.OpenAIErrorWrapperLocal(fmt.Errorf("documents is empty"), "invalid_documents", http.StatusBadRequest)
} }
// map model name
modelMapping := c.GetString("model_mapping")
//isModelMapped := false
if modelMapping != "" && modelMapping != "{}" {
modelMap := make(map[string]string)
err := json.Unmarshal([]byte(modelMapping), &modelMap)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
}
if modelMap[rerankRequest.Model] != "" {
rerankRequest.Model = modelMap[rerankRequest.Model]
// set upstream model name
//isModelMapped = true
}
}
relayInfo.UpstreamModelName = rerankRequest.Model relayInfo.UpstreamModelName = rerankRequest.Model
modelPrice, success := common.GetModelPrice(rerankRequest.Model, false) modelPrice, success := common.GetModelPrice(rerankRequest.Model, false)
groupRatio := common.GetGroupRatio(relayInfo.Group) groupRatio := common.GetGroupRatio(relayInfo.Group)

View File

@@ -41,6 +41,7 @@ func SetApiRouter(router *gin.Engine) {
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login) userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog) //userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
userRoute.GET("/logout", controller.Logout) userRoute.GET("/logout", controller.Logout)
userRoute.GET("/groups", controller.GetUserGroups)
selfRoute := userRoute.Group("/") selfRoute := userRoute.Group("/")
selfRoute.Use(middleware.UserAuth()) selfRoute.Use(middleware.UserAuth())

View File

@@ -54,6 +54,8 @@ func ShouldDisableChannel(channelType int, err *relaymodel.OpenAIErrorWithStatus
switch err.Error.Type { switch err.Error.Type {
case "insufficient_quota": case "insufficient_quota":
return true return true
case "insufficient_user_quota":
return true
// https://docs.anthropic.com/claude/reference/errors // https://docs.anthropic.com/claude/reference/errors
case "authentication_error": case "authentication_error":
return true return true

View File

@@ -28,13 +28,11 @@ func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int)
// OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode // OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode { func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
text := err.Error() text := err.Error()
// 定义一个正则表达式匹配URL lowerText := strings.ToLower(text)
if strings.Contains(text, "Post") || strings.Contains(text, "dial") { if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
common.SysLog(fmt.Sprintf("error: %s", text)) common.SysLog(fmt.Sprintf("error: %s", text))
text = "请求上游地址失败" text = "请求上游地址失败"
} }
//避免暴露内部错误
openAIError := dto.OpenAIError{ openAIError := dto.OpenAIError{
Message: text, Message: text,
Type: "new_api_error", Type: "new_api_error",
@@ -113,14 +111,12 @@ func TaskErrorWrapperLocal(err error, code string, statusCode int) *dto.TaskErro
func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError { func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError {
text := err.Error() text := err.Error()
lowerText := strings.ToLower(text)
// 定义一个正则表达式匹配URL if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
if strings.Contains(text, "Post") || strings.Contains(text, "dial") {
common.SysLog(fmt.Sprintf("error: %s", text)) common.SysLog(fmt.Sprintf("error: %s", text))
text = "请求上游地址失败" text = "请求上游地址失败"
} }
//避免暴露内部错误 //避免暴露内部错误
taskError := &dto.TaskError{ taskError := &dto.TaskError{
Code: code, Code: code,
Message: text, Message: text,

View File

@@ -40,6 +40,10 @@ func InitTokenEncoders() {
tokenEncoderMap[model] = gpt35TokenEncoder tokenEncoderMap[model] = gpt35TokenEncoder
} else if strings.HasPrefix(model, "gpt-4o") { } else if strings.HasPrefix(model, "gpt-4o") {
tokenEncoderMap[model] = gpt4oTokenEncoder tokenEncoderMap[model] = gpt4oTokenEncoder
} else if strings.HasPrefix(model, "chatgpt-4o") {
tokenEncoderMap[model] = gpt4oTokenEncoder
} else if "o1" == model || strings.HasPrefix(model, "o1") {
tokenEncoderMap[model] = gpt4oTokenEncoder
} else if strings.HasPrefix(model, "gpt-4") { } else if strings.HasPrefix(model, "gpt-4") {
tokenEncoderMap[model] = gpt4TokenEncoder tokenEncoderMap[model] = gpt4TokenEncoder
} else { } else {

View File

@@ -50,7 +50,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@so1ve/prettier-config": "^2.0.0", "@so1ve/prettier-config": "^3.1.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"typescript": "4.4.2", "typescript": "4.4.2",

104
web/pnpm-lock.yaml generated
View File

@@ -12,10 +12,10 @@ importers:
version: 2.53.2(react@18.2.0) version: 2.53.2(react@18.2.0)
'@douyinfe/semi-ui': '@douyinfe/semi-ui':
specifier: ^2.55.3 specifier: ^2.55.3
version: 2.55.3(react-dom@18.2.0)(react@18.2.0) version: 2.55.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@visactor/react-vchart': '@visactor/react-vchart':
specifier: ~1.8.8 specifier: ~1.8.8
version: 1.8.11(react-dom@18.2.0)(react@18.2.0) version: 1.8.11(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@visactor/vchart': '@visactor/vchart':
specifier: ~1.8.8 specifier: ~1.8.8
version: 1.8.11 version: 1.8.11
@@ -48,26 +48,26 @@ importers:
version: 1.0.4 version: 1.0.4
react-router-dom: react-router-dom:
specifier: ^6.3.0 specifier: ^6.3.0
version: 6.22.2(react-dom@18.2.0)(react@18.2.0) version: 6.22.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-telegram-login: react-telegram-login:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2(react@18.2.0) version: 1.1.2(react@18.2.0)
react-toastify: react-toastify:
specifier: ^9.0.8 specifier: ^9.0.8
version: 9.1.3(react-dom@18.2.0)(react@18.2.0) version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-turnstile: react-turnstile:
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.1.3(react-dom@18.2.0)(react@18.2.0) version: 1.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
semantic-ui-offline: semantic-ui-offline:
specifier: ^2.5.0 specifier: ^2.5.0
version: 2.5.0 version: 2.5.0
semantic-ui-react: semantic-ui-react:
specifier: ^2.1.3 specifier: ^2.1.3
version: 2.1.5(react-dom@18.2.0)(react@18.2.0) version: 2.1.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
devDependencies: devDependencies:
'@so1ve/prettier-config': '@so1ve/prettier-config':
specifier: ^2.0.0 specifier: ^3.1.0
version: 2.0.0(prettier@3.2.5) version: 3.1.0(prettier@3.2.5)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1(vite@5.2.5) version: 4.2.1(vite@5.2.5)
@@ -89,10 +89,10 @@ packages:
} }
engines: { node: '>=6.0.0' } engines: { node: '>=6.0.0' }
'@astrojs/compiler@1.8.2': '@astrojs/compiler@2.10.2':
resolution: resolution:
{ {
integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==, integrity: sha512-bvH+v8AirwpRWCkYJEyWYdc5Cs/BjG2ZTxIJzttHilXgfKJAdW2496KsUQKzf5j2tOHtaHXKKn9hb9WZiBGpEg==,
} }
'@babel/code-frame@7.23.5': '@babel/code-frame@7.23.5':
@@ -873,18 +873,18 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
'@so1ve/prettier-config@2.0.0': '@so1ve/prettier-config@3.1.0':
resolution: resolution:
{ {
integrity: sha512-s6qsH5Rf4Bl+J0LU9rKmSWe/rYRdsYw0ELyXhDDDqEaTWtah4NpHKJuVWARuKqj0TWLBeWmyWUoIH/Bkp/DHaw==, integrity: sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg==,
} }
peerDependencies: peerDependencies:
prettier: ^3.0.0 prettier: ^3.0.0
'@so1ve/prettier-plugin-toml@2.0.0': '@so1ve/prettier-plugin-toml@3.1.0':
resolution: resolution:
{ {
integrity: sha512-GvuFdTqhs3qxbhKTiCXWMXITmNLSdndUp7ql1yJbzzWaGqAdb3UH+R+0ZhtAEctBSx90MWAWW3kkW/Iba02tCg==, integrity: sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg==,
} }
peerDependencies: peerDependencies:
prettier: ^3.0.0 prettier: ^3.0.0
@@ -1887,17 +1887,17 @@ packages:
} }
hasBin: true hasBin: true
prettier-plugin-astro@0.13.0: prettier-plugin-astro@0.14.1:
resolution: resolution:
{ {
integrity: sha512-5HrJNnPmZqTUNoA97zn4gNQv9BgVhv+et03314WpQ9H9N8m2L9OSV798olwmG2YLXPl1iSstlJCR1zB3x5xG4g==, integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==,
} }
engines: { node: ^14.15.0 || >=16.0.0 } engines: { node: ^14.15.0 || >=16.0.0 }
prettier-plugin-curly-and-jsdoc@2.0.0: prettier-plugin-curly-and-jsdoc@3.1.0:
resolution: resolution:
{ {
integrity: sha512-uSjWOWmX8+yrCrfhJSI58ODqtX7lXx07M8JYeOC1hfRv+vCttfiDlZoM27mNChGitJNKI+pCBvMMBYh8JiV0HQ==, integrity: sha512-4QMOHnLlkP2jTRWS0MFH6j+cuOiXLvXOqCLKbtwwVd8PPyq8NenW5AAwfwqiTNHBQG/DmzViPphRrwgN0XkUVQ==,
} }
peerDependencies: peerDependencies:
prettier: ^3.0.0 prettier: ^3.0.0
@@ -2417,7 +2417,7 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.5 '@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.24 '@jridgewell/trace-mapping': 0.3.24
'@astrojs/compiler@1.8.2': {} '@astrojs/compiler@2.10.2': {}
'@babel/code-frame@7.23.5': '@babel/code-frame@7.23.5':
dependencies: dependencies:
@@ -2565,7 +2565,7 @@ snapshots:
react: 18.2.0 react: 18.2.0
tslib: 2.6.2 tslib: 2.6.2
'@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0)': '@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@dnd-kit/accessibility': 3.1.0(react@18.2.0) '@dnd-kit/accessibility': 3.1.0(react@18.2.0)
'@dnd-kit/utilities': 3.2.2(react@18.2.0) '@dnd-kit/utilities': 3.2.2(react@18.2.0)
@@ -2573,9 +2573,9 @@ snapshots:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2 tslib: 2.6.2
'@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0)': '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) '@dnd-kit/core': 6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@dnd-kit/utilities': 3.2.2(react@18.2.0) '@dnd-kit/utilities': 3.2.2(react@18.2.0)
react: 18.2.0 react: 18.2.0
tslib: 2.6.2 tslib: 2.6.2
@@ -2627,10 +2627,10 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
'@douyinfe/semi-ui@2.55.3(react-dom@18.2.0)(react@18.2.0)': '@douyinfe/semi-ui@2.55.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) '@dnd-kit/core': 6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0) '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
'@dnd-kit/utilities': 3.2.2(react@18.2.0) '@dnd-kit/utilities': 3.2.2(react@18.2.0)
'@douyinfe/semi-animation': 2.55.3 '@douyinfe/semi-animation': 2.55.3
'@douyinfe/semi-animation-react': 2.55.3 '@douyinfe/semi-animation-react': 2.55.3
@@ -2648,8 +2648,8 @@ snapshots:
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-resizable: 3.0.5(react-dom@18.2.0)(react@18.2.0) react-resizable: 3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-window: 1.8.10(react-dom@18.2.0)(react@18.2.0) react-window: 1.8.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
scroll-into-view-if-needed: 2.2.31 scroll-into-view-if-needed: 2.2.31
utility-types: 3.11.0 utility-types: 3.11.0
@@ -2722,13 +2722,13 @@ snapshots:
'@esbuild/win32-x64@0.20.2': '@esbuild/win32-x64@0.20.2':
optional: true optional: true
'@fluentui/react-component-event-listener@0.63.1(react-dom@18.2.0)(react@18.2.0)': '@fluentui/react-component-event-listener@0.63.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.24.0 '@babel/runtime': 7.24.0
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
'@fluentui/react-component-ref@0.63.1(react-dom@18.2.0)(react@18.2.0)': '@fluentui/react-component-ref@0.63.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.24.0 '@babel/runtime': 7.24.0
react: 18.2.0 react: 18.2.0
@@ -2846,22 +2846,22 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.13.0': '@rollup/rollup-win32-x64-msvc@4.13.0':
optional: true optional: true
'@semantic-ui-react/event-stack@3.1.3(react-dom@18.2.0)(react@18.2.0)': '@semantic-ui-react/event-stack@3.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
exenv: 1.2.2 exenv: 1.2.2
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
'@so1ve/prettier-config@2.0.0(prettier@3.2.5)': '@so1ve/prettier-config@3.1.0(prettier@3.2.5)':
dependencies: dependencies:
'@so1ve/prettier-plugin-toml': 2.0.0(prettier@3.2.5) '@so1ve/prettier-plugin-toml': 3.1.0(prettier@3.2.5)
prettier: 3.2.5 prettier: 3.2.5
prettier-plugin-astro: 0.13.0 prettier-plugin-astro: 0.14.1
prettier-plugin-curly-and-jsdoc: 2.0.0(prettier@3.2.5) prettier-plugin-curly-and-jsdoc: 3.1.0(prettier@3.2.5)
prettier-plugin-pkgsort: 0.2.1(prettier@3.2.5) prettier-plugin-pkgsort: 0.2.1(prettier@3.2.5)
'@so1ve/prettier-plugin-toml@2.0.0(prettier@3.2.5)': '@so1ve/prettier-plugin-toml@3.1.0(prettier@3.2.5)':
dependencies: dependencies:
prettier: 3.2.5 prettier: 3.2.5
@@ -2926,7 +2926,7 @@ snapshots:
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
'@visactor/react-vchart@1.8.11(react-dom@18.2.0)(react@18.2.0)': '@visactor/react-vchart@1.8.11(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies: dependencies:
'@visactor/vchart': 1.8.11 '@visactor/vchart': 1.8.11
'@visactor/vgrammar-core': 0.10.11 '@visactor/vgrammar-core': 0.10.11
@@ -3503,13 +3503,13 @@ snapshots:
sort-object-keys: 1.1.3 sort-object-keys: 1.1.3
sort-order: 1.1.2 sort-order: 1.1.2
prettier-plugin-astro@0.13.0: prettier-plugin-astro@0.14.1:
dependencies: dependencies:
'@astrojs/compiler': 1.8.2 '@astrojs/compiler': 2.10.2
prettier: 3.2.5 prettier: 3.2.5
sass-formatter: 0.7.9 sass-formatter: 0.7.9
prettier-plugin-curly-and-jsdoc@2.0.0(prettier@3.2.5): prettier-plugin-curly-and-jsdoc@3.1.0(prettier@3.2.5):
dependencies: dependencies:
prettier: 3.2.5 prettier: 3.2.5
@@ -3534,7 +3534,7 @@ snapshots:
react: 18.2.0 react: 18.2.0
scheduler: 0.23.0 scheduler: 0.23.0
react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0): react-draggable@4.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
clsx: 1.2.1 clsx: 1.2.1
prop-types: 15.8.1 prop-types: 15.8.1
@@ -3556,7 +3556,7 @@ snapshots:
react-is@18.2.0: {} react-is@18.2.0: {}
react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8
react: 18.2.0 react: 18.2.0
@@ -3566,15 +3566,15 @@ snapshots:
react-refresh@0.14.0: {} react-refresh@0.14.0: {}
react-resizable@3.0.5(react-dom@18.2.0)(react@18.2.0): react-resizable@3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.2.0 react: 18.2.0
react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) react-draggable: 4.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
react-router-dom@6.22.2(react-dom@18.2.0)(react@18.2.0): react-router-dom@6.22.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
'@remix-run/router': 1.15.2 '@remix-run/router': 1.15.2
react: 18.2.0 react: 18.2.0
@@ -3590,18 +3590,18 @@ snapshots:
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): react-toastify@9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
clsx: 1.2.1 clsx: 1.2.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-turnstile@1.1.3(react-dom@18.2.0)(react@18.2.0): react-turnstile@1.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): react-window@1.8.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
'@babel/runtime': 7.24.0 '@babel/runtime': 7.24.0
memoize-one: 5.2.1 memoize-one: 5.2.1
@@ -3683,13 +3683,13 @@ snapshots:
fs-extra: 4.0.3 fs-extra: 4.0.3
jquery: 3.7.1 jquery: 3.7.1
semantic-ui-react@2.1.5(react-dom@18.2.0)(react@18.2.0): semantic-ui-react@2.1.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
'@babel/runtime': 7.24.0 '@babel/runtime': 7.24.0
'@fluentui/react-component-event-listener': 0.63.1(react-dom@18.2.0)(react@18.2.0) '@fluentui/react-component-event-listener': 0.63.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@fluentui/react-component-ref': 0.63.1(react-dom@18.2.0)(react@18.2.0) '@fluentui/react-component-ref': 0.63.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8
'@semantic-ui-react/event-stack': 3.1.3(react-dom@18.2.0)(react@18.2.0) '@semantic-ui-react/event-stack': 3.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
clsx: 1.2.1 clsx: 1.2.1
keyboard-key: 1.1.0 keyboard-key: 1.1.0
lodash: 4.17.21 lodash: 4.17.21
@@ -3698,7 +3698,7 @@ snapshots:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-is: 18.2.0 react-is: 18.2.0
react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
shallowequal: 1.1.0 shallowequal: 1.1.0
semver@6.3.1: {} semver@6.3.1: {}

View File

@@ -21,11 +21,11 @@ import Redemption from './pages/Redemption';
import TopUp from './pages/TopUp'; import TopUp from './pages/TopUp';
import Log from './pages/Log'; import Log from './pages/Log';
import Chat from './pages/Chat'; import Chat from './pages/Chat';
import Chat2Link from './pages/Chat2Link';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import Midjourney from './pages/Midjourney'; import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing/index.js'; import Pricing from './pages/Pricing/index.js';
import Task from './pages/Task/index.js'; import Task from './pages/Task/index.js';
// import Detail from './pages/Detail';
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail')); const Detail = lazy(() => import('./pages/Detail'));
@@ -59,215 +59,224 @@ function App() {
}, []); }, []);
return ( return (
<Layout> <>
<Layout.Content> <Routes>
<Routes> <Route
<Route path='/'
path='/' element={
element={ <Suspense fallback={<Loading></Loading>}>
<Home />
</Suspense>
}
/>
<Route
path='/channel'
element={
<PrivateRoute>
<Channel />
</PrivateRoute>
}
/>
<Route
path='/channel/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<EditChannel />
</Suspense>
}
/>
<Route
path='/channel/add'
element={
<Suspense fallback={<Loading></Loading>}>
<EditChannel />
</Suspense>
}
/>
<Route
path='/token'
element={
<PrivateRoute>
<Token />
</PrivateRoute>
}
/>
<Route
path='/redemption'
element={
<PrivateRoute>
<Redemption />
</PrivateRoute>
}
/>
<Route
path='/user'
element={
<PrivateRoute>
<User />
</PrivateRoute>
}
/>
<Route
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
</Suspense>
}
/>
<Route
path='/user/edit'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
</Suspense>
}
/>
<Route
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm />
</Suspense>
}
/>
<Route
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<LoginForm />
</Suspense>
}
/>
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<RegisterForm />
</Suspense>
}
/>
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetForm />
</Suspense>
}
/>
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<GitHubOAuth />
</Suspense>
}
/>
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<LinuxDoOAuth />
</Suspense>
}
/>
<Route
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<Home /> <Setting />
</Suspense> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/channel' <Route
element={ path='/topup'
<PrivateRoute> element={
<Channel /> <PrivateRoute>
</PrivateRoute>
}
/>
<Route
path='/channel/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditChannel /> <TopUp />
</Suspense> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/channel/add' <Route
element={ path='/log'
element={
<PrivateRoute>
<Log />
</PrivateRoute>
}
/>
<Route
path='/detail'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditChannel /> <Detail />
</Suspense> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/token' <Route
element={ path='/midjourney'
<PrivateRoute> element={
<Token /> <PrivateRoute>
</PrivateRoute>
}
/>
<Route
path='/redemption'
element={
<PrivateRoute>
<Redemption />
</PrivateRoute>
}
/>
<Route
path='/user'
element={
<PrivateRoute>
<User />
</PrivateRoute>
}
/>
<Route
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditUser /> <Midjourney />
</Suspense> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/user/edit' <Route
element={ path='/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditUser /> <Task />
</Suspense> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/user/reset' <Route
element={ path='/pricing'
element={
<Suspense fallback={<Loading></Loading>}>
<Pricing />
</Suspense>
}
/>
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About />
</Suspense>
}
/>
<Route
path='/chat'
element={
<Suspense fallback={<Loading></Loading>}>
<Chat />
</Suspense>
}
/>
{/* 方便使用chat2link直接跳转聊天... */}
<Route
path='/chat2link'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm /> <Chat2Link />
</Suspense> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/login' <Route path='*' element={<NotFound />} />
element={ </Routes>
<Suspense fallback={<Loading></Loading>}> </>
<LoginForm />
</Suspense>
}
/>
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<RegisterForm />
</Suspense>
}
/>
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetForm />
</Suspense>
}
/>
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<GitHubOAuth />
</Suspense>
}
/>
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<LinuxDoOAuth />
</Suspense>
}
/>
<Route
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Setting />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<TopUp />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/log'
element={
<PrivateRoute>
<Log />
</PrivateRoute>
}
/>
<Route
path='/detail'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Detail />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/midjourney'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Midjourney />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Task />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/pricing'
element={
<Suspense fallback={<Loading></Loading>}>
<Pricing />
</Suspense>
}
/>
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About />
</Suspense>
}
/>
<Route
path='/chat'
element={
<Suspense fallback={<Loading></Loading>}>
<Chat />
</Suspense>
}
/>
<Route path='*' element={<NotFound />} />
</Routes>
</Layout.Content>
</Layout>
); );
} }

View File

@@ -749,7 +749,8 @@ const ChannelsTable = () => {
<Form.Select <Form.Select
field='group' field='group'
label='分组' label='分组'
optionList={groupOptions} optionList={[{ label: '选择分组', value: null }, ...groupOptions]}
initValue={null}
onChange={(v) => { onChange={(v) => {
setSearchGroup(v); setSearchGroup(v);
searchChannels(searchKeyword, v, searchModel); searchChannels(searchKeyword, v, searchModel);

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import { getFooterHTML, getSystemName } from '../helpers'; import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui'; import { Layout, Tooltip } from '@douyinfe/semi-ui';
const Footer = () => { const FooterBar = () => {
const systemName = getSystemName(); const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML()); const [footer, setFooter] = useState(getFooterHTML());
let remainCheckTimes = 5; let remainCheckTimes = 5;
@@ -52,21 +52,17 @@ const Footer = () => {
}, []); }, []);
return ( return (
<Layout> <div style={{ textAlign: 'center' }}>
<Layout.Content style={{ textAlign: 'center' }}> {footer ? (
{footer ? ( <div
<Tooltip content={defaultFooter}> className='custom-footer'
<div dangerouslySetInnerHTML={{ __html: footer }}
className='custom-footer' ></div>
dangerouslySetInnerHTML={{ __html: footer }} ) : (
></div> defaultFooter
</Tooltip> )}
) : ( </div>
defaultFooter
)}
</Layout.Content>
</Layout>
); );
}; };
export default Footer; export default FooterBar;

View File

@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { useSetTheme, useTheme } from '../context/Theme'; import { useSetTheme, useTheme } from '../context/Theme';
import { API, getLogo, getSystemName, showSuccess } from '../helpers'; import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
import '../index.css'; import '../index.css';
import fireworks from 'react-fireworks'; import fireworks from 'react-fireworks';
import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons'; import {
IconHelpCircle,
IconHome,
IconHomeStroked,
IconKey,
IconNoteMoneyStroked,
IconPriceTag,
IconUser,
} from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui'; import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render'; import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
// HeaderBar Buttons // HeaderBar Buttons
let headerButtons = [ let headerButtons = [
@@ -22,6 +31,21 @@ let headerButtons = [
}, },
]; ];
let buttons = [
{
text: '首页',
itemKey: 'home',
to: '/',
// icon: <IconHomeStroked />,
},
// {
// text: '模型价格',
// itemKey: 'pricing',
// to: '/pricing',
// icon: <IconNoteMoneyStroked />,
// },
];
if (localStorage.getItem('chat_link')) { if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, { headerButtons.splice(1, 0, {
name: '聊天', name: '聊天',
@@ -90,6 +114,7 @@ const HeaderBar = () => {
about: '/about', about: '/about',
login: '/login', login: '/login',
register: '/register', register: '/register',
home: '/',
}; };
return ( return (
<Link <Link
@@ -103,6 +128,23 @@ const HeaderBar = () => {
selectedKeys={[]} selectedKeys={[]}
// items={headerButtons} // items={headerButtons}
onSelect={(key) => {}} onSelect={(key) => {}}
header={
isMobile()
? {
logo: (
<img
src={logo}
alt='logo'
style={{ marginRight: '0.75em' }}
/>
),
}
: {
logo: <img src={logo} alt='logo' />,
text: systemName,
}
}
items={buttons}
footer={ footer={
<> <>
{isNewYear && ( {isNewYear && (
@@ -121,15 +163,19 @@ const HeaderBar = () => {
</Dropdown> </Dropdown>
)} )}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
<Switch <>
checkedText='🌞' {!isMobile() && (
size={'large'} <Switch
checked={theme === 'dark'} checkedText='🌞'
uncheckedText='🌙' size={'large'}
onChange={(checked) => { checked={theme === 'dark'}
setTheme(checked); uncheckedText='🌙'
}} onChange={(checked) => {
/> setTheme(checked);
}}
/>
)}
</>
{userState.user ? ( {userState.user ? (
<> <>
<Dropdown <Dropdown
@@ -155,7 +201,7 @@ const HeaderBar = () => {
<Nav.Item <Nav.Item
itemKey={'login'} itemKey={'login'}
text={'登录'} text={'登录'}
icon={<IconKey />} // icon={<IconKey />}
/> />
<Nav.Item <Nav.Item
itemKey={'register'} itemKey={'register'}

View File

@@ -415,8 +415,6 @@ const LogsTable = () => {
const [activePage, setActivePage] = useState(1); const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [logType, setLogType] = useState(0); const [logType, setLogType] = useState(0);
const isAdminUser = isAdmin(); const isAdminUser = isAdmin();
let now = new Date(); let now = new Date();
@@ -518,10 +516,7 @@ const LogsTable = () => {
logs[i].timestamp2string = timestamp2string(logs[i].created_at); logs[i].timestamp2string = timestamp2string(logs[i].created_at);
logs[i].key = '' + logs[i].id; logs[i].key = '' + logs[i].id;
} }
// data.key = '' + data.id
setLogs(logs); setLogs(logs);
setLogCount(logs.length + ITEMS_PER_PAGE);
// console.log(logCount);
}; };
const loadLogs = async (startIdx, pageSize, logType = 0) => { const loadLogs = async (startIdx, pageSize, logType = 0) => {
@@ -539,37 +534,28 @@ const LogsTable = () => {
const res = await API.get(url); const res = await API.get(url);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (startIdx === 0) { const newPageData = data.items;
setLogsFormat(data); setActivePage(data.page);
} else { setPageSize(data.page_size);
let newLogs = [...logs]; setLogCount(data.total);
newLogs.splice(startIdx * pageSize, data.length, ...data);
setLogsFormat(newLogs); setLogsFormat(newPageData);
}
} else { } else {
showError(message); showError(message);
} }
setLoading(false); setLoading(false);
}; };
const pageData = logs.slice(
(activePage - 1) * pageSize,
activePage * pageSize,
);
const handlePageChange = (page) => { const handlePageChange = (page) => {
setActivePage(page); setActivePage(page);
if (page === Math.ceil(logs.length / pageSize) + 1) { loadLogs(page, pageSize, logType).then((r) => {});
// In this case we have to load more data and then append them.
loadLogs(page - 1, pageSize, logType).then((r) => {});
}
}; };
const handlePageSizeChange = async (size) => { const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + ''); localStorage.setItem('page-size', size + '');
setPageSize(size); setPageSize(size);
setActivePage(1); setActivePage(1);
loadLogs(0, size) loadLogs(activePage, size)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
@@ -577,27 +563,24 @@ const LogsTable = () => {
}; };
const refresh = async () => { const refresh = async () => {
// setLoading(true);
setActivePage(1); setActivePage(1);
handleEyeClick(); handleEyeClick();
await loadLogs(0, pageSize, logType); await loadLogs(activePage, pageSize, logType);
}; };
const copyText = async (text) => { const copyText = async (text) => {
if (await copy(text)) { if (await copy(text)) {
showSuccess('已复制:' + text); showSuccess('已复制:' + text);
} else { } else {
// setSearchKeyword(text);
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
} }
}; };
useEffect(() => { useEffect(() => {
// console.log('default effect')
const localPageSize = const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize); setPageSize(localPageSize);
loadLogs(0, localPageSize) loadLogs(activePage, localPageSize)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
@@ -605,25 +588,6 @@ const LogsTable = () => {
handleEyeClick(); handleEyeClick();
}, []); }, []);
const searchLogs = async () => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
await loadLogs(0, pageSize);
setActivePage(1);
return;
}
setSearching(true);
const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
const { success, message, data } = res.data;
if (success) {
setLogs(data);
setActivePage(1);
} else {
showError(message);
}
setSearching(false);
};
return ( return (
<> <>
<Layout> <Layout>
@@ -722,7 +686,7 @@ const LogsTable = () => {
<Table <Table
style={{ marginTop: 5 }} style={{ marginTop: 5 }}
columns={columns} columns={columns}
dataSource={pageData} dataSource={logs}
pagination={{ pagination={{
currentPage: activePage, currentPage: activePage,
pageSize: pageSize, pageSize: pageSize,
@@ -730,7 +694,7 @@ const LogsTable = () => {
pageSizeOpts: [10, 20, 50, 100], pageSizeOpts: [10, 20, 50, 100],
showSizeChanger: true, showSizeChanger: true,
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
handlePageSizeChange(size).then(); handlePageSizeChange(size);
}, },
onPageChange: handlePageChange, onPageChange: handlePageChange,
}} }}

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react'; import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
import { API, copy, showError, showSuccess } from '../helpers'; import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import { import {
Banner, Banner,
@@ -83,6 +83,7 @@ const ModelPricing = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [modalImageUrl, setModalImageUrl] = useState(''); const [modalImageUrl, setModalImageUrl] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false); const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [selectedGroup, setSelectedGroup] = useState('default');
const rowSelection = useMemo( const rowSelection = useMemo(
() => ({ () => ({
@@ -116,7 +117,8 @@ const ModelPricing = () => {
title: '可用性', title: '可用性',
dataIndex: 'available', dataIndex: 'available',
render: (text, record, index) => { render: (text, record, index) => {
return renderAvailable(text); // if record.enable_groups contains selectedGroup, then available is true
return renderAvailable(record.enable_groups.includes(selectedGroup));
}, },
sorter: (a, b) => a.available - b.available, sorter: (a, b) => a.available - b.available,
}, },
@@ -162,6 +164,44 @@ const ModelPricing = () => {
}, },
sorter: (a, b) => a.quota_type - b.quota_type, sorter: (a, b) => a.quota_type - b.quota_type,
}, },
{
title: '可用分组',
dataIndex: 'enable_groups',
render: (text, record, index) => {
// enable_groups is a string array
return (
<Space>
{text.map((group) => {
if (group === selectedGroup) {
return (
<Tag color='blue' size='large' prefixIcon={<IconVerify />}>
{group}
</Tag>
);
} else {
return (
<Tag
color='blue'
size='large'
onClick={() => {
setSelectedGroup(group);
showInfo(
'当前查看的分组为:' +
group +
',倍率为:' +
groupRatio[group],
);
}}
>
{group}
</Tag>
);
}
})}
</Space>
);
},
},
{ {
title: () => ( title: () => (
<span style={{ display: 'flex', alignItems: 'center' }}> <span style={{ display: 'flex', alignItems: 'center' }}>
@@ -203,6 +243,8 @@ const ModelPricing = () => {
<Text> <Text>
补全{record.quota_type === 0 ? completionRatio : '无'} 补全{record.quota_type === 0 ? completionRatio : '无'}
</Text> </Text>
<br />
<Text>分组{groupRatio[selectedGroup]}</Text>
</> </>
); );
return <div>{content}</div>; return <div>{content}</div>;
@@ -215,12 +257,13 @@ const ModelPricing = () => {
let content = text; let content = text;
if (record.quota_type === 0) { if (record.quota_type === 0) {
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除 // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = record.model_ratio * 2 * record.group_ratio; let inputRatioPrice =
record.model_ratio * 2 * groupRatio[selectedGroup];
let completionRatioPrice = let completionRatioPrice =
record.model_ratio * record.model_ratio *
record.completion_ratio * record.completion_ratio *
2 * 2 *
record.group_ratio; groupRatio[selectedGroup];
content = ( content = (
<> <>
<Text>提示 ${inputRatioPrice} / 1M tokens</Text> <Text>提示 ${inputRatioPrice} / 1M tokens</Text>
@@ -229,7 +272,7 @@ const ModelPricing = () => {
</> </>
); );
} else { } else {
let price = parseFloat(text) * record.group_ratio; let price = parseFloat(text) * groupRatio[selectedGroup];
content = <>模型价格${price}</>; content = <>模型价格${price}</>;
} }
return <div>{content}</div>; return <div>{content}</div>;
@@ -240,12 +283,12 @@ const ModelPricing = () => {
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [groupRatio, setGroupRatio] = useState(1); const [groupRatio, setGroupRatio] = useState({});
const setModelsFormat = (models, groupRatio) => { const setModelsFormat = (models, groupRatio) => {
for (let i = 0; i < models.length; i++) { for (let i = 0; i < models.length; i++) {
models[i].key = models[i].model_name; models[i].key = models[i].model_name;
models[i].group_ratio = groupRatio; models[i].group_ratio = groupRatio[models[i].model_name];
} }
// sort by quota_type // sort by quota_type
models.sort((a, b) => { models.sort((a, b) => {
@@ -278,6 +321,7 @@ const ModelPricing = () => {
const { success, message, data, group_ratio } = res.data; const { success, message, data, group_ratio } = res.data;
if (success) { if (success) {
setGroupRatio(group_ratio); setGroupRatio(group_ratio);
setSelectedGroup(userState.user ? userState.user.group : 'default');
setModelsFormat(data, group_ratio); setModelsFormat(data, group_ratio);
} else { } else {
showError(message); showError(message);
@@ -310,14 +354,14 @@ const ModelPricing = () => {
type='success' type='success'
fullMode={false} fullMode={false}
closeIcon='null' closeIcon='null'
description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`} description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
/> />
) : ( ) : (
<Banner <Banner
type='warning' type='warning'
fullMode={false} fullMode={false}
closeIcon='null' closeIcon='null'
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`} description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
/> />
)} )}
<br /> <br />

View File

@@ -23,6 +23,7 @@ const OperationSetting = () => {
CompletionRatio: '', CompletionRatio: '',
ModelPrice: '', ModelPrice: '',
GroupRatio: '', GroupRatio: '',
UserUsableGroups: '',
TopUpLink: '', TopUpLink: '',
ChatLink: '', ChatLink: '',
ChatLink2: '', // 添加的新状态变量 ChatLink2: '', // 添加的新状态变量
@@ -62,6 +63,7 @@ const OperationSetting = () => {
if ( if (
item.key === 'ModelRatio' || item.key === 'ModelRatio' ||
item.key === 'GroupRatio' || item.key === 'GroupRatio' ||
item.key === 'UserUsableGroups' ||
item.key === 'CompletionRatio' || item.key === 'CompletionRatio' ||
item.key === 'ModelPrice' item.key === 'ModelPrice'
) { ) {

View File

@@ -19,6 +19,7 @@ import {
IconComment, IconComment,
IconCreditCard, IconCreditCard,
IconGift, IconGift,
IconHelpCircle,
IconHistogram, IconHistogram,
IconHome, IconHome,
IconImage, IconImage,
@@ -28,8 +29,10 @@ import {
IconSetting, IconSetting,
IconUser, IconUser,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { Layout, Nav } from '@douyinfe/semi-ui'; import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { setStatusData } from '../helpers/data.js'; import { setStatusData } from '../helpers/data.js';
import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js';
// HeaderBar Buttons // HeaderBar Buttons
@@ -44,6 +47,8 @@ const SiderBar = () => {
const systemName = getSystemName(); const systemName = getSystemName();
const logo = getLogo(); const logo = getLogo();
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed); const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
const theme = useTheme();
const setTheme = useSetTheme();
const routerMap = { const routerMap = {
home: '/', home: '/',
@@ -64,11 +69,17 @@ const SiderBar = () => {
const headerButtons = useMemo( const headerButtons = useMemo(
() => [ () => [
// {
// text: '首页',
// itemKey: 'home',
// to: '/',
// icon: <IconHome />,
// },
{ {
text: '首页', text: '模型价格',
itemKey: 'home', itemKey: 'pricing',
to: '/', to: '/pricing',
icon: <IconHome />, icon: <IconPriceTag />,
}, },
{ {
text: '渠道', text: '渠道',
@@ -105,12 +116,6 @@ const SiderBar = () => {
to: '/topup', to: '/topup',
icon: <IconCreditCard />, icon: <IconCreditCard />,
}, },
{
text: '模型价格',
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag />,
},
{ {
text: '用户管理', text: '用户管理',
itemKey: 'user', itemKey: 'user',
@@ -206,48 +211,58 @@ const SiderBar = () => {
return ( return (
<> <>
<Layout> <Nav
<div style={{ height: '100%' }}> style={{ maxWidth: 220, height: '100%' }}
<Nav defaultIsCollapsed={
// bodyStyle={{ maxWidth: 200 }} isMobile() ||
style={{ maxWidth: 200 }} localStorage.getItem('default_collapse_sidebar') === 'true'
defaultIsCollapsed={ }
isMobile() || isCollapsed={isCollapsed}
localStorage.getItem('default_collapse_sidebar') === 'true' onCollapseChange={(collapsed) => {
} setIsCollapsed(collapsed);
isCollapsed={isCollapsed} }}
onCollapseChange={(collapsed) => { selectedKeys={selectedKeys}
setIsCollapsed(collapsed); renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
}} return (
selectedKeys={selectedKeys} <Link
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => { style={{ textDecoration: 'none' }}
return ( to={routerMap[props.itemKey]}
<Link >
style={{ textDecoration: 'none' }} {itemElement}
to={routerMap[props.itemKey]} </Link>
> );
{itemElement} }}
</Link> items={headerButtons}
); onSelect={(key) => {
}} setSelectedKeys([key.itemKey]);
items={headerButtons} }}
onSelect={(key) => { // header={{
setSelectedKeys([key.itemKey]); // logo: (
}} // <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
header={{ // ),
logo: ( // text: systemName,
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} /> // }}
), // footer={{
text: systemName, // text: '© 2021 NekoAPI',
}} // }}
// footer={{ footer={
// text: '© 2021 NekoAPI', <>
// }} {isMobile() && (
> <Switch
<Nav.Footer collapseButton={true}></Nav.Footer> checkedText='🌞'
</Nav> size={'small'}
</div> checked={theme === 'dark'}
</Layout> uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
</>
}
>
<Nav.Footer collapseButton={true}></Nav.Footer>
</Nav>
</> </>
); );
}; };

View File

@@ -8,7 +8,7 @@ import {
} from '../helpers'; } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; import { renderGroup, renderQuota } from '../helpers/render';
import { import {
Button, Button,
Dropdown, Dropdown,
@@ -16,6 +16,7 @@ import {
Modal, Modal,
Popconfirm, Popconfirm,
Popover, Popover,
Space,
SplitButtonGroup, SplitButtonGroup,
Table, Table,
Tag, Tag,
@@ -119,7 +120,14 @@ const TokensTable = () => {
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderStatus(text, record.model_limits_enabled)}</div>; return (
<div>
<Space>
{renderStatus(text, record.model_limits_enabled)}
{renderGroup(record.group)}
</Space>
</div>
);
}, },
}, },
{ {
@@ -225,14 +233,14 @@ const TokensTable = () => {
onOpenLink('next-mj', record.key); onOpenLink('next-mj', record.key);
}, },
}, },
{ // {
node: 'item', // node: 'item',
key: 'lobe', // key: 'lobe',
name: 'Lobe Chat', // name: 'Lobe Chat',
onClick: () => { // onClick: () => {
onOpenLink('lobe', record.key); // onOpenLink('lobe', record.key);
}, // },
}, // },
{ {
node: 'item', node: 'item',
key: 'ama', key: 'ama',
@@ -425,7 +433,7 @@ const TokensTable = () => {
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
break; break;
case 'lobe': case 'lobe':
url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}"}}}`; url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
break; break;
case 'next-mj': case 'next-mj':
url = url =

View File

@@ -0,0 +1,70 @@
// src/hooks/useTokenKeys.js
import { useEffect, useState } from 'react';
import { API, showError } from '../helpers';
async function fetchTokenKeys() {
try {
const response = await API.get('/api/token/?p=0&size=999');
const { success, data } = response.data;
if (success) {
const activeTokens = data.filter((token) => token.status === 1);
return activeTokens.map((token) => token.key);
} else {
throw new Error('Failed to fetch token keys');
}
} catch (error) {
console.error('Error fetching token keys:', error);
return [];
}
}
function getServerAddress() {
let status = localStorage.getItem('status');
let serverAddress = '';
if (status) {
try {
status = JSON.parse(status);
serverAddress = status.server_address || '';
} catch (error) {
console.error('Failed to parse status from localStorage:', error);
}
}
if (!serverAddress) {
serverAddress = window.location.origin;
}
return serverAddress;
}
export function useTokenKeys() {
const [keys, setKeys] = useState([]);
const [chatLink, setChatLink] = useState('');
const [serverAddress, setServerAddress] = useState('');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadAllData = async () => {
const fetchedKeys = await fetchTokenKeys();
if (fetchedKeys.length === 0) {
showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
setTimeout(() => {
window.location.href = '/token';
}, 1500); // 延迟 1.5 秒后跳转
}
setKeys(fetchedKeys);
setIsLoading(false);
const link = localStorage.getItem('chat_link');
setChatLink(link);
const address = getServerAddress();
setServerAddress(address);
};
loadAllData();
}, []);
return { keys, chatLink, serverAddress, isLoading };
}

View File

@@ -36,6 +36,7 @@ export const CHANNEL_OPTIONS = [
color: 'indigo', color: 'indigo',
label: 'AWS Claude', label: 'AWS Claude',
}, },
{ key: 41, text: 'Vertex AI', value: 41, color: 'blue', label: 'Vertex AI' },
{ {
key: 3, key: 3,
text: 'Azure OpenAI', text: 'Azure OpenAI',
@@ -113,6 +114,13 @@ export const CHANNEL_OPTIONS = [
{ key: 35, text: 'MiniMax', value: 35, color: 'green', label: 'MiniMax' }, { key: 35, text: 'MiniMax', value: 35, color: 'green', label: 'MiniMax' },
{ key: 37, text: 'Dify', value: 37, color: 'teal', label: 'Dify' }, { key: 37, text: 'Dify', value: 37, color: 'teal', label: 'Dify' },
{ key: 38, text: 'Jina', value: 38, color: 'blue', label: 'Jina' }, { key: 38, text: 'Jina', value: 38, color: 'blue', label: 'Jina' },
{
key: 40,
text: 'SiliconCloud',
value: 40,
color: 'purple',
label: 'SiliconCloud',
},
{ key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
{ {
key: 22, key: 22,

View File

@@ -15,8 +15,8 @@ export function renderText(text, limit) {
export function renderGroup(group) { export function renderGroup(group) {
if (group === '') { if (group === '') {
return ( return (
<Tag size='large' key='default'> <Tag size='large' key='default' color='orange'>
unknown 用户分组
</Tag> </Tag>
); );
} }

View File

@@ -9,11 +9,12 @@ body {
scrollbar-width: none; scrollbar-width: none;
color: var(--semi-color-text-0) !important; color: var(--semi-color-text-0) !important;
background-color: var(--semi-color-bg-0) !important; background-color: var(--semi-color-bg-0) !important;
height: 100%; height: 100vh;
} }
#root { #root {
height: 100%; height: 100vh;
flex-direction: column;
} }
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
@@ -50,9 +51,9 @@ body {
} }
} }
.semi-layout { /*.semi-layout {*/
height: 100%; /* height: 100%;*/
} /*}*/
.tableShow { .tableShow {
display: revert; display: revert;

View File

@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import HeaderBar from './components/HeaderBar'; import HeaderBar from './components/HeaderBar';
import Footer from './components/Footer';
import 'semantic-ui-offline/semantic.min.css'; import 'semantic-ui-offline/semantic.min.css';
import './index.css'; import './index.css';
import { UserProvider } from './context/User'; import { UserProvider } from './context/User';
@@ -13,35 +12,40 @@ import { StatusProvider } from './context/Status';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './components/SiderBar'; import SiderBar from './components/SiderBar';
import { ThemeProvider } from './context/Theme'; import { ThemeProvider } from './context/Theme';
import FooterBar from './components/Footer';
// initialization // initialization
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
const { Sider, Content, Header } = Layout; const { Sider, Content, Header, Footer } = Layout;
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<StatusProvider> <StatusProvider>
<UserProvider> <UserProvider>
<BrowserRouter> <BrowserRouter>
<ThemeProvider> <ThemeProvider>
<Layout> <Layout
<Sider> style={{
<SiderBar /> height: '100vh',
</Sider> display: 'flex',
<Layout> flexDirection: 'column',
<Header> }}
<HeaderBar /> >
</Header> <Header>
<Content <HeaderBar />
style={{ </Header>
padding: '24px', <Layout style={{ flex: 1, overflow: 'hidden' }}>
}} <Sider>
> <SiderBar />
<App /> </Sider>
</Content> <Layout>
<Layout.Footer> <Content style={{ overflowY: 'auto', padding: '24px' }}>
<Footer></Footer> <App />
</Layout.Footer> </Content>
<Layout.Footer>
<FooterBar></FooterBar>
</Layout.Footer>
</Layout>
</Layout> </Layout>
<ToastContainer /> <ToastContainer />
</Layout> </Layout>

View File

@@ -37,6 +37,11 @@ const STATUS_CODE_MAPPING_EXAMPLE = {
400: '500', 400: '500',
}; };
const REGION_EXAMPLE = {
default: 'us-central1',
'claude-3-5-sonnet-20240620': 'europe-west1',
};
const fetchButtonTips = const fetchButtonTips =
'1. 新建渠道时请求通过当前浏览器发出2. 编辑已有渠道,请求通过后端服务器发出'; '1. 新建渠道时请求通过当前浏览器发出2. 编辑已有渠道,请求通过后端服务器发出';
@@ -584,6 +589,44 @@ const EditChannel = (props) => {
/> />
</> </>
)} )}
{inputs.type === 41 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>部署地区</Typography.Text>
</div>
<TextArea
name='other'
placeholder={
'请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
'{\n' +
' "default": "us-central1",\n' +
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
'}'
}
autosize={{ minRows: 2 }}
onChange={(value) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
/>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
}}
onClick={() => {
handleInputChange(
'other',
JSON.stringify(REGION_EXAMPLE, null, 2),
);
}}
>
填入模板
</Typography.Text>
</>
)}
{inputs.type === 21 && ( {inputs.type === 21 && (
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
@@ -730,17 +773,48 @@ const EditChannel = (props) => {
autoComplete='new-password' autoComplete='new-password'
/> />
) : ( ) : (
<Input <>
label='密钥' {inputs.type === 41 ? (
name='key' <TextArea
required label='鉴权json'
placeholder={type2secretPrompt(inputs.type)} name='key'
onChange={(value) => { required
handleInputChange('key', value); placeholder={
}} '{\n' +
value={inputs.key} ' "type": "service_account",\n' +
autoComplete='new-password' ' "project_id": "abc-bcd-123-456",\n' +
/> ' "private_key_id": "123xxxxx456",\n' +
' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
' "client_email": "xxx@developer.gserviceaccount.com",\n' +
' "client_id": "111222333",\n' +
' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
' "token_uri": "https://oauth2.googleapis.com/token",\n' +
' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
' "universe_domain": "googleapis.com"\n' +
'}'
}
onChange={(value) => {
handleInputChange('key', value);
}}
autosize={{ minRows: 10 }}
value={inputs.key}
autoComplete='new-password'
/>
) : (
<Input
label='密钥'
name='key'
required
placeholder={type2secretPrompt(inputs.type)}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
autoComplete='new-password'
/>
)}
</>
)} )}
{inputs.type === 1 && ( {inputs.type === 1 && (
<> <>

View File

@@ -1,14 +1,36 @@
import React from 'react'; import React from 'react';
import { useTokenKeys } from '../../components/fetchTokenKeys';
import { Layout } from '@douyinfe/semi-ui';
const Chat = () => { const ChatPage = () => {
const chatLink = localStorage.getItem('chat_link'); const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
return ( const comLink = (key) => {
if (!chatLink || !serverAddress || !key) return '';
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
};
const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
return !isLoading && iframeSrc ? (
<iframe <iframe
src={chatLink} src={iframeSrc}
style={{ width: '100%', height: '85vh', border: 'none' }} style={{ width: '100%', height: '85vh', border: 'none' }}
title='Token Frame'
/> />
) : (
<div>
<Layout>
<Layout.Header>
<h3 style={{ color: 'red' }}>
当前没有可用的已启用令牌请确认是否有令牌处于启用状态
<br />
正在跳转......
</h3>
</Layout.Header>
</Layout>
</div>
); );
}; };
export default Chat; export default ChatPage;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useTokenKeys } from '../../components/fetchTokenKeys';
const chat2page = () => {
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
const comLink = (key) => {
if (!chatLink || !serverAddress || !key) return '';
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
};
if (keys.length > 0) {
const redirectLink = comLink(keys[0]);
if (redirectLink) {
window.location.href = redirectLink;
}
}
return (
<div>
<h3>正在加载请稍候...</h3>
</div>
);
};
export default chat2page;

View File

@@ -25,6 +25,7 @@ export default function SettingsMagnification(props) {
ModelRatio: '', ModelRatio: '',
CompletionRatio: '', CompletionRatio: '',
GroupRatio: '', GroupRatio: '',
UserUsableGroups: '',
}); });
const refForm = useRef(); const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs); const [inputsRow, setInputsRow] = useState(inputs);
@@ -224,6 +225,33 @@ export default function SettingsMagnification(props) {
/> />
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'用户可选分组'}
extraText={''}
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
field={'UserUsableGroups'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({
...inputs,
UserUsableGroups: value,
})
}
/>
</Col>
</Row>
</Form.Section> </Form.Section>
</Form> </Form>
<Space> <Space>

View File

@@ -19,6 +19,7 @@ import {
SideSheet, SideSheet,
Space, Space,
Spin, Spin,
TextArea,
Typography, Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Title from '@douyinfe/semi-ui/lib/es/typography/title';
@@ -34,6 +35,8 @@ const EditToken = (props) => {
unlimited_quota: false, unlimited_quota: false,
model_limits_enabled: false, model_limits_enabled: false,
model_limits: [], model_limits: [],
allow_ips: '',
group: '',
}; };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const { const {
@@ -43,9 +46,12 @@ const EditToken = (props) => {
unlimited_quota, unlimited_quota,
model_limits_enabled, model_limits_enabled,
model_limits, model_limits,
allow_ips,
group,
} = inputs; } = inputs;
// const [visible, setVisible] = useState(false); // const [visible, setVisible] = useState(false);
const [models, setModels] = useState({}); const [models, setModels] = useState({});
const [groups, setGroups] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -86,6 +92,22 @@ const EditToken = (props) => {
} }
}; };
const loadGroups = async () => {
let res = await API.get(`/api/user/groups`);
const { success, message, data } = res.data;
if (success) {
// return data is a map, key is group name, value is group description
// label is group description, value is group name
let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group],
value: group,
}));
setGroups(localGroupOptions);
} else {
showError(message);
}
};
const loadToken = async () => { const loadToken = async () => {
setLoading(true); setLoading(true);
let res = await API.get(`/api/token/${props.editingToken.id}`); let res = await API.get(`/api/token/${props.editingToken.id}`);
@@ -118,6 +140,7 @@ const EditToken = (props) => {
}); });
} }
loadModels(); loadModels();
loadGroups();
}, [isEdit]); }, [isEdit]);
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1 // 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
@@ -374,6 +397,19 @@ const EditToken = (props) => {
</Button> </Button>
</div> </div>
<Divider /> <Divider />
<div style={{ marginTop: 10 }}>
<Typography.Text>IP白名单请勿过度信任此功能</Typography.Text>
</div>
<TextArea
label='IP白名单'
name='allow_ips'
placeholder={'允许的IP一行一个'}
onChange={(value) => {
handleInputChange('allow_ips', value);
}}
value={inputs.allow_ips}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
/>
<div style={{ marginTop: 10, display: 'flex' }}> <div style={{ marginTop: 10, display: 'flex' }}>
<Space> <Space>
<Checkbox <Checkbox
@@ -404,6 +440,31 @@ const EditToken = (props) => {
optionList={models} optionList={models}
disabled={!model_limits_enabled} disabled={!model_limits_enabled}
/> />
<div style={{ marginTop: 10 }}>
<Typography.Text>令牌分组默认为用户的分组</Typography.Text>
</div>
{groups.length > 0 ? (
<Select
style={{ marginTop: 8 }}
placeholder={'令牌分组,默认为用户的分组'}
name='gruop'
required
selection
onChange={(value) => {
handleInputChange('group', value);
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups}
/>
) : (
<Select
style={{ marginTop: 8 }}
placeholder={'管理员未设置用户可选分组'}
name='gruop'
disabled={true}
/>
)}
</Spin> </Spin>
</SideSheet> </SideSheet>
</> </>