Compare commits

...

61 Commits

Author SHA1 Message Date
1808837298@qq.com
f07ae8139b fix: log page error 2024-05-22 01:20:48 +08:00
1808837298@qq.com
d860289601 chore: 添加注释 2024-05-21 21:16:17 +08:00
1808837298@qq.com
cf8fe63fb6 fix: 模型价格 2024-05-21 21:12:38 +08:00
1808837298@qq.com
1568d6481a fix: 模型价格 2024-05-21 21:07:32 +08:00
1808837298@qq.com
d05a786b4c chore: 删除无用代码 2024-05-21 20:50:48 +08:00
1808837298@qq.com
01160658a5 chore: 删除无用代码 2024-05-21 20:01:32 +08:00
Calcium-Ion
f421699e1b Merge pull request #266 from Calcium-Ion/custom-channel
feat: 自定义渠道功能变更
2024-05-21 19:57:51 +08:00
Calcium-Ion
f0c884cb55 Merge pull request #272 from hepeichun/main
fix: 删除显示模型倍率都乘两倍的问题
2024-05-21 19:57:31 +08:00
1808837298@qq.com
51e0754ade fix: log page error (close #270) 2024-05-21 19:57:50 +08:00
hepeichun
1ab93717bb fix:删除显示模型倍率都乘两倍的问题 2024-05-21 18:14:23 +08:00
CaIon
d6c1e3f37c feat: update SettingsMagnification 2024-05-18 23:04:55 +08:00
CaIon
774ce7195c feat: update model ratio 2024-05-18 18:32:10 +08:00
CaIon
dbaa9390d3 feat: update model ratio 2024-05-18 17:51:53 +08:00
CaIon
84da88506f feat: 自定义渠道功能变更 (#262) 2024-05-18 16:06:12 +08:00
CaIon
98a991306d chore: update minimax url 2024-05-18 15:15:20 +08:00
CaIon
a3de309175 chore: token counter 2024-05-18 15:14:49 +08:00
Calcium-Ion
de81eba90b Merge pull request #265 from jimmyshjj/original
Update Perplexity and 01AI models
2024-05-18 13:54:33 +08:00
Jiayun Shen
ea0c99ac1b Update Perplexity and 01 models
更新Perplexity和01万物模型,增加相关模型价格。对于模型价格,从one-api引入了 价格 * 系数 的方式,目前仅对新模型使用了新方式,待进一步测试。
2024-05-17 19:37:18 +08:00
CaIon
095121673d chore: update model list 2024-05-16 19:08:37 +08:00
CaIon
039fda91f2 feat: support minimax 2024-05-16 19:06:35 +08:00
CaIon
e0df8bbbda feat: support minimax 2024-05-16 19:03:42 +08:00
CaIon
5e07ff85eb feat: pre to delete custom channel type 2024-05-16 18:31:03 +08:00
CaIon
71dcf43c71 feat: 日志显示重试信息 2024-05-16 16:41:08 +08:00
CaIon
7003a4ed94 fix: try to fix sqlite database migration (#231) 2024-05-16 16:10:25 +08:00
Calcium-Ion
e3b885b7f3 Merge pull request #257 from p3psi-boo/main
修复渠道测试时,没有走模型映射
2024-05-16 15:55:13 +08:00
Calcium-Ion
55962acf7c Merge pull request #259 from jimmyshjj/original
Add Baidu Default Behavior and Updating Baidu&360 Models & Prices
2024-05-16 15:54:31 +08:00
Akarin
d33b802dac Squashed commit of the following:
commit 5a6a0df45dee3dfbf2f65591a79fe5f2b74a49e6
Author: Akarin <jimmyshjj@gmail.com>
Date:   Thu May 16 14:05:28 2024 +0800

    Revert "Update docker-image-amd64.yml"

    This reverts commit 581343a78783bbd779e65b476e125af0e2b64ce5.

commit a0aec1bd030da2c6b25d9541199d598f16813a60
Merge: 5b46c7d 58abb38
Author: Jiayun Shen <jimmyshjj@gmail.com>
Date:   Thu May 16 06:46:51 2024 +0800

    Merge branch 'main' of https://github.com/jimmyshjj/new-api

commit 58abb3864a89294d82f812cda9fe49ccf7e2dd91
Merge: 7d2c026 93858c3
Author: Akarin <jimmyshjj@gmail.com>
Date:   Thu May 16 06:46:00 2024 +0800

    Merge branch 'Calcium-Ion:main' into main

commit 5b46c7dd8e6132d2be3b59c7b2ed6a4b84b93cef
Author: Jiayun Shen <jimmyshjj@gmail.com>
Date:   Thu May 16 06:45:00 2024 +0800

    Update constants.go

    Remove replaced Baidu models

commit 7d2c02679cd90b8b53f4145f83969b980a8c2095
Author: Akarin <jimmyshjj@gmail.com>
Date:   Wed May 15 23:40:50 2024 +0800

    Update adaptor.go - Normalize model name to lowercase

    Baidu's official model names may include mixed case letters, but their model APIs are case-sensitive and accept only lowercase. To ensure compatibility, the default behavior has been updated to convert model names to lowercase before constructing API requests.

commit 6bc168a39d9a6194d66f2f32b175e56de9295b2e
Merge: bb9fecd 910e76a
Author: Jiayun Shen <jimmyshjj@gmail.com>
Date:   Wed May 15 21:51:52 2024 +0800

    Merge branch 'main' of https://github.com/jimmyshjj/new-api

commit 910e76ac94d7f5dca6254abb4d0669cbb762e724
Merge: 581343a ff044de
Author: Akarin <jimmyshjj@gmail.com>
Date:   Wed May 15 21:51:13 2024 +0800

    Merge branch 'Calcium-Ion:main' into main

commit bb9fecd5bf2bd9f1859a4017e7e68f80bdb6685a
Author: Jiayun Shen <jimmyshjj@gmail.com>
Date:   Wed May 15 21:50:08 2024 +0800

    update Baidu and 360 models

    Add Baidu and 360 new models. Add Baidu completion ratio

commit 581343a78783bbd779e65b476e125af0e2b64ce5
Author: Akarin <jimmyshjj@gmail.com>
Date:   Wed May 15 19:41:34 2024 +0800

    Update docker-image-amd64.yml

commit de17e2d95eec80f1eeae66e82dec4e9601cdee43
Merge: 046f653 a3b3e6c
Author: Akarin <jimmyshjj@gmail.com>
Date:   Wed May 15 19:22:09 2024 +0800

    Merge branch 'Calcium-Ion:main' into main

commit 046f6537913ae8ad8ecf21019b64c0379331b3fd
Merge: 4164d51 7b58305
Author: Akarin <jimmyshjj@gmail.com>
Date:   Wed May 15 15:32:38 2024 +0800

    Merge branch 'Calcium-Ion:main' into main

commit 4164d51207808283a18ca2728241fd5cddc4855f
Merge: ef35b07 c222bc8
Author: Akarin <jimmyshjj@gmail.com>
Date:   Wed May 15 11:19:13 2024 +0800

    Merge branch 'Calcium-Ion:main' into main

commit ef35b072824b5095ecd2d1ed7ca9fa11673da2c4
Author: Jiayun Shen <jimmyshjj@gmail.com>
Date:   Tue May 14 19:17:32 2024 +0800

    Update adaptor.go

    Update frequently used model names from Baidu official docs and support custom models
2024-05-16 14:05:44 +08:00
Bubu
63d68ce7bf Merge branch 'Calcium-Ion:main' into main 2024-05-16 08:26:49 +08:00
Boo p3psi
95ac7c343b 修复渠道测试没有走模型映射 2024-05-16 08:24:42 +08:00
CaIon
93858c32d9 feat: 完善模型价格获取逻辑 2024-05-15 23:56:26 +08:00
CaIon
ff044de42a feat: 完善模型价格页面 2024-05-15 20:17:27 +08:00
CaIon
a3b3e6cc38 chore: update InitTokenEncoders (#255) 2024-05-15 16:32:00 +08:00
CaIon
7b5830522a Merge remote-tracking branch 'origin/main' 2024-05-15 14:18:43 +08:00
CaIon
9dcec2772d chore: update tiktoken (#254) 2024-05-15 14:18:29 +08:00
Calcium-Ion
8faf5d2517 Merge pull request #252 from utopeadia/main
add gemini-1.5-flash-latest support
2024-05-15 14:15:16 +08:00
HowieWu
a3a6733fb5 Update constant.go 2024-05-15 10:28:30 +08:00
HowieWu
0f11461af3 Update model-ratio.go 2024-05-15 10:27:30 +08:00
HowieWu
a5b84ba524 Update adaptor.go 2024-05-15 10:25:01 +08:00
Calcium-Ion
c222bc8752 Merge pull request #251 from congyijiu/main
Update constant.go
2024-05-14 21:24:19 +08:00
congyijiu
3dd2a5bfc5 Update constant.go
To prevent the default testing of GPT-4o
2024-05-14 20:47:06 +08:00
CaIon
9f18641d7e fix: gpt-4-gizmo-* model ratio 2024-05-14 16:35:43 +08:00
Calcium-Ion
ced67b9bb3 Merge pull request #248 from QuentinHsu/refactor-settings-operation
Refactor settings operation
2024-05-14 11:38:23 +08:00
CaIon
eda3bd1c9d feat: update model ratio 2024-05-14 11:37:24 +08:00
Calcium-Ion
9a9fd34cba Merge pull request #247 from MapleEve/gpt4o
Support Gpt4o
2024-05-14 11:36:59 +08:00
CaIon
475dea96d2 feat: update model ratio 2024-05-14 11:08:42 +08:00
QuentinHsu
0ddb67f9a2 Merge branch 'main' into refactor-settings-operation 2024-05-14 10:19:33 +08:00
QuentinHsu
470f3a1d51 perf: 运营设置-提示文案 2024-05-14 10:18:24 +08:00
QuentinHsu
65ae70919b perf: 运营设置-数据刷新逻辑 2024-05-14 10:17:20 +08:00
Maple Gao
256ccfa989 add: Support GPT4o ratio 2024-05-14 09:48:04 +08:00
Maple Gao
6c059d5bf2 add: Support GPT4o 2024-05-14 09:46:39 +08:00
QuentinHsu
98c347e048 refactor: 运营设置-数据刷新 2024-05-13 18:14:57 +08:00
QuentinHsu
b283365ebc refactor: 运营设置-倍率设置 2024-05-13 17:55:15 +08:00
QuentinHsu
698af0786d Merge branch 'main' into refactor-settings-operation 2024-05-13 16:29:02 +08:00
QuentinHsu
968ef1e5fa refactor: 运营设置-额度设置 2024-05-11 17:48:05 +08:00
QuentinHsu
88bc295855 refactor: 运营设置-监控设置 2024-05-11 17:20:18 +08:00
QuentinHsu
76f6b41bb2 refactor: 运营设置-数据看板设置 2024-05-11 16:23:10 +08:00
QuentinHsu
a9d9877bce perf: 移除不生效的参数 2024-05-11 16:13:28 +08:00
QuentinHsu
003745abcb refactor: 运营设置-日志设置 2024-05-11 14:06:32 +08:00
QuentinHsu
96468ce64f refactor: 运营设置-屏蔽词过滤设置 2024-05-10 16:17:48 +08:00
QuentinHsu
9886cdd527 refactor: 运营设置-绘图设置 2024-05-09 17:20:51 +08:00
QuentinHsu
83dd62982e refactor: 运营设置-通用设置 2024-05-09 17:01:55 +08:00
46 changed files with 1853 additions and 783 deletions

View File

@@ -208,6 +208,7 @@ const (
ChannelTypeLingYiWanWu = 31
ChannelTypeAws = 33
ChannelTypeCohere = 34
ChannelTypeMiniMax = 35
ChannelTypeDummy // this one is only for count, do not add any channel after this
)
@@ -248,4 +249,5 @@ var ChannelBaseURLs = []string{
"", //32
"", //33
"https://api.cohere.ai", //34
"https://api.minimax.chat", //35
}

View File

@@ -5,6 +5,13 @@ import (
"strings"
)
// from songquanpeng/one-api
const (
USD2RMB = 7.3 // 暂定 1 USD = 7.3 RMB
USD = 500 // $0.002 = 1 -> $1 = 500
RMB = USD / USD2RMB
)
// modelRatio
// https://platform.openai.com/docs/models/model-endpoint-compatibility
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
@@ -27,83 +34,106 @@ var DefaultModelRatio = map[string]float64{
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
"gpt-4-1106-vision-preview": 5, // $0.01 / 1K tokens
"gpt-4o": 2.5, // $0.01 / 1K tokens
"gpt-4o-2024-05-13": 2.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-3.5-turbo": 0.25, // $0.0015 / 1K tokens
//"gpt-3.5-turbo-0301": 0.75, //deprecated
"gpt-3.5-turbo-0613": 0.75,
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
"gpt-3.5-turbo-16k-0613": 1.5,
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
"gpt-3.5-turbo-0125": 0.25,
"babbage-002": 0.2, // $0.0004 / 1K tokens
"davinci-002": 1, // $0.002 / 1K tokens
"text-ada-001": 0.2,
"text-babbage-001": 0.25,
"text-curie-001": 1,
"text-davinci-002": 10,
"text-davinci-003": 10,
"text-davinci-edit-001": 10,
"code-davinci-edit-001": 10,
"whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens
"tts-1": 7.5, // 1k characters -> $0.015
"tts-1-1106": 7.5, // 1k characters -> $0.015
"tts-1-hd": 15, // 1k characters -> $0.03
"tts-1-hd-1106": 15, // 1k characters -> $0.03
"davinci": 10,
"curie": 10,
"babbage": 10,
"ada": 10,
"text-embedding-3-small": 0.01,
"text-embedding-3-large": 0.065,
"text-embedding-ada-002": 0.05,
"text-search-ada-doc-001": 10,
"text-moderation-stable": 0.1,
"text-moderation-latest": 0.1,
"claude-instant-1": 0.4, // $0.8 / 1M tokens
"claude-2.0": 4, // $8 / 1M tokens
"claude-2.1": 4, // $8 / 1M tokens
"claude-3-haiku-20240307": 0.125, // $0.25 / 1M tokens
"claude-3-sonnet-20240229": 1.5, // $3 / 1M tokens
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens
"ERNIE-Bot-4": 8.572, // ¥0.12 / 1k tokens
"Embedding-V1": 0.1429, // ¥0.002 / 1k tokens
"PaLM-2": 1,
"gemini-pro": 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-001": 1,
"gemini-1.5-pro-latest": 1,
"gemini-1.0-pro-latest": 1,
"gemini-1.0-pro-vision-latest": 1,
"gemini-ultra": 1,
"chatglm_turbo": 0.3572, // 0.005 / 1k tokens
"chatglm_pro": 0.7143, // 0.01 / 1k tokens
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
"glm-4": 7.143, // ¥0.1 / 1k tokens
"glm-4v": 7.143, // ¥0.1 / 1k tokens
"glm-3-turbo": 0.3572,
"qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
"qwen-plus": 10, // ¥0.14 / 1k tokens
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
"gpt-3.5-turbo-0613": 0.75,
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
"gpt-3.5-turbo-16k-0613": 1.5,
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
"gpt-3.5-turbo-0125": 0.25,
"babbage-002": 0.2, // $0.0004 / 1K tokens
"davinci-002": 1, // $0.002 / 1K tokens
"text-ada-001": 0.2,
"text-babbage-001": 0.25,
"text-curie-001": 1,
"text-davinci-002": 10,
"text-davinci-003": 10,
"text-davinci-edit-001": 10,
"code-davinci-edit-001": 10,
"whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens
"tts-1": 7.5, // 1k characters -> $0.015
"tts-1-1106": 7.5, // 1k characters -> $0.015
"tts-1-hd": 15, // 1k characters -> $0.03
"tts-1-hd-1106": 15, // 1k characters -> $0.03
"davinci": 10,
"curie": 10,
"babbage": 10,
"ada": 10,
"text-embedding-3-small": 0.01,
"text-embedding-3-large": 0.065,
"text-embedding-ada-002": 0.05,
"text-search-ada-doc-001": 10,
"text-moderation-stable": 0.1,
"text-moderation-latest": 0.1,
"claude-instant-1": 0.4, // $0.8 / 1M tokens
"claude-2.0": 4, // $8 / 1M tokens
"claude-2.1": 4, // $8 / 1M tokens
"claude-3-haiku-20240307": 0.125, // $0.25 / 1M tokens
"claude-3-sonnet-20240229": 1.5, // $3 / 1M tokens
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens //renamed to ERNIE-3.5-8K
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens //renamed to ERNIE-Lite-8K
"ERNIE-Bot-4": 8.572, // ¥0.12 / 1k tokens //renamed to ERNIE-4.0-8K
"ERNIE-4.0-8K": 8.572, // ¥0.12 / 1k tokens
"ERNIE-3.5-8K": 0.8572, // ¥0.012 / 1k tokens
"ERNIE-Speed-8K": 0.2858, // 0.004 / 1k tokens
"ERNIE-Speed-128K": 0.2858, // 0.004 / 1k tokens
"ERNIE-Lite-8K": 0.2143, // ¥0.003 / 1k tokens
"ERNIE-Tiny-8K": 0.0715, // ¥0.001 / 1k tokens
"ERNIE-Character-8K": 0.2858, // ¥0.004 / 1k tokens
"ERNIE-Functions-8K": 0.2858, // ¥0.004 / 1k tokens
"Embedding-V1": 0.1429, // ¥0.002 / 1k tokens
"PaLM-2": 1,
"gemini-pro": 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-001": 1,
"gemini-1.5-pro-latest": 1,
"gemini-1.5-flash-latest": 1,
"gemini-1.0-pro-latest": 1,
"gemini-1.0-pro-vision-latest": 1,
"gemini-ultra": 1,
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
"glm-4": 7.143, // ¥0.1 / 1k tokens
"glm-4v": 7.143, // 0.1 / 1k tokens
"glm-3-turbo": 0.3572,
"qwen-turbo": 0.8572, // 0.012 / 1k tokens
"qwen-plus": 10, // 0.14 / 1k tokens
"text-embedding-v1": 0.05, // 0.0007 / 1k tokens
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"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#-计费单元
// 已经按照 7.2 来换算美元价格
"yi-34b-chat-0205": 0.018,
"yi-34b-chat-200k": 0.0864,
"yi-vl-plus": 0.0432,
"yi-34b-chat-0205": 0.18,
"yi-34b-chat-200k": 0.864,
"yi-vl-plus": 0.432,
"yi-large": 20.0 / 1000 * RMB,
"yi-medium": 2.5 / 1000 * RMB,
"yi-vision": 6.0 / 1000 * RMB,
"yi-medium-200k": 12.0 / 1000 * RMB,
"yi-spark": 1.0 / 1000 * RMB,
"yi-large-rag": 25.0 / 1000 * RMB,
"yi-large-turbo": 12.0 / 1000 * RMB,
"yi-large-preview": 20.0 / 1000 * RMB,
"yi-large-rag-preview": 25.0 / 1000 * RMB,
"command": 0.5,
"command-nightly": 0.5,
"command-light": 0.5,
@@ -112,6 +142,11 @@ var DefaultModelRatio = map[string]float64{
"command-r-plus ": 1.5,
"deepseek-chat": 0.07,
"deepseek-coder": 0.07,
// Perplexity online 模型对搜索额外收费,有需要应自行调整,此处不计入搜索费用
"llama-3-sonar-small-32k-chat": 0.2 / 1000 * USD,
"llama-3-sonar-small-32k-online": 0.2 / 1000 * USD,
"llama-3-sonar-large-32k-chat": 1 / 1000 * USD,
"llama-3-sonar-large-32k-online": 1 / 1000 * USD,
}
var DefaultModelPrice = map[string]float64{
@@ -240,6 +275,9 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error {
}
func GetCompletionRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4-gizmo") {
name = "gpt-4-gizmo-*"
}
if strings.HasPrefix(name, "gpt-3.5") {
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
// https://openai.com/blog/new-embedding-models-and-api-updates
@@ -251,8 +289,8 @@ func GetCompletionRatio(name string) float64 {
}
return 4.0 / 3.0
}
if strings.HasPrefix(name, "gpt-4") && name != "gpt-4-all" && !strings.HasPrefix(name, "gpt-4-gizmo") {
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") {
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") || strings.HasPrefix(name, "gpt-4o") {
return 3
}
return 2
@@ -283,6 +321,15 @@ func GetCompletionRatio(name string) float64 {
if strings.HasPrefix(name, "deepseek") {
return 2
}
if strings.HasPrefix(name, "ERNIE-Speed-") {
return 2
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
return 2
} else if strings.HasPrefix(name, "ERNIE-Character") {
return 2
} else if strings.HasPrefix(name, "ERNIE-Functions") {
return 2
}
switch name {
case "llama2-70b-4096":
return 0.8 / 0.64

View File

@@ -258,3 +258,12 @@ func MapToJsonStrFloat(m map[string]float64) string {
}
return string(bytes)
}
func StrToMap(str string) map[string]interface{} {
m := make(map[string]interface{})
err := json.Unmarshal([]byte(str), &m)
if err != nil {
return nil
}
return m
}

View File

@@ -64,7 +64,21 @@ func testChannel(channel *model.Channel, testModel string) (err error, openaiErr
} else {
testModel = adaptor.GetModelList()[0]
}
} else {
modelMapping := *channel.ModelMapping
if modelMapping != "" && modelMapping != "{}" {
modelMap := make(map[string]string)
err := json.Unmarshal([]byte(modelMapping), &modelMap)
if err != nil {
openaiErr := service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError).Error
return err, &openaiErr
}
if modelMap[testModel] != "" {
testModel = modelMap[testModel]
}
}
}
request := buildTestRequest()
request.Model = testModel
meta.UpstreamModelName = testModel

View File

@@ -11,6 +11,7 @@ import (
"one-api/relay"
"one-api/relay/channel/ai360"
"one-api/relay/channel/lingyiwanwu"
"one-api/relay/channel/minimax"
"one-api/relay/channel/moonshot"
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
@@ -79,7 +80,7 @@ func init() {
Id: modelName,
Object: "model",
Created: 1626777600,
OwnedBy: "moonshot",
OwnedBy: moonshot.ChannelName,
Permission: permission,
Root: modelName,
Parent: nil,
@@ -90,7 +91,18 @@ func init() {
Id: modelName,
Object: "model",
Created: 1626777600,
OwnedBy: "lingyiwanwu",
OwnedBy: lingyiwanwu.ChannelName,
Permission: permission,
Root: modelName,
Parent: nil,
})
}
for _, modelName := range minimax.ModelList {
openAIModels = append(openAIModels, dto.OpenAIModels{
Id: modelName,
Object: "model",
Created: 1626777600,
OwnedBy: minimax.ChannelName,
Permission: permission,
Root: modelName,
Parent: nil,
@@ -108,8 +120,8 @@ func init() {
})
}
openAIModelsMap = make(map[string]dto.OpenAIModels)
for _, model := range openAIModels {
openAIModelsMap[model.Id] = model
for _, aiModel := range openAIModels {
openAIModelsMap[aiModel.Id] = aiModel
}
channelId2Models = make(map[int][]string)
for i := 1; i <= common.ChannelTypeDummy; i++ {
@@ -174,8 +186,8 @@ func DashboardListModels(c *gin.Context) {
func RetrieveModel(c *gin.Context) {
modelId := c.Param("model")
if model, ok := openAIModelsMap[modelId]; ok {
c.JSON(200, model)
if aiModel, ok := openAIModelsMap[modelId]; ok {
c.JSON(200, aiModel)
} else {
openAIError := dto.OpenAIError{
Message: fmt.Sprintf("The model '%s' does not exist", modelId),
@@ -191,12 +203,12 @@ func RetrieveModel(c *gin.Context) {
func GetPricing(c *gin.Context) {
userId := c.GetInt("id")
user, _ := model.GetUserById(userId, true)
group, err := model.CacheGetUserGroup(userId)
groupRatio := common.GetGroupRatio("default")
if user != nil {
groupRatio = common.GetGroupRatio(user.Group)
if err != nil {
groupRatio = common.GetGroupRatio(group)
}
pricing := model.GetPricing(user, openAIModels)
pricing := model.GetPricing(group)
c.JSON(200, gin.H{
"success": true,
"data": pricing,

View File

@@ -43,7 +43,7 @@ func Relay(c *gin.Context) {
group := c.GetString("group")
originalModel := c.GetString("original_model")
openaiErr := relayHandler(c, relayMode)
useChannel := []int{channelId}
c.Set("use_channel", []string{fmt.Sprintf("%d", channelId)})
if openaiErr != nil {
go processChannelError(c, channelId, openaiErr)
} else {
@@ -56,7 +56,9 @@ func Relay(c *gin.Context) {
break
}
channelId = channel.Id
useChannel = append(useChannel, channelId)
useChannel := c.GetStringSlice("use_channel")
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
c.Set("use_channel", useChannel)
common.LogInfo(c.Request.Context(), fmt.Sprintf("using channel #%d to retry (remain times %d)", channel.Id, i))
middleware.SetupContextForSelectedChannel(c, channel, originalModel)
@@ -67,6 +69,7 @@ func Relay(c *gin.Context) {
go processChannelError(c, channelId, openaiErr)
}
}
useChannel := c.GetStringSlice("use_channel")
if len(useChannel) > 1 {
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]"))
common.LogInfo(c.Request.Context(), retryLogStr)

6
go.mod
View File

@@ -17,11 +17,11 @@ require (
github.com/go-playground/validator/v10 v10.19.0
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.4.0
github.com/linux-do/tiktoken-go v0.7.0
github.com/pkg/errors v0.9.1
github.com/pkoukk/tiktoken-go v0.1.6
github.com/samber/lo v1.39.0
github.com/shirou/gopsutil v3.21.11+incompatible
golang.org/x/crypto v0.21.0
@@ -42,7 +42,7 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect

12
go.sum
View File

@@ -32,8 +32,8 @@ 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/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/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/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
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=
@@ -81,8 +81,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
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/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/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/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -124,6 +124,8 @@ 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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -148,8 +150,6 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc
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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=

View File

@@ -142,6 +142,15 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
tx = tx.Where("created_at <= ?", endTimestamp)
}
err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error
for i := range logs {
var otherMap map[string]interface{}
otherMap = common.StrToMap(logs[i].Other)
if otherMap != nil {
// delete admin
delete(otherMap, "admin_info")
}
logs[i].Other = common.MapToJsonStr(otherMap)
}
return logs, err
}

View File

@@ -93,12 +93,12 @@ func InitDB() (err error) {
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
}
//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 = db.AutoMigrate(&Channel{})
if err != nil {

View File

@@ -13,16 +13,16 @@ var (
updatePricingLock sync.Mutex
)
func GetPricing(user *User, openAIModels []dto.OpenAIModels) []dto.ModelPricing {
func GetPricing(group string) []dto.ModelPricing {
updatePricingLock.Lock()
defer updatePricingLock.Unlock()
if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
updatePricing(openAIModels)
updatePricing()
}
if user != nil {
if group != "" {
userPricingMap := make([]dto.ModelPricing, 0)
models := GetGroupModels(user.Group)
models := GetGroupModels(group)
for _, pricing := range pricingMap {
if !common.StringsContains(models, pricing.ModelName) {
pricing.Available = false
@@ -34,28 +34,19 @@ func GetPricing(user *User, openAIModels []dto.OpenAIModels) []dto.ModelPricing
return pricingMap
}
func updatePricing(openAIModels []dto.OpenAIModels) {
modelRatios := common.GetModelRatios()
func updatePricing() {
//modelRatios := common.GetModelRatios()
enabledModels := GetEnabledModels()
allModels := make(map[string]string)
for _, openAIModel := range openAIModels {
if common.StringsContains(enabledModels, openAIModel.Id) {
allModels[openAIModel.Id] = openAIModel.OwnedBy
}
}
for model, _ := range modelRatios {
if common.StringsContains(enabledModels, model) {
if _, ok := allModels[model]; !ok {
allModels[model] = "custom"
}
}
allModels := make(map[string]int)
for i, model := range enabledModels {
allModels[model] = i
}
pricingMap = make([]dto.ModelPricing, 0)
for model, ownerBy := range allModels {
for model, _ := range allModels {
pricing := dto.ModelPricing{
Available: true,
ModelName: model,
OwnerBy: ownerBy,
}
modelPrice, findPrice := common.GetModelPrice(model, false)
if findPrice {

View File

@@ -11,7 +11,7 @@ import (
type Token struct {
Id int `json:"id"`
UserId int `json:"user_id"`
UserId int `json:"user_id" gorm:"index"`
Key string `json:"key" gorm:"type:char(48);uniqueIndex"`
Status int `json:"status" gorm:"default:1"`
Name string `json:"name" gorm:"index" `

View File

@@ -1,6 +1,9 @@
package ai360
var ModelList = []string{
"360gpt-turbo",
"360gpt-turbo-responsibility-8k",
"360gpt-pro",
"360GPT_S2_V9",
"embedding-bert-512-v1",
"embedding_s1_v1",

View File

@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
"strings"
)
type Adaptor struct {
@@ -33,8 +34,24 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant"
case "BLOOMZ-7B":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/bloomz_7b1"
case "ERNIE-4.0-8K":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro"
case "ERNIE-3.5-8K":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions"
case "ERNIE-Speed-8K":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_speed"
case "ERNIE-Character-8K":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-char-8k"
case "ERNIE-Functions-8K":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-func-8k"
case "ERNIE-Lite-8K-0922":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant"
case "Yi-34B-Chat":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/yi_34b_chat"
case "Embedding-V1":
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1"
default:
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/" + strings.ToLower(info.UpstreamModelName)
}
var accessToken string
var err error

View File

@@ -1,11 +1,19 @@
package baidu
var ModelList = []string{
"ERNIE-Bot-4",
"ERNIE-Bot-8K",
"ERNIE-Bot",
"ERNIE-Speed",
"ERNIE-Bot-turbo",
"ERNIE-3.5-8K",
"ERNIE-4.0-8K",
"ERNIE-Speed-8K",
"ERNIE-Speed-128K",
"ERNIE-Lite-8K",
"ERNIE-Tiny-8K",
"ERNIE-Character-8K",
"ERNIE-Functions-8K",
//"ERNIE-Bot-4",
//"ERNIE-Bot-8K",
//"ERNIE-Bot",
//"ERNIE-Speed",
//"ERNIE-Bot-turbo",
"Embedding-V1",
}

View File

@@ -21,6 +21,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo, request dto.GeneralOpenAIReq
// 定义一个映射,存储模型名称和对应的版本
var modelVersionMap = map[string]string{
"gemini-1.5-pro-latest": "v1beta",
"gemini-1.5-flash-latest": "v1beta",
"gemini-ultra": "v1beta",
}

View File

@@ -5,7 +5,7 @@ const (
)
var ModelList = []string{
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-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",
}

View File

@@ -1,9 +1,9 @@
package lingyiwanwu
// https://platform.lingyiwanwu.com/docs
var ModelList = []string{
"yi-34b-chat-0205",
"yi-34b-chat-200k",
"yi-vl-plus",
}
package lingyiwanwu
// https://platform.lingyiwanwu.com/docs
var ModelList = []string{
"yi-large", "yi-medium", "yi-vision", "yi-medium-200k", "yi-spark", "yi-large-rag", "yi-large-turbo", "yi-large-preview", "yi-large-rag-preview",
}
var ChannelName = "lingyiwanwu"

View File

@@ -0,0 +1,13 @@
package minimax
// https://www.minimaxi.com/document/guides/chat-model/V2?id=65e0736ab2845de20908e2dd
var ModelList = []string{
"abab6.5-chat",
"abab6.5s-chat",
"abab6-chat",
"abab5.5-chat",
"abab5.5s-chat",
}
var ChannelName = "minimax"

View File

@@ -0,0 +1,10 @@
package minimax
import (
"fmt"
relaycommon "one-api/relay/common"
)
func GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
return fmt.Sprintf("%s/v1/text/chatcompletion_v2", info.BaseUrl), nil
}

View File

@@ -5,3 +5,5 @@ var ModelList = []string{
"moonshot-v1-32k",
"moonshot-v1-128k",
}
var ChannelName = "moonshot"

View File

@@ -11,6 +11,7 @@ import (
"one-api/relay/channel"
"one-api/relay/channel/ai360"
"one-api/relay/channel/lingyiwanwu"
"one-api/relay/channel/minimax"
"one-api/relay/channel/moonshot"
relaycommon "one-api/relay/common"
"one-api/service"
@@ -26,7 +27,8 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo, request dto.GeneralOpenAIReq
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.ChannelType == common.ChannelTypeAzure {
switch info.ChannelType {
case common.ChannelTypeAzure:
// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
requestURL := strings.Split(info.RequestURLPath, "?")[0]
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, info.ApiVersion)
@@ -37,8 +39,15 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task)
return relaycommon.GetFullRequestURL(info.BaseUrl, requestURL, info.ChannelType), nil
case common.ChannelTypeMiniMax:
return minimax.GetRequestURL(info)
case common.ChannelTypeCustom:
url := info.BaseUrl
url = strings.Replace(url, "{model}", info.UpstreamModelName, -1)
return url, nil
default:
return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
}
return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
@@ -90,11 +99,24 @@ func (a *Adaptor) GetModelList() []string {
return moonshot.ModelList
case common.ChannelTypeLingYiWanWu:
return lingyiwanwu.ModelList
case common.ChannelTypeMiniMax:
return minimax.ModelList
default:
return ModelList
}
}
func (a *Adaptor) GetChannelName() string {
return ChannelName
switch a.ChannelType {
case common.ChannelType360:
return ai360.ChannelName
case common.ChannelTypeMoonshot:
return moonshot.ChannelName
case common.ChannelTypeLingYiWanWu:
return lingyiwanwu.ChannelName
case common.ChannelTypeMiniMax:
return minimax.ChannelName
default:
return ChannelName
}
}

View File

@@ -8,6 +8,7 @@ var ModelList = []string{
"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-vision-preview",
"gpt-4o", "gpt-4o-2024-05-13",
"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-moderation-latest", "text-moderation-stable",

View File

@@ -1,7 +1,7 @@
package perplexity
var ModelList = []string{
"sonar-small-chat", "sonar-small-online", "sonar-medium-chat", "sonar-medium-online", "mistral-7b-instruct", "mixtral-8x7b-instruct",
"llama-3-sonar-small-32k-chat", "llama-3-sonar-small-32k-online", "llama-3-sonar-large-32k-chat", "llama-3-sonar-large-32k-online", "llama-3-8b-instruct", "llama-3-70b-instruct", "mixtral-8x7b-instruct",
}
var ChannelName = "perplexity"

View File

@@ -15,7 +15,7 @@ const (
APITypeAIProxyLibrary
APITypeTencent
APITypeGemini
APITypeZhipu_v4
APITypeZhipuV4
APITypeOllama
APITypePerplexity
APITypeAws
@@ -48,7 +48,7 @@ func ChannelType2APIType(channelType int) (int, bool) {
case common.ChannelTypeGemini:
apiType = APITypeGemini
case common.ChannelTypeZhipu_v4:
apiType = APITypeZhipu_v4
apiType = APITypeZhipuV4
case common.ChannelTypeOllama:
apiType = APITypeOllama
case common.ChannelTypePerplexity:

View File

@@ -320,6 +320,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRe
other["group_ratio"] = groupRatio
other["completion_ratio"] = completionRatio
other["model_price"] = modelPrice
adminInfo := make(map[string]interface{})
adminInfo["use_channel"] = ctx.GetStringSlice("use_channel")
other["admin_info"] = adminInfo
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
//if quota != 0 {

View File

@@ -41,7 +41,7 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &xunfei.Adaptor{}
case constant.APITypeZhipu:
return &zhipu.Adaptor{}
case constant.APITypeZhipu_v4:
case constant.APITypeZhipuV4:
return &zhipu_4v.Adaptor{}
case constant.APITypeOllama:
return &ollama.Adaptor{}

View File

@@ -20,7 +20,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/about", controller.GetAbout)
//apiRouter.GET("/midjourney", controller.GetMidjourney)
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
apiRouter.GET("/pricing", middleware.CriticalRateLimit(), middleware.TryUserAuth(), controller.GetPricing)
apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing)
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/pkoukk/tiktoken-go"
"github.com/linux-do/tiktoken-go"
"image"
"log"
"math"
@@ -26,6 +26,7 @@ func InitTokenEncoders() {
}
defaultTokenEncoder = gpt35TokenEncoder
gpt4TokenEncoder, err := tiktoken.EncodingForModel("gpt-4")
gpt4oTokenEncoder, err := tiktoken.EncodingForModel("gpt-4o")
if err != nil {
common.FatalLog(fmt.Sprintf("failed to get gpt-4 token encoder: %s", err.Error()))
}
@@ -33,7 +34,11 @@ func InitTokenEncoders() {
if strings.HasPrefix(model, "gpt-3.5") {
tokenEncoderMap[model] = gpt35TokenEncoder
} else if strings.HasPrefix(model, "gpt-4") {
tokenEncoderMap[model] = gpt4TokenEncoder
if strings.HasPrefix(model, "gpt-4o") {
tokenEncoderMap[model] = gpt4oTokenEncoder
} else {
tokenEncoderMap[model] = gpt4TokenEncoder
}
} else {
tokenEncoderMap[model] = nil
}
@@ -62,7 +67,11 @@ func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int {
return len(tokenEncoder.Encode(text, nil, nil))
}
func getImageToken(imageUrl *dto.MessageImageUrl) (int, error) {
func getImageToken(imageUrl *dto.MessageImageUrl, model string, stream bool) (int, error) {
// TODO: 非流模式下不计算图片token数量
if model == "glm-4v" {
return 1047, nil
}
if imageUrl.Detail == "low" {
return 85, nil
}
@@ -118,7 +127,7 @@ func getImageToken(imageUrl *dto.MessageImageUrl) (int, error) {
func CountTokenChatRequest(request dto.GeneralOpenAIRequest, model string, checkSensitive bool) (int, error, bool) {
tkm := 0
msgTokens, err, b := CountTokenMessages(request.Messages, model, checkSensitive)
msgTokens, err, b := CountTokenMessages(request.Messages, model, request.Stream, checkSensitive)
if err != nil {
return 0, err, b
}
@@ -151,7 +160,7 @@ func CountTokenChatRequest(request dto.GeneralOpenAIRequest, model string, check
return tkm, nil, false
}
func CountTokenMessages(messages []dto.Message, model string, checkSensitive bool) (int, error, bool) {
func CountTokenMessages(messages []dto.Message, model string, stream bool, checkSensitive bool) (int, error, bool) {
//recover when panic
tokenEncoder := getTokenEncoder(model)
// Reference:
@@ -188,19 +197,13 @@ func CountTokenMessages(messages []dto.Message, model string, checkSensitive boo
tokenNum += getTokenNum(tokenEncoder, *message.Name)
}
} else {
var err error
arrayContent := message.ParseContent()
for _, m := range arrayContent {
if m.Type == "image_url" {
var imageTokenNum int
if model == "glm-4v" {
imageTokenNum = 1047
} else {
imageUrl := m.ImageUrl.(dto.MessageImageUrl)
imageTokenNum, err = getImageToken(&imageUrl)
if err != nil {
return 0, err, false
}
imageUrl := m.ImageUrl.(dto.MessageImageUrl)
imageTokenNum, err := getImageToken(&imageUrl, model, stream)
if err != nil {
return 0, err, false
}
tokenNum += imageTokenNum
log.Printf("image token num: %d", imageTokenNum)

View File

@@ -5,11 +5,12 @@
"type": "module",
"dependencies": {
"@douyinfe/semi-icons": "^2.46.1",
"@douyinfe/semi-ui": "^2.46.1",
"@douyinfe/semi-ui": "^2.55.3",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
"axios": "^0.27.2",
"dayjs": "^1.11.11",
"history": "^5.3.0",
"marked": "^4.1.1",
"react": "^18.2.0",

View File

@@ -6,6 +6,7 @@ import {
showError,
showInfo,
showSuccess,
showWarning,
timestamp2string,
} from '../helpers';
@@ -309,6 +310,12 @@ const ChannelsTable = () => {
const setChannelFormat = (channels) => {
for (let i = 0; i < channels.length; i++) {
// if (channels[i].type === 8) {
// showWarning(
// '检测到您使用了“自定义渠道”类型请更换为“OpenAI”渠道类型',
// );
// showWarning('下个版本将不再支持“自定义渠道”类型!');
// }
channels[i].key = '' + channels[i].id;
let test_models = [];
channels[i].models.split(',').forEach((item, index) => {

View File

@@ -294,11 +294,42 @@ const LogsTable = () => {
);
},
},
{
title: '重试',
dataIndex: 'retry',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
let content = '渠道:' + record.channel;
if (record.other !== '') {
let other = JSON.parse(record.other);
if (other === null) {
return <></>
}
if (other.admin_info !== undefined) {
if (
other.admin_info.use_channel !== null &&
other.admin_info.use_channel !== undefined &&
other.admin_info.use_channel !== ''
) {
// channel id array
let useChannel = other.admin_info.use_channel;
let useChannelStr = useChannel.join('->');
content = `渠道:${useChannelStr}`;
}
}
}
return isAdminUser ? <div>{content}</div> : <></>;
},
},
{
title: '详情',
dataIndex: 'content',
render: (text, record, index) => {
if (record.other === '') {
record.other = '{}'
}
let other = JSON.parse(record.other);
if (other == null) {
return (
<Paragraph
ellipsis={{
@@ -314,7 +345,6 @@ const LogsTable = () => {
</Paragraph>
);
}
let other = JSON.parse(record.other);
let content = renderModelPrice(
record.prompt_tokens,
record.completion_tokens,

View File

@@ -1,7 +1,16 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { API, copy, showError, showSuccess } from '../helpers';
import { Banner, Layout, Modal, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
import {
Banner,
Input,
Layout,
Modal,
Space,
Table,
Tag,
Tooltip,
} from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render.js';
import { UserContext } from '../context/User/index.js';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
@@ -45,6 +54,27 @@ function renderAvailable(available) {
}
const ModelPricing = () => {
const [filteredValue, setFilteredValue] = useState([]);
const compositionRef = useRef({ isComposition: false });
const handleChange = (value) => {
if (compositionRef.current.isComposition) {
return;
}
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const handleCompositionStart = () => {
compositionRef.current.isComposition = true;
};
const handleCompositionEnd = (event) => {
compositionRef.current.isComposition = false;
const value = event.target.value;
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const columns = [
{
title: '可用性',
@@ -52,28 +82,28 @@ const ModelPricing = () => {
render: (text, record, index) => {
return renderAvailable(text);
},
sorter: (a, b) => a.available - b.available,
},
{
title: '提供者',
dataIndex: 'owner_by',
render: (text, record, index) => {
return (
<>
<Tag color={stringToColor(text)} size='large'>
{text}
</Tag>
</>
);
},
},
{
title: '模型名称',
title: (
<Space>
<span>模型名称</span>
<Input
placeholder='模糊搜索'
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
</Space>
),
dataIndex: 'model_name', // 以finish_time作为dataIndex
render: (text, record, index) => {
return (
<>
<Tag
color={stringToColor(record.owner_by)}
color={stringToColor(text)}
size='large'
onClick={() => {
copyText(text);
@@ -84,6 +114,8 @@ const ModelPricing = () => {
</>
);
},
onFilter: (value, record) => record.model_name.includes(value),
filteredValue,
},
{
title: '计费类型',
@@ -91,6 +123,7 @@ const ModelPricing = () => {
render: (text, record, index) => {
return renderQuotaType(parseInt(text));
},
sorter: (a, b) => a.quota_type - b.quota_type,
},
{
title: '模型倍率',
@@ -113,11 +146,11 @@ const ModelPricing = () => {
render: (text, record, index) => {
let content = text;
if (record.quota_type === 0) {
let inputRatioPrice = record.model_ratio * 2.0 * record.group_ratio;
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = record.model_ratio * 2 * record.group_ratio;
let completionRatioPrice =
record.model_ratio *
record.completion_ratio *
2.0 *
record.completion_ratio * 2 *
record.group_ratio;
content = (
<>
@@ -150,14 +183,17 @@ const ModelPricing = () => {
return a.quota_type - b.quota_type;
});
// sort by owner_by, openai is max, other use localeCompare
// sort by model_name, start with gpt is max, other use localeCompare
models.sort((a, b) => {
if (a.owner_by === 'openai') {
if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
return -1;
} else if (b.owner_by === 'openai') {
} else if (
!a.model_name.startsWith('gpt') &&
b.model_name.startsWith('gpt')
) {
return 1;
} else {
return a.owner_by.localeCompare(b.owner_by);
return a.model_name.localeCompare(b.model_name);
}
});

View File

@@ -1,17 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
import {
API,
showError,
showSuccess,
timestamp2string,
verifyJSON,
} from '../helpers';
import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
import { useTheme } from '../context/Theme';
import { API, showError, showSuccess } from '../helpers';
const OperationSetting = () => {
let now = new Date();
let [inputs, setInputs] = useState({
QuotaForNewUser: 0,
QuotaForInviter: 0,
@@ -27,39 +27,31 @@ const OperationSetting = () => {
ChatLink: '',
ChatLink2: '', // 添加的新状态变量
QuotaPerUnit: 0,
AutomaticDisableChannelEnabled: '',
AutomaticEnableChannelEnabled: '',
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
ChannelDisableThreshold: 0,
LogConsumeEnabled: '',
DisplayInCurrencyEnabled: '',
DisplayTokenStatEnabled: '',
CheckSensitiveEnabled: '',
CheckSensitiveOnPromptEnabled: '',
LogConsumeEnabled: false,
DisplayInCurrencyEnabled: false,
DisplayTokenStatEnabled: false,
CheckSensitiveEnabled: false,
CheckSensitiveOnPromptEnabled: false,
CheckSensitiveOnCompletionEnabled: '',
StopOnSensitiveEnabled: '',
SensitiveWords: '',
MjNotifyEnabled: '',
MjAccountFilterEnabled: '',
MjModeClearEnabled: '',
MjForwardUrlEnabled: '',
DrawingEnabled: '',
DataExportEnabled: '',
MjNotifyEnabled: false,
MjAccountFilterEnabled: false,
MjModeClearEnabled: false,
MjForwardUrlEnabled: false,
DrawingEnabled: false,
DataExportEnabled: false,
DataExportDefaultTime: 'hour',
DataExportInterval: 5,
DefaultCollapseSidebar: '', // 默认折叠侧边栏
DefaultCollapseSidebar: false, // 默认折叠侧边栏
RetryTimes: 0,
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
let [historyTimestamp, setHistoryTimestamp] = useState(
timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600),
); // a month ago
// 精确时间选项(小时,天,周)
const timeOptions = [
{ key: 'hour', text: '小时', value: 'hour' },
{ key: 'day', text: '天', value: 'day' },
{ key: 'week', text: '周', value: 'week' },
];
const getOptions = async () => {
const res = await API.get('/api/option/');
const { success, message, data } = res.data;
@@ -74,566 +66,74 @@ const OperationSetting = () => {
) {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
newInputs[item.key] = item.value;
if (
item.key.endsWith('Enabled') ||
['DefaultCollapseSidebar'].includes(item.key)
) {
newInputs[item.key] = item.value === 'true' ? true : false;
} else {
newInputs[item.key] = item.value;
}
});
setInputs(newInputs);
setOriginInputs(newInputs);
} else {
showError(message);
}
};
const theme = useTheme();
const isDark = theme === 'dark';
async function onRefresh() {
try {
setLoading(true);
await getOptions();
showSuccess('刷新成功');
} catch (error) {
showError('刷新失败');
} finally {
setLoading(false);
}
}
useEffect(() => {
getOptions().then();
onRefresh();
}, []);
const updateOption = async (key, value) => {
setLoading(true);
if (key.endsWith('Enabled')) {
value = inputs[key] === 'true' ? 'false' : 'true';
}
if (key === 'DefaultCollapseSidebar') {
value = inputs[key] === 'true' ? 'false' : 'true';
}
console.log(key, value);
const res = await API.put('/api/option/', {
key,
value,
});
const { success, message } = res.data;
if (success) {
setInputs((inputs) => ({ ...inputs, [key]: value }));
} else {
showError(message);
}
setLoading(false);
};
const handleInputChange = async (e, { name, value }) => {
if (
name.endsWith('Enabled') ||
name === 'DataExportInterval' ||
name === 'DataExportDefaultTime' ||
name === 'DefaultCollapseSidebar'
) {
if (name === 'DataExportDefaultTime') {
localStorage.setItem('data_export_default_time', value);
} else if (name === 'MjNotifyEnabled') {
localStorage.setItem('mj_notify_enabled', value);
}
await updateOption(name, value);
} else {
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
};
const submitConfig = async (group) => {
switch (group) {
case 'monitor':
if (
originInputs['ChannelDisableThreshold'] !==
inputs.ChannelDisableThreshold
) {
await updateOption(
'ChannelDisableThreshold',
inputs.ChannelDisableThreshold,
);
}
if (
originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
) {
await updateOption(
'QuotaRemindThreshold',
inputs.QuotaRemindThreshold,
);
}
break;
case 'ratio':
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
if (!verifyJSON(inputs.ModelRatio)) {
showError('模型倍率不是合法的 JSON 字符串');
return;
}
await updateOption('ModelRatio', inputs.ModelRatio);
}
if (originInputs['CompletionRatio'] !== inputs.CompletionRatio) {
if (!verifyJSON(inputs.CompletionRatio)) {
showError('模型补全倍率不是合法的 JSON 字符串');
return;
}
await updateOption('CompletionRatio', inputs.CompletionRatio);
}
if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
if (!verifyJSON(inputs.GroupRatio)) {
showError('分组倍率不是合法的 JSON 字符串');
return;
}
await updateOption('GroupRatio', inputs.GroupRatio);
}
if (originInputs['ModelPrice'] !== inputs.ModelPrice) {
if (!verifyJSON(inputs.ModelPrice)) {
showError('模型固定价格不是合法的 JSON 字符串');
return;
}
await updateOption('ModelPrice', inputs.ModelPrice);
}
break;
case 'words':
if (originInputs['SensitiveWords'] !== inputs.SensitiveWords) {
await updateOption('SensitiveWords', inputs.SensitiveWords);
}
break;
case 'quota':
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
}
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
}
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
}
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
}
break;
case 'general':
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
await updateOption('TopUpLink', inputs.TopUpLink);
}
if (originInputs['ChatLink'] !== inputs.ChatLink) {
await updateOption('ChatLink', inputs.ChatLink);
}
if (originInputs['ChatLink2'] !== inputs.ChatLink2) {
await updateOption('ChatLink2', inputs.ChatLink2);
}
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
}
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
await updateOption('RetryTimes', inputs.RetryTimes);
}
break;
}
};
const deleteHistoryLogs = async () => {
console.log(inputs);
const res = await API.delete(
`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`,
);
const { success, message, data } = res.data;
if (success) {
showSuccess(`${data} 条日志已清理!`);
return;
}
showError('日志清理失败:' + message);
};
return (
<Grid columns={1}>
<Grid.Column>
<Form loading={loading} inverted={isDark}>
<Header as='h3' inverted={isDark}>
通用设置
</Header>
<Form.Group widths={4}>
<Form.Input
label='充值链接'
name='TopUpLink'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.TopUpLink}
type='link'
placeholder='例如发卡网站的购买链接'
/>
<Form.Input
label='默认聊天页面链接'
name='ChatLink'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.ChatLink}
type='link'
placeholder='例如 ChatGPT Next Web 的部署地址'
/>
<Form.Input
label='聊天页面2链接'
name='ChatLink2'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.ChatLink2}
type='link'
placeholder='例如 ChatGPT Web & Midjourney 的部署地址'
/>
<Form.Input
label='单位美元额度'
name='QuotaPerUnit'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaPerUnit}
type='number'
step='0.01'
placeholder='一单位货币能兑换的额度'
/>
<Form.Input
label='失败重试次数'
name='RetryTimes'
type={'number'}
step='1'
min='0'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.RetryTimes}
placeholder='失败重试次数'
/>
</Form.Group>
<Form.Group inline>
<Form.Checkbox
checked={inputs.DisplayInCurrencyEnabled === 'true'}
label='以货币形式显示额度'
name='DisplayInCurrencyEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.DisplayTokenStatEnabled === 'true'}
label='Billing 相关 API 显示令牌额度而非用户额度'
name='DisplayTokenStatEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.DefaultCollapseSidebar === 'true'}
label='默认折叠侧边栏'
name='DefaultCollapseSidebar'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button
onClick={() => {
submitConfig('general').then();
}}
>
保存通用设置
</Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
绘图设置
</Header>
<Form.Group inline>
<Form.Checkbox
checked={inputs.DrawingEnabled === 'true'}
label='启用绘图功能'
name='DrawingEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.MjNotifyEnabled === 'true'}
label='允许回调会泄露服务器ip地址'
name='MjNotifyEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.MjAccountFilterEnabled === 'true'}
label='允许AccountFilter参数'
name='MjAccountFilterEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.MjForwardUrlEnabled === 'true'}
label='开启之后将上游地址替换为服务器地址'
name='MjForwardUrlEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.MjModeClearEnabled === 'true'}
label='开启之后会清除用户提示词中的--fast、--relax以及--turbo参数'
name='MjModeClearEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Divider />
<Header as='h3' inverted={isDark}>
屏蔽词过滤设置
</Header>
<Form.Group inline>
<Form.Checkbox
checked={inputs.CheckSensitiveEnabled === 'true'}
label='启用屏蔽词过滤功能'
name='CheckSensitiveEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group inline>
<Form.Checkbox
checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
label='启用prompt检查'
name='CheckSensitiveOnPromptEnabled'
onChange={handleInputChange}
/>
{/*<Form.Checkbox*/}
{/* checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}*/}
{/* label='启用生成内容检查'*/}
{/* name='CheckSensitiveOnCompletionEnabled'*/}
{/* onChange={handleInputChange}*/}
{/*/>*/}
</Form.Group>
{/*<Form.Group inline>*/}
{/* <Form.Checkbox*/}
{/* checked={inputs.StopOnSensitiveEnabled === 'true'}*/}
{/* label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'*/}
{/* name='StopOnSensitiveEnabled'*/}
{/* onChange={handleInputChange}*/}
{/* />*/}
{/*</Form.Group>*/}
{/*<Form.Group>*/}
{/* <Form.Input*/}
{/* label="流模式下缓存队列,默认不缓存,设置越大检测越准确,但是回复会有卡顿感"*/}
{/* name="StreamCacheTextLength"*/}
{/* onChange={handleInputChange}*/}
{/* value={inputs.StreamCacheQueueLength}*/}
{/* type="number"*/}
{/* min="0"*/}
{/* placeholder="例如10"*/}
{/* />*/}
{/*</Form.Group>*/}
<Form.Group widths='equal'>
<Form.TextArea
label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
name='SensitiveWords'
onChange={handleInputChange}
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
value={inputs.SensitiveWords}
placeholder='一行一个屏蔽词'
/>
</Form.Group>
<Form.Button
onClick={() => {
submitConfig('words').then();
}}
>
保存屏蔽词设置
</Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
日志设置
</Header>
<Form.Group inline>
<Form.Checkbox
checked={inputs.LogConsumeEnabled === 'true'}
label='启用额度消费日志记录'
name='LogConsumeEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group widths={4}>
<Form.Input
label='目标时间'
value={historyTimestamp}
type='datetime-local'
name='history_timestamp'
onChange={(e, { name, value }) => {
setHistoryTimestamp(value);
}}
/>
</Form.Group>
<Form.Button
onClick={() => {
deleteHistoryLogs().then();
}}
>
清理历史日志
</Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
数据看板
</Header>
<Form.Checkbox
checked={inputs.DataExportEnabled === 'true'}
label='启用数据看板(实验性)'
name='DataExportEnabled'
onChange={handleInputChange}
/>
<Form.Group>
<Form.Input
label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
name='DataExportInterval'
type={'number'}
step='1'
min='1'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.DataExportInterval}
placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
/>
<Form.Select
label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
options={timeOptions}
name='DataExportDefaultTime'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.DataExportDefaultTime}
placeholder='数据看板默认时间粒度'
/>
</Form.Group>
<Divider />
<Header as='h3' inverted={isDark}>
监控设置
</Header>
<Form.Group widths={3}>
<Form.Input
label='最长响应时间'
name='ChannelDisableThreshold'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.ChannelDisableThreshold}
type='number'
min='0'
placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
/>
<Form.Input
label='额度提醒阈值'
name='QuotaRemindThreshold'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaRemindThreshold}
type='number'
min='0'
placeholder='低于此额度时将发送邮件提醒用户'
/>
</Form.Group>
<Form.Group inline>
<Form.Checkbox
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
label='失败时自动禁用通道'
name='AutomaticDisableChannelEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
label='成功时自动启用通道'
name='AutomaticEnableChannelEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button
onClick={() => {
submitConfig('monitor').then();
}}
>
保存监控设置
</Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
额度设置
</Header>
<Form.Group widths={4}>
<Form.Input
label='新用户初始额度'
name='QuotaForNewUser'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForNewUser}
type='number'
min='0'
placeholder='例如100'
/>
<Form.Input
label='请求预扣费额度'
name='PreConsumedQuota'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.PreConsumedQuota}
type='number'
min='0'
placeholder='请求结束后多退少补'
/>
<Form.Input
label='邀请新用户奖励额度'
name='QuotaForInviter'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForInviter}
type='number'
min='0'
placeholder='例如2000'
/>
<Form.Input
label='新用户使用邀请码奖励额度'
name='QuotaForInvitee'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForInvitee}
type='number'
min='0'
placeholder='例如1000'
/>
</Form.Group>
<Form.Button
onClick={() => {
submitConfig('quota').then();
}}
>
保存额度设置
</Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
倍率设置
</Header>
<Form.Group widths='equal'>
<Form.TextArea
label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
name='ModelPrice'
onChange={handleInputChange}
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
value={inputs.ModelPrice}
placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀'
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.TextArea
label='模型倍率'
name='ModelRatio'
onChange={handleInputChange}
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
value={inputs.ModelRatio}
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.TextArea
label='模型补全倍率(仅对自定义模型有效)'
name='CompletionRatio'
onChange={handleInputChange}
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
value={inputs.CompletionRatio}
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.TextArea
label='分组倍率'
name='GroupRatio'
onChange={handleInputChange}
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
value={inputs.GroupRatio}
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
/>
</Form.Group>
<Form.Button
onClick={() => {
submitConfig('ratio').then();
}}
>
保存倍率设置
</Form.Button>
</Form>
</Grid.Column>
</Grid>
<>
<Spin spinning={loading} size='large'>
{/* 通用设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsGeneral options={inputs} refresh={onRefresh} />
</Card>
{/* 绘图设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsDrawing options={inputs} refresh={onRefresh} />
</Card>
{/* 屏蔽词过滤设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsSensitiveWords options={inputs} refresh={onRefresh} />
</Card>
{/* 日志设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsLog options={inputs} refresh={onRefresh} />
</Card>
{/* 数据看板 */}
<Card style={{ marginTop: '10px' }}>
<SettingsDataDashboard options={inputs} refresh={onRefresh} />
</Card>
{/* 监控设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsMonitoring options={inputs} refresh={onRefresh} />
</Card>
{/* 额度设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
</Card>
{/* 倍率设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsMagnification options={inputs} refresh={onRefresh} />
</Card>
</Spin>
</>
);
};

View File

@@ -36,13 +36,6 @@ export const CHANNEL_OPTIONS = [
color: 'teal',
label: 'Azure OpenAI',
},
{
key: 11,
text: 'Google PaLM2',
value: 11,
color: 'orange',
label: 'Google PaLM2',
},
{
key: 24,
text: 'Google Gemini',
@@ -92,10 +85,18 @@ export const CHANNEL_OPTIONS = [
color: 'purple',
label: '智谱 GLM-4V',
},
{
key: 11,
text: 'Google PaLM2',
value: 11,
color: 'orange',
label: 'Google PaLM2',
},
{ key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
{ key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' },
{ key: 35, text: 'MiniMax', value: 35, color: 'green', label: 'MiniMax' },
{ key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
{
key: 22,

View File

@@ -149,8 +149,9 @@ export function renderModelPrice(
if (completionRatio === undefined) {
completionRatio = 0;
}
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = modelRatio * 2.0 * groupRatio;
let completionRatioPrice = modelRatio * completionRatio * 2.0 * groupRatio;
let completionRatioPrice = modelRatio * 2.0 * completionRatio * groupRatio;
let price =
(inputTokens / 1000000) * inputRatioPrice +
(completionTokens / 1000000) * completionRatioPrice;

View File

@@ -220,3 +220,28 @@ export function shouldShowPrompt(id) {
export function setPromptShown(id) {
localStorage.setItem(`prompt-${id}`, 'true');
}
/**
* 比较两个对象的属性,找出有变化的属性,并返回包含变化属性信息的数组
* @param {Object} oldObject - 旧对象
* @param {Object} newObject - 新对象
* @return {Array} 包含变化属性信息的数组,每个元素是一个对象,包含 key, oldValue 和 newValue
*/
export function compareObjects(oldObject, newObject) {
const changedProperties = [];
// 比较两个对象的属性
for (const key in oldObject) {
if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) {
if (oldObject[key] !== newObject[key]) {
changedProperties.push({
key: key,
oldValue: oldObject[key],
newValue: newObject[key],
});
}
}
}
return changedProperties;
}

View File

@@ -301,17 +301,18 @@ const EditChannel = (props) => {
const addCustomModels = () => {
if (customModel.trim() === '') return;
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
const modelArray = customModel.split(',').map(model => model.trim());
const modelArray = customModel.split(',').map((model) => model.trim());
let localModels = [...inputs.models];
let localModelOptions = [...modelOptions];
let hasError = false;
modelArray.forEach(model => {
modelArray.forEach((model) => {
// 检查模型是否已存在,且模型名称非空
if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表
localModelOptions.push({ // 添加到下拉选项
localModelOptions.push({
// 添加到下拉选项
key: model,
text: model,
value: model,
@@ -330,7 +331,6 @@ const EditChannel = (props) => {
handleInputChange('models', localModels);
};
return (
<>
<SideSheet
@@ -433,11 +433,15 @@ const EditChannel = (props) => {
{inputs.type === 8 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>Base URL</Typography.Text>
<Typography.Text strong>
完整的 Base URL支持变量{'{model}'}
</Typography.Text>
</div>
<Input
name='base_url'
placeholder={'请输入自定义渠道的 Base URL'}
placeholder={
'请输入完整的URL例如https://api.openai.com/v1/chat/completions'
}
onChange={(value) => {
handleInputChange('base_url', value);
}}

View File

@@ -0,0 +1,156 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function SettingsCreditLimit(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
QuotaForNewUser: '',
PreConsumedQuota: '',
QuotaForInviter: '',
QuotaForInvitee: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'额度设置'}>
<Row gutter={16}>
<Col span={6}>
<Form.InputNumber
label={'新用户初始额度'}
field={'QuotaForNewUser'}
step={1}
min={0}
suffix={'Token'}
placeholder={''}
onChange={(value) =>
setInputs({
...inputs,
QuotaForNewUser: String(value),
})
}
/>
</Col>
<Col span={6}>
<Form.InputNumber
label={'请求预扣费额度'}
field={'PreConsumedQuota'}
step={1}
min={0}
suffix={'Token'}
extraText={'请求结束后多退少补'}
placeholder={''}
onChange={(value) =>
setInputs({
...inputs,
PreConsumedQuota: String(value),
})
}
/>
</Col>
<Col span={6}>
<Form.InputNumber
label={'邀请新用户奖励额度'}
field={'QuotaForInviter'}
step={1}
min={0}
suffix={'Token'}
extraText={''}
placeholder={'例如2000'}
onChange={(value) =>
setInputs({
...inputs,
QuotaForInviter: String(value),
})
}
/>
</Col>
<Col span={6}>
<Form.InputNumber
label={'新用户使用邀请码奖励额度'}
field={'QuotaForInvitee'}
step={1}
min={0}
suffix={'Token'}
extraText={''}
placeholder={'例如1000'}
onChange={(value) =>
setInputs({
...inputs,
QuotaForInvitee: String(value),
})
}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存额度设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,147 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function DataDashboard(props) {
const optionsDataExportDefaultTime = [
{ key: 'hour', label: '小时', value: 'hour' },
{ key: 'day', label: '天', value: 'day' },
{ key: 'week', label: '周', value: 'week' },
];
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
DataExportEnabled: false,
DataExportInterval: '',
DataExportDefaultTime: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
localStorage.setItem(
'data_export_default_time',
String(inputs.DataExportDefaultTime),
);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'数据看板设置'}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'DataExportEnabled'}
label={'启用数据看板(实验性)'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
DataExportEnabled: value,
});
}}
/>
</Col>
</Row>
<Row>
<Col span={8}>
<Form.InputNumber
label={'数据看板更新间隔 '}
step={1}
min={1}
suffix={'分钟'}
extraText={'设置过短会影响数据库性能'}
placeholder={'数据看板更新间隔'}
field={'DataExportInterval'}
onChange={(value) =>
setInputs({
...inputs,
DataExportInterval: String(value),
})
}
/>
</Col>
<Col span={8}>
<Form.Select
label='数据看板默认时间粒度'
optionList={optionsDataExportDefaultTime}
field={'DataExportDefaultTime'}
extraText={'仅修改展示粒度,统计精确到小时'}
placeholder={'数据看板默认时间粒度'}
style={{ width: 180 }}
onChange={(value) =>
setInputs({
...inputs,
DataExportDefaultTime: String(value),
})
}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存数据看板设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,170 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function SettingsDrawing(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
DrawingEnabled: false,
MjNotifyEnabled: false,
MjAccountFilterEnabled: false,
MjForwardUrlEnabled: false,
MjModeClearEnabled: false,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
localStorage.setItem('mj_notify_enabled', String(inputs.MjNotifyEnabled));
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'绘图设置'}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'DrawingEnabled'}
label={'启用绘图功能'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
DrawingEnabled: value,
});
}}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'MjNotifyEnabled'}
label={'允许回调(会泄露服务器 IP 地址)'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
MjNotifyEnabled: value,
})
}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'MjAccountFilterEnabled'}
label={'允许 AccountFilter 参数'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
MjAccountFilterEnabled: value,
})
}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'MjForwardUrlEnabled'}
label={'开启之后将上游地址替换为服务器地址'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
MjForwardUrlEnabled: value,
})
}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'MjModeClearEnabled'}
label={
<>
开启之后会清除用户提示词中的 <Tag>--fast</Tag>
<Tag>--relax</Tag> <Tag>--turbo</Tag>
</>
}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
MjModeClearEnabled: value,
})
}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存绘图设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,192 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function GeneralSettings(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
TopUpLink: '',
ChatLink: '',
ChatLink2: '',
QuotaPerUnit: '',
RetryTimes: '',
DisplayInCurrencyEnabled: false,
DisplayTokenStatEnabled: false,
DefaultCollapseSidebar: false,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onChange(value, e) {
const name = e.target.id;
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'通用设置'}>
<Row gutter={16}>
<Col span={8}>
<Form.Input
field={'TopUpLink'}
label={'充值链接'}
initValue={''}
placeholder={'例如发卡网站的购买链接'}
onChange={onChange}
showClear
/>
</Col>
<Col span={8}>
<Form.Input
field={'ChatLink'}
label={'默认聊天页面链接'}
initValue={''}
placeholder='例如 ChatGPT Next Web 的部署地址'
onChange={onChange}
showClear
/>
</Col>
<Col span={8}>
<Form.Input
field={'ChatLink2'}
label={'聊天页面 2 链接'}
initValue={''}
placeholder='例如 ChatGPT Next Web 的部署地址'
onChange={onChange}
showClear
/>
</Col>
<Col span={8}>
<Form.Input
field={'QuotaPerUnit'}
label={'单位美元额度'}
initValue={''}
placeholder='一单位货币能兑换的额度'
onChange={onChange}
showClear
/>
</Col>
<Col span={8}>
<Form.Input
field={'RetryTimes'}
label={'失败重试次数'}
initValue={''}
placeholder='失败重试次数'
onChange={onChange}
showClear
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'DisplayInCurrencyEnabled'}
label={'以货币形式显示额度'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
DisplayInCurrencyEnabled: value,
});
}}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'DisplayTokenStatEnabled'}
label={'Billing 相关 API 显示令牌额度而非用户额度'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
DisplayTokenStatEnabled: value,
})
}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'DefaultCollapseSidebar'}
label={'默认折叠侧边栏'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
DefaultCollapseSidebar: value,
})
}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存通用设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,147 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui';
import dayjs from 'dayjs';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function SettingsLog(props) {
const [loading, setLoading] = useState(false);
const [loadingCleanHistoryLog, setLoadingCleanHistoryLog] = useState(false);
const [inputs, setInputs] = useState({
LogConsumeEnabled: false,
historyTimestamp: dayjs().subtract(1, 'month').toDate(),
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow).filter(
(item) => item.key !== 'historyTimestamp',
);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
async function onCleanHistoryLog() {
try {
setLoadingCleanHistoryLog(true);
if (!inputs.historyTimestamp) throw new Error('请选择日志记录时间');
const res = await API.delete(
`/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
);
const { success, message, data } = res.data;
if (success) {
showSuccess(`${data} 条日志已清理!`);
return;
} else {
throw new Error('日志清理失败:' + message);
}
} catch (error) {
showError(error.message);
} finally {
setLoadingCleanHistoryLog(false);
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
currentInputs['historyTimestamp'] = inputs.historyTimestamp;
setInputs(Object.assign(inputs, currentInputs));
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'日志设置'}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'LogConsumeEnabled'}
label={'启用额度消费日志记录'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
LogConsumeEnabled: value,
});
}}
/>
</Col>
<Col span={8}>
<Spin spinning={loadingCleanHistoryLog}>
<Form.DatePicker
label='日志记录时间'
field={'historyTimestamp'}
type='dateTime'
inputReadOnly={true}
onChange={(value) => {
setInputs({
...inputs,
historyTimestamp: value,
});
}}
/>
<Button size='default' onClick={onCleanHistoryLog}>
清除历史日志
</Button>
</Spin>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存日志设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,194 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
export default function SettingsMagnification(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ModelPrice: '',
ModelRatio: '',
CompletionRatio: '',
GroupRatio: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
async function onSubmit() {
try {
await refForm.current.validate();
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
} catch (error) {
showError('请检查输入');
console.error(error);
} finally {
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'倍率设置'}>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'模型固定价格'}
extraText={'一次调用消耗多少刀,优先级大于模型倍率'}
placeholder={
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀'
}
field={'ModelPrice'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({
...inputs,
ModelPrice: value,
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'模型倍率'}
extraText={''}
placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
field={'ModelRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({
...inputs,
ModelRatio: value,
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'模型补全倍率(仅对自定义模型有效)'}
extraText={'仅对自定义模型有效'}
placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
field={'CompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({
...inputs,
CompletionRatio: value,
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'分组倍率'}
extraText={''}
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
field={'GroupRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({
...inputs,
GroupRatio: value,
})
}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存倍率设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,154 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function SettingsMonitoring(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ChannelDisableThreshold: '',
QuotaRemindThreshold: '',
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'监控设置'}>
<Row gutter={16}>
<Col span={8}>
<Form.InputNumber
label={'最长响应时间'}
step={1}
min={0}
suffix={'秒'}
extraText={'当运行通道全部测试时,超过此时间将自动禁用通道'}
placeholder={''}
field={'ChannelDisableThreshold'}
onChange={(value) =>
setInputs({
...inputs,
ChannelDisableThreshold: String(value),
})
}
/>
</Col>
<Col span={8}>
<Form.InputNumber
label={'额度提醒阈值'}
step={1}
min={0}
suffix={'Token'}
extraText={'低于此额度时将发送邮件提醒用户'}
placeholder={''}
field={'QuotaRemindThreshold'}
onChange={(value) =>
setInputs({
...inputs,
QuotaRemindThreshold: String(value),
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'AutomaticDisableChannelEnabled'}
label={'失败时自动禁用通道'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
AutomaticDisableChannelEnabled: value,
});
}}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'AutomaticEnableChannelEnabled'}
label={'成功时自动启用通道'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
AutomaticEnableChannelEnabled: value,
})
}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存监控设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -0,0 +1,135 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
export default function SettingsSensitiveWords(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
CheckSensitiveEnabled: false,
CheckSensitiveOnPromptEnabled: false,
SensitiveWords: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试');
}
showSuccess('保存成功');
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'屏蔽词过滤设置'}>
<Row gutter={16}>
<Col span={8}>
<Form.Switch
field={'CheckSensitiveEnabled'}
label={'启用屏蔽词过滤功能'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
CheckSensitiveEnabled: value,
});
}}
/>
</Col>
<Col span={8}>
<Form.Switch
field={'CheckSensitiveOnPromptEnabled'}
label={'启用 Prompt 检查'}
size='large'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
CheckSensitiveOnPromptEnabled: value,
})
}
/>
</Col>
</Row>
<Row>
<Col span={16}>
<Form.TextArea
label={'屏蔽词列表'}
extraText={'一行一个屏蔽词,不需要符号分割'}
placeholder={'一行一个屏蔽词,不需要符号分割'}
field={'SensitiveWords'}
onChange={(value) =>
setInputs({
...inputs,
SensitiveWords: value,
})
}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
/>
</Col>
</Row>
<Row>
<Button size='large' onClick={onSubmit}>
保存屏蔽词过滤设置
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}