mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
feat: charge alpha
This commit is contained in:
parent
fa9f7da55e
commit
035e71b2f2
29
README.md
29
README.md
@ -9,7 +9,7 @@
|
||||
🚀 Powerful and beautiful **AI Aggregation** chat platform
|
||||
|
||||
|
||||
[官网](https://chatnio.net) | [开放文档](https://docs.chatnio.net) | [SDKs](https://docs.chatnio.net/kuai-su-kai-shi) | [QQ 群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=YKcvGGlM03LYWlPk-iosqAqL4qHwOtBx&authKey=6cjCqRNKNuOUJltyo%2FNgmKm%2BS%2FUCtAyVHCnirHyxNuxGExUHsJRtlSaW1EeDxhNx&noverify=0&group_code=565902327)
|
||||
[官网](https://chatnio.net) | [开放文档](https://docs.chatnio.net) | [SDKs](https://docs.chatnio.net/kuai-su-kai-shi) | [QQ 群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=1mv1Y8SyxnQVvQCoqhmIgVTbwQmkNmvQ&authKey=5KUA9nJPR29nQwjbsYNknN2Fj6cKePkRes%2B1QZy84Dr4GHYVzcvb0yklxiMMNVJN&noverify=0&group_code=749482576)
|
||||
|
||||
[](https://stats.deeptrain.net)
|
||||
|
||||
@ -38,24 +38,26 @@
|
||||
- 🎁 Image generation
|
||||
11. 🔔 PWA 应用
|
||||
- 🔔 PWA application
|
||||
12. ⚡ Token 计费系统
|
||||
- ⚡ Token billing system
|
||||
13. 📚 逆向工程模型支持
|
||||
12. 📚 逆向工程模型支持
|
||||
- 📚 Reverse engineering model support
|
||||
14. 🌏 国际化支持
|
||||
13. 🌏 国际化支持
|
||||
- 🌏 Internationalization support
|
||||
- 🇨🇳 简体中文
|
||||
- 🇺🇸 English
|
||||
- 🇷🇺 Русский
|
||||
15. 🍎 主题切换
|
||||
14. 🍎 主题切换
|
||||
- 🍎 Theme switching
|
||||
16. 🥪 Key 中转服务
|
||||
15. 🥪 Key 中转服务
|
||||
- 🥪 Key relay service
|
||||
17. 🔨 多模型支持
|
||||
16. 🔨 多模型支持
|
||||
- 🔨 Multi-model support
|
||||
18. ⚙ 后台管理系统
|
||||
- ⚙ Admin system
|
||||
19. 📂 文件上传功能 (支持 pdf, docx, pptx, xlsx, 音频, 图片等)
|
||||
17. ⚙ 后台管理系统 (仪表盘,用户管理,公告管理等)
|
||||
- ⚙ Admin system (dashboard, user management, announcement management, etc.)
|
||||
18. ⚒ 渠道管理 (多账号均衡负载,优先级调配,权重负载,模型映射,渠道状态管理)
|
||||
- ⚒ Channel management (multi-account load balancing, priority allocation, weight load, model mapping, channel status management)
|
||||
19. ⚡ 计费系统 (支持匿名计费,按次数计费,Token 弹性计费等方式)
|
||||
- ⚡ Billing system (support anonymous billing, billing by number of times, Token billing, etc.)
|
||||
20. 📂 文件上传功能 (支持 pdf, docx, pptx, xlsx, 音频, 图片等)
|
||||
- 📂 File upload function (support pdf, docx, pptx, xlsx, audio, images, etc.)
|
||||
|
||||
|
||||
@ -185,11 +187,6 @@ Replace `https://api.openai.com` with `https://api.chatnio.net` and fill in the
|
||||
- 应用技术: PWA + HTTP2 + WebSocket + Stream Buffer
|
||||
|
||||
|
||||
## 🎈 感谢 | Thanks
|
||||
感谢这些开源项目提供的思路:
|
||||
- ChatGPT 逆向工程: [go-chatgpt-api](https://github.com/linweiyuan/go-chatgpt-api)
|
||||
- New Bing 逆向工程: [EdgeGPT](https://github.com/acheong08/EdgeGPT)
|
||||
|
||||
## 🎃 开发团队 | Team
|
||||
- [@ProgramZmh](https://github.com/zmh-program) (全栈开发)
|
||||
- [@Sh1n3zz](https://github.com/sh1n3zz) (全栈开发)
|
||||
|
@ -54,7 +54,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
||||
Message: props.Message,
|
||||
Token: utils.Multi(
|
||||
props.Token == 0,
|
||||
utils.Multi(globals.IsFreeModel(model) && !props.Plan, utils.ToPtr(2500), nil),
|
||||
utils.Multi(props.Infinity || props.Plan, nil, utils.ToPtr(2500)),
|
||||
&props.Token,
|
||||
),
|
||||
PresencePenalty: props.PresencePenalty,
|
||||
|
@ -10,14 +10,11 @@ import (
|
||||
|
||||
type Hook func(message []globals.Message, token int) (string, error)
|
||||
|
||||
func ChatWithWeb(message []globals.Message, long bool) []globals.Message {
|
||||
data := SearchWebResult(GetPointByLatestMessage(message))
|
||||
func ChatWithWeb(message []globals.Message) []globals.Message {
|
||||
data := utils.GetSegmentString(
|
||||
SearchWebResult(GetPointByLatestMessage(message)), 2048,
|
||||
)
|
||||
|
||||
if long {
|
||||
data = utils.GetSegmentString(data, 6000)
|
||||
} else {
|
||||
data = utils.GetSegmentString(data, 3000)
|
||||
}
|
||||
return utils.Insert(message, 0, globals.Message{
|
||||
Role: globals.System,
|
||||
Content: fmt.Sprintf("You will play the role of an AI Q&A assistant, where your knowledge base is not offline, but can be networked in real time, and you can provide real-time networked information with links to networked search sources."+
|
||||
|
@ -9,7 +9,7 @@ func UsingWebSegment(instance *conversation.Conversation) []globals.Message {
|
||||
segment := conversation.CopyMessage(instance.GetChatMessage())
|
||||
|
||||
if instance.IsEnableWeb() {
|
||||
segment = ChatWithWeb(segment, globals.IsLongContextModel(instance.GetModel()))
|
||||
segment = ChatWithWeb(segment)
|
||||
}
|
||||
|
||||
return segment
|
||||
@ -17,7 +17,7 @@ func UsingWebSegment(instance *conversation.Conversation) []globals.Message {
|
||||
|
||||
func UsingWebNativeSegment(enable bool, message []globals.Message) []globals.Message {
|
||||
if enable {
|
||||
return ChatWithWeb(message, false)
|
||||
return ChatWithWeb(message)
|
||||
} else {
|
||||
return message
|
||||
}
|
||||
|
@ -63,4 +63,5 @@ type UserData struct {
|
||||
IsSubscribed bool `json:"is_subscribed"`
|
||||
TotalMonth int64 `json:"total_month"`
|
||||
Enterprise bool `json:"enterprise"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
|
||||
SELECT
|
||||
auth.id, auth.username, auth.is_admin,
|
||||
quota.quota, quota.used,
|
||||
subscription.expired_at, subscription.total_month, subscription.enterprise
|
||||
subscription.expired_at, subscription.total_month, subscription.enterprise, subscription.level
|
||||
FROM auth
|
||||
LEFT JOIN quota ON quota.user_id = auth.id
|
||||
LEFT JOIN subscription ON subscription.user_id = auth.id
|
||||
@ -44,13 +44,14 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
|
||||
for rows.Next() {
|
||||
var user UserData
|
||||
var (
|
||||
expired []uint8
|
||||
quota sql.NullFloat64
|
||||
usedQuota sql.NullFloat64
|
||||
totalMonth sql.NullInt64
|
||||
isEnterprise sql.NullBool
|
||||
expired []uint8
|
||||
quota sql.NullFloat64
|
||||
usedQuota sql.NullFloat64
|
||||
totalMonth sql.NullInt64
|
||||
isEnterprise sql.NullBool
|
||||
subscriptionLevel sql.NullInt64
|
||||
)
|
||||
if err := rows.Scan(&user.Id, &user.Username, &user.IsAdmin, "a, &usedQuota, &expired, &totalMonth, &isEnterprise); err != nil {
|
||||
if err := rows.Scan(&user.Id, &user.Username, &user.IsAdmin, "a, &usedQuota, &expired, &totalMonth, &isEnterprise, &subscriptionLevel); err != nil {
|
||||
return PaginationForm{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
@ -65,6 +66,9 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
|
||||
if totalMonth.Valid {
|
||||
user.TotalMonth = totalMonth.Int64
|
||||
}
|
||||
if subscriptionLevel.Valid {
|
||||
user.Level = int(subscriptionLevel.Int64)
|
||||
}
|
||||
stamp := utils.ConvertTime(expired)
|
||||
if stamp != nil {
|
||||
user.IsSubscribed = stamp.After(time.Now())
|
||||
|
@ -11,6 +11,6 @@
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "src/components/ui/lib/utils"
|
||||
"utils": "@/components/ui/lib/utils"
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
|
33
app/pnpm-lock.yaml
generated
33
app/pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ dependencies:
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-radio-group':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-scroll-area':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -1213,6 +1216,36 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@types/react': 18.2.33
|
||||
'@types/react-dom': 18.2.14
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
|
||||
peerDependencies:
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ChannelCommonResponse } from "@/admin/api/channel.ts";
|
||||
|
||||
export type Channel = {
|
||||
id: number;
|
||||
name: string;
|
||||
@ -175,15 +173,3 @@ export function getChannelType(type?: string): string {
|
||||
if (type && type in ChannelTypes) return ChannelTypes[type];
|
||||
return ChannelTypes.openai;
|
||||
}
|
||||
|
||||
export function toastState(
|
||||
toast: any,
|
||||
t: any,
|
||||
state: ChannelCommonResponse,
|
||||
toastSuccess?: boolean,
|
||||
) {
|
||||
if (state.status)
|
||||
toastSuccess &&
|
||||
toast({ title: t("success"), description: t("request-success") });
|
||||
else toast({ title: t("error"), description: state.error });
|
||||
}
|
||||
|
15
app/src/admin/charge.ts
Normal file
15
app/src/admin/charge.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const tokenBilling = "token-billing";
|
||||
export const timesBilling = "times-billing";
|
||||
export const nonBilling = "non-billing";
|
||||
|
||||
export const defaultChargeType = tokenBilling;
|
||||
export const chargeTypes = [nonBilling, timesBilling, tokenBilling];
|
||||
|
||||
export type ChargeProps = {
|
||||
id: number;
|
||||
models: string[];
|
||||
type: string;
|
||||
anonymous: boolean;
|
||||
input: number;
|
||||
output: number;
|
||||
};
|
@ -66,6 +66,7 @@ export type UserData = {
|
||||
used_quota: number;
|
||||
is_subscribed: boolean;
|
||||
total_month: number;
|
||||
level: number;
|
||||
enterprise: boolean;
|
||||
};
|
||||
|
||||
|
16
app/src/admin/utils.ts
Normal file
16
app/src/admin/utils.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export type CommonResponse = {
|
||||
status: boolean;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export function toastState(
|
||||
toast: any,
|
||||
t: any,
|
||||
state: CommonResponse,
|
||||
toastSuccess?: boolean,
|
||||
) {
|
||||
if (state.status)
|
||||
toastSuccess &&
|
||||
toast({ title: t("success"), description: t("request-success") });
|
||||
else toast({ title: t("error"), description: state.error });
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
@import "management";
|
||||
@import "broadcast";
|
||||
@import "channel";
|
||||
@import "charge";
|
||||
|
||||
.admin-page {
|
||||
position: relative;
|
||||
|
@ -1,6 +1,6 @@
|
||||
.broadcast {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: max-content;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1,6 +1,6 @@
|
||||
.channel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: max-content;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -10,6 +10,17 @@
|
||||
height: 100%;
|
||||
min-height: 20vh;
|
||||
}
|
||||
|
||||
.channel-table {
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--border-hover));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-wrapper {
|
||||
|
46
app/src/assets/admin/charge.less
Normal file
46
app/src/assets/admin/charge.less
Normal file
@ -0,0 +1,46 @@
|
||||
.charge {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.charge-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 20vh;
|
||||
}
|
||||
}
|
||||
|
||||
.charge-widget {
|
||||
height: max-content;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charge-editor {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
|
||||
.token {
|
||||
color: hsl(var(--text-secondary));
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.charge-table {
|
||||
padding-bottom: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
}
|
@ -45,8 +45,9 @@
|
||||
height: max-content;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius);
|
||||
background: hsl(var(--background-container));
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
|
||||
user-select: none;
|
||||
max-width: 460px;
|
||||
|
||||
@ -113,10 +114,10 @@
|
||||
padding: 1rem 2rem;
|
||||
margin: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
background: hsl(var(--background-container));
|
||||
background: hsl(var(--background));
|
||||
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
|
||||
border: 1px solid hsl(var(--border));
|
||||
user-select: none;
|
||||
box-shadow: 0 0 1rem 0 hsla(var(--foreground), 0.1);
|
||||
|
||||
@media (max-width: 680px) {
|
||||
width: 100%;
|
||||
|
@ -2,8 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 56px);
|
||||
height: calc(100vh - 56px);
|
||||
padding: 2rem;
|
||||
|
||||
& > * {
|
||||
|
@ -4,9 +4,9 @@
|
||||
flex-direction: column;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding: 0.75rem 0;
|
||||
margin: 0;
|
||||
background: var(--background-sidebar);
|
||||
background: hsl(var(--background));
|
||||
transition: 0.225s ease-in-out;
|
||||
min-height: calc(100vh - 56px);
|
||||
transition-property: width, background, box-shadow, opacity;
|
||||
@ -28,13 +28,13 @@
|
||||
height: max-content;
|
||||
padding: 0.75rem 1rem;
|
||||
align-items: center;
|
||||
background: var(--conversation-card);
|
||||
margin: 0.75rem 0.75rem -0.25rem;
|
||||
margin: 0 0.75rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease-in-out;
|
||||
border-radius: var(--radius);
|
||||
font-size: 16px;
|
||||
color: hsl(var(--text-secondary));
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
@ -50,16 +50,16 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--conversation-card-hover);
|
||||
background: hsl(var(--card-hover));
|
||||
|
||||
&:before {
|
||||
opacity: .25;
|
||||
opacity: .05;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--conversation-card-active);
|
||||
color: hsl(var(--text));
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
@ -92,14 +92,9 @@
|
||||
|
||||
.admin-content {
|
||||
flex-grow: 1;
|
||||
height: max-content;
|
||||
min-height: calc(100vh - 56px);
|
||||
height: calc(100vh - 56px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
background: hsla(var(--background-container));
|
||||
}
|
||||
|
@ -4,57 +4,59 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 71 65% 97%;
|
||||
--background-hover: 56 43% 97%;
|
||||
--background-container: 71 65% 97%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--background: 0 0% 100%;
|
||||
--background-hover: 5 12% 100%;
|
||||
--background-container: 0, 0%, 97%, 0.8;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-hover: 0 0% 97.5%;
|
||||
--card-active: 0 0% 95%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--muted: 60 26% 92%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--accent: 37 26% 90%;
|
||||
--accent-secondary: 37 26% 95%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-secondary: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 67.22% 50.59%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--border-hover: 240 5.9% 85%;
|
||||
--border-active: 240 5.9% 80%;
|
||||
|
||||
--input: 240 5.9% 90%;
|
||||
--input-unread: 240 5.9% 50%;
|
||||
--ring: 240 5% 64.9%;
|
||||
|
||||
--border: 37 26% 83%;
|
||||
--border-hover: 37 26% 78%;
|
||||
--border-active: 37 26% 73%;
|
||||
--input: 37 26% 90%;
|
||||
--input-unread: 37 26% 70%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--text: 0 0% 0%;
|
||||
--text-dark: 0 0% 100%;
|
||||
--text-secondary: 0 0% 20%;
|
||||
--text-secondary-dark: 0 0% 80%;
|
||||
|
||||
--selection: 212 100% 41%;
|
||||
--selection-foreground: 0 0% 98%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--conversation-card: rgba(222,214,200,.05);
|
||||
--conversation-card-hover: rgba(222,214,200,.2);
|
||||
--conversation-card-active: rgba(222,214,200,.3);
|
||||
--conversation-card-border: rgba(222,214,200,.5);
|
||||
--shadow: #00000005;
|
||||
|
||||
--background-sidebar: rgba(240, 242, 231, 0.25);
|
||||
--chat-bot-background: rgba(88, 166, 255, .04);
|
||||
--chat-bot-border: rgba(88, 166, 255, .12);
|
||||
--chat-bot-border-hover: rgba(88, 166, 255, .24);
|
||||
|
||||
--model-card-background: rgba(222,214,200,.2);
|
||||
--assistant-background: hsla(218, 100%, 64%, .04);
|
||||
--assistant-border: hsla(218, 100%, 64%, .12);
|
||||
--assistant-border-hover: hsla(218, 100%, 64%, .24);
|
||||
--assistant-shadow: hsla(218, 100%, 64%, .03);
|
||||
|
||||
--gold: 45 100% 50%;
|
||||
}
|
||||
@ -62,10 +64,12 @@
|
||||
.dark {
|
||||
--background: 0 0% 0%;
|
||||
--background-hover: 0 0% 7.8%;
|
||||
--background-container: 0 0% 7.8%;
|
||||
--background-container: 0, 0%, 7.8%, 0.8;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-hover: 240 10% 8.9%;
|
||||
--card-active: 240 10% 13.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
@ -81,7 +85,7 @@
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-secondary: 240 3.7% 15.9%;
|
||||
--accent-secondary: 240 5% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
@ -96,17 +100,12 @@
|
||||
--text: 0 0% 100%;
|
||||
--text-secondary: 0 0% 80%;
|
||||
|
||||
--conversation-card: rgba(152,153,165,.05);
|
||||
--conversation-card-hover: rgba(152,153,165,.2);
|
||||
--conversation-card-active: rgba(152,153,165,.3);
|
||||
--conversation-card-border: rgba(152,153,165,.1);
|
||||
--background-sidebar: rgba(0, 0, 0, 0.25);
|
||||
--shadow: #ffffff05;
|
||||
|
||||
--chat-bot-background: rgba(88, 166, 255, .1);
|
||||
--chat-bot-border: rgba(88, 166, 255, .2);
|
||||
--chat-bot-border-hover: rgba(88, 166, 255, .25);
|
||||
|
||||
--model-card-background: rgba(152,153,165,.05);
|
||||
--assistant-background: hsla(218, 100%, 64%, .1);
|
||||
--assistant-border: hsla(218, 100%, 64%, .2);
|
||||
--assistant-border-hover: hsla(218, 100%, 64%, .25);
|
||||
--assistant-shadow: hsla(218, 100%, 64%, .05);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,7 +147,7 @@ strong {
|
||||
height: max-content;
|
||||
min-height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
padding: 2.5rem 5rem;
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
@ -11,7 +11,7 @@
|
||||
margin-bottom: 4px !important;
|
||||
padding: 6px 12px !important;
|
||||
border: 1px solid hsl(var(--border-hover));
|
||||
background: hsl(var(--background-container)) !important;
|
||||
background: hsla(var(--background-container)) !important;
|
||||
border-radius: var(--radius);
|
||||
|
||||
@media (max-width: 668px) {
|
||||
|
@ -2,5 +2,5 @@
|
||||
width: 100%;
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
}
|
||||
|
@ -144,16 +144,28 @@
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&.user {
|
||||
.message-content {
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--border-hover));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.assistant,
|
||||
&.system {
|
||||
align-items: flex-start;
|
||||
|
||||
.message-content {
|
||||
background: var(--chat-bot-background);
|
||||
border: 1px solid var(--chat-bot-border);
|
||||
background: var(--assistant-background);
|
||||
border: 1px solid var(--assistant-border);
|
||||
box-shadow: 0.25rem 0.25rem 1rem 0 var(--assistant-shadow);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--chat-bot-border-hover);
|
||||
border-color: var(--assistant-border-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@
|
||||
color: hsl(var(--text));
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
|
||||
&:hover {
|
||||
border: 1px solid hsl(var(--border-hover));
|
||||
|
@ -5,7 +5,7 @@
|
||||
width: 100%;
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
}
|
||||
|
||||
.model-market {
|
||||
@ -126,7 +126,7 @@
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: var(--model-card-background);
|
||||
background: hsl(var(--background));
|
||||
transition: 0.5s;
|
||||
z-index: -1;
|
||||
border-radius: var(--radius);
|
||||
@ -277,7 +277,7 @@
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--background-sidebar);
|
||||
background: hsl(var(--background));
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: width, background, box-shadow, border-right, opacity;
|
||||
border-right: 0;
|
||||
@ -381,9 +381,9 @@
|
||||
margin: 0 6px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--conversation-card-border);
|
||||
border: 1px solid hsl(var(--border));
|
||||
transition: 0.2s ease-in-out;
|
||||
background: var(--conversation-card);
|
||||
background: hsl(var(--card));
|
||||
|
||||
.more {
|
||||
color: hsl(var(--text-secondary));
|
||||
@ -399,7 +399,8 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--conversation-card-hover);
|
||||
background: hsl(var(--card-hover));
|
||||
border-color: hsl(var(--border-hover));
|
||||
|
||||
.id {
|
||||
display: none;
|
||||
@ -412,7 +413,8 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--conversation-card-active);
|
||||
background: hsl(var(--card-hover));
|
||||
border-color: hsl(var(--border-active));
|
||||
}
|
||||
}
|
||||
|
||||
@ -508,7 +510,7 @@
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
transition: width 0.2s ease-in-out;
|
||||
|
||||
.chat-wrapper {
|
||||
|
@ -114,7 +114,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
border-radius: var(--radius);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
@ -58,7 +58,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--background-container));
|
||||
background: hsla(var(--background-container));
|
||||
color: hsl(var(--text));
|
||||
padding: 0.85rem 1rem 0.75rem;
|
||||
align-items: center;
|
||||
|
@ -30,24 +30,12 @@
|
||||
color: hsl(var(--text));
|
||||
|
||||
&:hover {
|
||||
background: hsl(var(--accent));
|
||||
background: hsl(var(--accent-secondary));
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: hsl(var(--text));
|
||||
border-color: hsl(var(--border-hover));
|
||||
color: hsl(var(--background));
|
||||
|
||||
.select-element.badge {
|
||||
&.badge-default {
|
||||
background: hsl(var(--background)) !important;
|
||||
color: hsl(var(--text));
|
||||
|
||||
&:hover {
|
||||
background: hsl(var(--background)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
background: hsl(var(--accent));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,3 +111,15 @@ input[type="number"] {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selection {
|
||||
::selection {
|
||||
color: hsl(var(--selection-foreground));
|
||||
background: hsl(var(--selection));
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
color: hsl(var(--selection-foreground));
|
||||
background: hsl(var(--selection));
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={`mx-1 w-8 h-8`}
|
||||
className={`w-8 h-8`}
|
||||
variant={variant}
|
||||
>
|
||||
{children}
|
||||
@ -56,7 +56,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
||||
) : (
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={`mx-1 w-8 h-8`}
|
||||
className={`w-8 h-8`}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
>
|
||||
|
367
app/src/components/admin/ChargeWidget.tsx
Normal file
367
app/src/components/admin/ChargeWidget.tsx
Normal file
@ -0,0 +1,367 @@
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import {
|
||||
ChargeProps,
|
||||
chargeTypes,
|
||||
defaultChargeType, nonBilling, timesBilling, tokenBilling,
|
||||
} from "@/admin/charge.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import {useMemo, useReducer, useState} from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import {
|
||||
Cloud,
|
||||
DownloadCloud,
|
||||
Eraser,
|
||||
EyeOff,
|
||||
Minus,
|
||||
Plus, RotateCw,
|
||||
Search,
|
||||
Settings2, Trash,
|
||||
UploadCloud,
|
||||
} from "lucide-react";
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu.tsx";
|
||||
import {Command, CommandInput, CommandItem, CommandList} from "@/components/ui/command.tsx";
|
||||
import {ChannelModels} from "@/admin/channel.ts";
|
||||
import {toastState} from "@/admin/utils.ts";
|
||||
import {Switch} from "@/components/ui/switch.tsx";
|
||||
import {NumberInput} from "@/components/ui/number-input.tsx";
|
||||
import {Table, TableBody, TableCell, TableHeader, TableRow} from "@/components/ui/table.tsx";
|
||||
import OperationAction from "@/components/OperationAction.tsx";
|
||||
import {Badge} from "@/components/ui/badge.tsx";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
const initialState: ChargeProps = {
|
||||
id: 0,
|
||||
type: defaultChargeType,
|
||||
models: [],
|
||||
anonymous: false,
|
||||
input: 0,
|
||||
output: 0,
|
||||
};
|
||||
|
||||
function reducer(state: ChargeProps, action: any): ChargeProps {
|
||||
switch (action.type) {
|
||||
case "set-models":
|
||||
return { ...state, models: action.payload };
|
||||
case "add-model":
|
||||
const model = action.payload.trim();
|
||||
if (model.length === 0 || state.models.includes(model)) return state;
|
||||
return { ...state, models: [...state.models, model] };
|
||||
case "remove-model":
|
||||
return {
|
||||
...state,
|
||||
models: state.models.filter((model) => model !== action.payload),
|
||||
};
|
||||
case "set-type":
|
||||
return { ...state, type: action.payload };
|
||||
case "set-anonymous":
|
||||
return { ...state, anonymous: action.payload };
|
||||
case "set-input":
|
||||
return { ...state, input: action.payload };
|
||||
case "set-output":
|
||||
return { ...state, output: action.payload };
|
||||
case "clear":
|
||||
return initialState;
|
||||
case "clear-param":
|
||||
return { ...initialState, id: state.id };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function preflight(state: ChargeProps): ChargeProps {
|
||||
state.models = state.models.map((model) => model.trim()).filter((model) => model.length > 0);
|
||||
switch (state.type) {
|
||||
case nonBilling:
|
||||
state.input = 0;
|
||||
state.output = 0;
|
||||
break;
|
||||
case timesBilling:
|
||||
state.input = 0;
|
||||
state.anonymous = false;
|
||||
break;
|
||||
case tokenBilling:
|
||||
state.anonymous = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.input < 0) state.input = 0;
|
||||
if (state.output < 0) state.output = 0;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function ChargeEditor() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [model, setModel] = useState("");
|
||||
const [form, dispatch] = useReducer(reducer, initialState);
|
||||
const unusedModels = useMemo(() => {
|
||||
return ChannelModels.filter(
|
||||
(model) => !form.models.includes(model) && model !== "",
|
||||
);
|
||||
}, [form.models]);
|
||||
|
||||
async function post() {
|
||||
const data = preflight({ ...form });
|
||||
console.log(data);
|
||||
toastState(toast, t, {});
|
||||
dispatch({ type: "clear" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`charge-editor`}>
|
||||
<div className={`w-full h-max mb-5`}>
|
||||
<RadioGroup
|
||||
value={form.type}
|
||||
onValueChange={(value) =>
|
||||
dispatch({ type: "set-type", payload: value })
|
||||
}
|
||||
className={`flex flex-row gap-5 whitespace-nowrap flex-wrap`}
|
||||
>
|
||||
{chargeTypes.map((chargeType, index) => (
|
||||
<div
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
key={index}
|
||||
>
|
||||
<RadioGroupItem
|
||||
className={`transition-all duration-200`}
|
||||
value={chargeType}
|
||||
id={chargeType}
|
||||
/>
|
||||
<Label htmlFor={chargeType} className={`cursor-pointer`}>
|
||||
{t(`admin.charge.${chargeType}`)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className={`flex flex-row w-full h-max mb-4`}>
|
||||
<Button
|
||||
onClick={() => dispatch({ type: "add-model", payload: model })}
|
||||
size={`icon`}
|
||||
className={`mr-2 shrink-0`}
|
||||
>
|
||||
<Plus className={`w-4 h-4`} />
|
||||
</Button>
|
||||
<Input
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder={t("admin.channels.model")}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size={`icon`} className={`ml-2 shrink-0`}>
|
||||
<Search className={`w-4 h-4`} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={`end`} asChild>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("admin.channels.search-model")}
|
||||
/>
|
||||
<CommandList className={`thin-scrollbar`}>
|
||||
{unusedModels.map((model, idx) => (
|
||||
<CommandItem
|
||||
key={idx}
|
||||
value={model}
|
||||
onSelect={(value) => dispatch({ type: "add-model", payload: value })}
|
||||
className={`px-2`}
|
||||
>
|
||||
{model}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className={`flex flex-col w-full h-max mb-2`}>
|
||||
{form.models.map((model, index) => (
|
||||
<div className={`flex flex-row w-full h-max shrink-0 mb-2 select-none`} key={index}>
|
||||
<Input value={model} readOnly />
|
||||
<Button
|
||||
onClick={() => dispatch({ type: "remove-model", payload: model })}
|
||||
size={`icon`} variant={`outline`}
|
||||
className={`ml-2 shrink-0`}
|
||||
>
|
||||
<Minus className={`w-4 h-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{
|
||||
form.type === nonBilling && (
|
||||
<div className={`flex flex-row w-full h-max items-center mt-4 mb-6`}>
|
||||
<EyeOff className={`w-4 h-4 mr-2`} />
|
||||
<Label className={`grow`}>{t("admin.charge.anonymous")}</Label>
|
||||
<Switch
|
||||
checked={form.anonymous}
|
||||
onCheckedChange={(checked) => dispatch({ type: "set-anonymous", payload: checked })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
form.type === timesBilling && (
|
||||
<div className={`flex flex-row w-full h-max items-center`}>
|
||||
<Cloud className={`w-4 h-4 mr-2`} />
|
||||
<Label className={`grow`}>{t("admin.charge.time-count")}</Label>
|
||||
<NumberInput
|
||||
value={form.output}
|
||||
onValueChange={(value) => dispatch({ type: "set-output", payload: value })}
|
||||
acceptNegative={false}
|
||||
className={`w-20`}
|
||||
min={0}
|
||||
max={99999}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
form.type === tokenBilling && (
|
||||
<div className={`flex flex-col w-full h-max gap-2`}>
|
||||
<div className={`flex flex-row w-full h-max items-center`}>
|
||||
<UploadCloud className={`w-4 h-4 mr-2`} />
|
||||
<Label className={`grow`}>
|
||||
{t("admin.charge.input-count")}
|
||||
<span className={`token`}> / 1k tokens</span>
|
||||
</Label>
|
||||
<NumberInput
|
||||
value={form.input}
|
||||
onValueChange={(value) => dispatch({ type: "set-input", payload: value })}
|
||||
acceptNegative={false}
|
||||
className={`w-20`}
|
||||
min={0}
|
||||
max={99999}
|
||||
/>
|
||||
</div>
|
||||
<div className={`flex flex-row w-full h-max items-center`}>
|
||||
<DownloadCloud className={`w-4 h-4 mr-2`} />
|
||||
<Label className={`grow`}>
|
||||
{t("admin.charge.output-count")}
|
||||
<span className={`token`}> / 1k tokens</span>
|
||||
</Label>
|
||||
<NumberInput
|
||||
value={form.output}
|
||||
onValueChange={(value) => dispatch({ type: "set-output", payload: value })}
|
||||
acceptNegative={false}
|
||||
className={`w-20`}
|
||||
min={0}
|
||||
max={99999}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={`flex flex-row w-full h-max mt-5 gap-2`}>
|
||||
<div className={`grow`} />
|
||||
<Button variant={`outline`} size={`icon`} onClick={() => dispatch({ type: "clear-param" })}>
|
||||
<Eraser className={`w-4 h-4`} />
|
||||
</Button>
|
||||
<Button onClick={post}>
|
||||
<Plus className={`w-4 h-4 mr-2`} />
|
||||
{t("admin.charge.add-rule")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChargeTable() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [data, setData] = useState<ChargeProps[]>([
|
||||
{
|
||||
id: 1,
|
||||
type: nonBilling,
|
||||
models: ["gpt-4", "gpt-4-0613"],
|
||||
anonymous: true,
|
||||
input: 0,
|
||||
output: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
function refresh() {
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`charge-table`}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className={`select-none whitespace-nowrap`}>
|
||||
<TableCell>{t("admin.charge.id")}</TableCell>
|
||||
<TableCell>{t("admin.charge.type")}</TableCell>
|
||||
<TableCell>{t("admin.charge.model")}</TableCell>
|
||||
<TableCell>{t("admin.charge.input")}</TableCell>
|
||||
<TableCell>{t("admin.charge.output")}</TableCell>
|
||||
<TableCell>{t("admin.charge.support-anonymous")}</TableCell>
|
||||
<TableCell>{t("admin.charge.action")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((charge, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell>{charge.id}</TableCell>
|
||||
<TableCell>
|
||||
<Badge>
|
||||
{charge.type.split("-")[0]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className={`select-none`}>
|
||||
<pre>{charge.models.join("\n")}</pre>
|
||||
</TableCell>
|
||||
<TableCell>{charge.input === 0 ? 0 : charge.input.toFixed(2)}</TableCell>
|
||||
<TableCell>{charge.output === 0 ? 0 : charge.output.toFixed(2)}</TableCell>
|
||||
<TableCell>
|
||||
{t(String(charge.anonymous))}
|
||||
</TableCell>
|
||||
<TableCell className={`flex flex-row flex-wrap gap-2`}>
|
||||
<OperationAction
|
||||
tooltip={t("admin.channels.edit")}
|
||||
>
|
||||
<Settings2 className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
<OperationAction
|
||||
tooltip={t("admin.channels.delete")}
|
||||
variant={`destructive`}
|
||||
>
|
||||
<Trash className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className={`mt-6 pr-2 flex flex-row w-full h-max`}>
|
||||
<div className={`grow`} />
|
||||
<Button
|
||||
variant={`outline`}
|
||||
size={`icon`}
|
||||
className={`mr-2`}
|
||||
onClick={refresh}
|
||||
>
|
||||
<RotateCw className={`h-4 w-4`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChargeWidget() {
|
||||
return (
|
||||
<div className={`charge-widget`}>
|
||||
<ChargeEditor />
|
||||
<ChargeTable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChargeWidget;
|
@ -66,7 +66,7 @@ function MenuBar() {
|
||||
<MenuItem
|
||||
title={t("admin.prize")}
|
||||
icon={<CandlestickChart />}
|
||||
path={"/prize"}
|
||||
path={"/charge"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -156,6 +156,7 @@ function UserTable() {
|
||||
<TableHead>{t("admin.quota")}</TableHead>
|
||||
<TableHead>{t("admin.used-quota")}</TableHead>
|
||||
<TableHead>{t("admin.is-subscribed")}</TableHead>
|
||||
<TableHead>{t("admin.level")}</TableHead>
|
||||
<TableHead>{t("admin.total-month")}</TableHead>
|
||||
<TableHead>{t("admin.enterprise")}</TableHead>
|
||||
<TableHead>{t("admin.is-admin")}</TableHead>
|
||||
@ -166,10 +167,11 @@ function UserTable() {
|
||||
{(data.data || []).map((user, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell className={`whitespace-nowrap`}>{user.username}</TableCell>
|
||||
<TableCell>{user.quota}</TableCell>
|
||||
<TableCell>{user.used_quota}</TableCell>
|
||||
<TableCell>{t(user.is_subscribed.toString())}</TableCell>
|
||||
<TableCell>{user.level}</TableCell>
|
||||
<TableCell>{user.total_month}</TableCell>
|
||||
<TableCell>{t(user.enterprise.toString())}</TableCell>
|
||||
<TableCell>{t(user.is_admin.toString())}</TableCell>
|
||||
|
@ -13,8 +13,8 @@ import {
|
||||
ChannelModels,
|
||||
ChannelTypes,
|
||||
getChannelInfo,
|
||||
toastState,
|
||||
} from "@/admin/channel.ts";
|
||||
import {toastState} from "@/admin/utils.ts";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
@ -288,7 +288,7 @@ function ChannelEditor({ display, id, setEnabled }: ChannelEditorProps) {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>{t("admin.channels.add-model")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent asChild>
|
||||
<DropdownMenuContent align={`start`} asChild>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("admin.channels.search-model")}
|
||||
|
@ -10,7 +10,8 @@ import { Check, Plus, RotateCw, Settings2, Trash, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import OperationAction from "@/components/OperationAction.tsx";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Channel, getChannelType, toastState } from "@/admin/channel.ts";
|
||||
import { Channel, getChannelType } from "@/admin/channel.ts";
|
||||
import {toastState} from "@/admin/utils.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import {
|
||||
@ -87,10 +88,10 @@ function ChannelTable({ display, setId, setEnabled }: ChannelTableProps) {
|
||||
{chan.state ? (
|
||||
<Check className={`h-4 w-4 text-green-500`} />
|
||||
) : (
|
||||
<X className={`h-4 w-4 text-red-500`} />
|
||||
<X className={`h-4 w-4 text-destructive`} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className={`flex flex-row flex-wrap gap-2`}>
|
||||
<OperationAction
|
||||
tooltip={t("admin.channels.edit")}
|
||||
onClick={() => {
|
||||
|
42
app/src/components/ui/radio-group.tsx
Normal file
42
app/src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/components/ui/lib/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
@ -141,7 +141,7 @@ function SettingsDialog() {
|
||||
<div className={`name`}>{t("settings.history")}</div>
|
||||
<div className={`grow`} />
|
||||
<NumberInput
|
||||
className={cn(`value`, history === 0 && `text-red-500`)}
|
||||
className={cn(`value`, history === 0 && `text-destructive`)}
|
||||
value={history}
|
||||
acceptNaN={false}
|
||||
min={0}
|
||||
|
@ -345,6 +345,7 @@ const resources = {
|
||||
"post-success-prompt": "公告发布成功。",
|
||||
"post-failed": "发布失败",
|
||||
"post-failed-prompt": "发布失败,原因:{{reason}}",
|
||||
level: "等级",
|
||||
"is-admin": "管理员",
|
||||
"used-quota": "已用点数",
|
||||
"is-subscribed": "是否订阅",
|
||||
@ -402,6 +403,24 @@ const resources = {
|
||||
"add-model": "添加模型",
|
||||
"clear-models": "清空全部模型",
|
||||
},
|
||||
charge: {
|
||||
id: "ID",
|
||||
type: "类型",
|
||||
model: "模型",
|
||||
quota: "点数",
|
||||
action: "操作",
|
||||
input: "输入",
|
||||
output: "输出",
|
||||
"support-anonymous": "支持匿名",
|
||||
"non-billing": "不计费",
|
||||
"times-billing": "按次计费",
|
||||
"token-billing": "按 Token 计费",
|
||||
anonymous: "支持匿名调用",
|
||||
"time-count": "单次请求点数",
|
||||
"input-count": "输入点数",
|
||||
"output-count": "输出点数",
|
||||
"add-rule": "添加规则",
|
||||
},
|
||||
},
|
||||
mask: {
|
||||
title: "预设设置",
|
||||
@ -759,6 +778,7 @@ const resources = {
|
||||
"post-success-prompt": "Broadcast posted successfully.",
|
||||
"post-failed": "Post Failed",
|
||||
"post-failed-prompt": "Post failed, reason: {{reason}}",
|
||||
level: "Level",
|
||||
"is-admin": "Admin",
|
||||
"used-quota": "Used Quota",
|
||||
"is-subscribed": "Subscribed",
|
||||
@ -1182,6 +1202,7 @@ const resources = {
|
||||
"post-failed": "Не удалось",
|
||||
"post-failed-prompt":
|
||||
"Не удалось отправить объявление, причина: {{reason}}",
|
||||
level: "Уровень",
|
||||
"is-admin": "Админ",
|
||||
"used-quota": "Использовано",
|
||||
"is-subscribed": "Подписан",
|
||||
|
@ -11,7 +11,7 @@ const Article = lazy(() => import("@/routes/Article.tsx"));
|
||||
const Admin = lazy(() => import("@/routes/Admin.tsx"));
|
||||
const Dashboard = lazy(() => import("@/routes/admin/DashBoard.tsx"));
|
||||
const Channel = lazy(() => import("@/routes/admin/Channel.tsx"));
|
||||
const Prize = lazy(() => import("@/routes/admin/Prize.tsx"));
|
||||
const Charge = lazy(() => import("@/routes/admin/Charge.tsx"));
|
||||
const Users = lazy(() => import("@/routes/admin/Users.tsx"));
|
||||
const Broadcast = lazy(() => import("@/routes/admin/Broadcast.tsx"));
|
||||
|
||||
@ -95,11 +95,11 @@ const router = createBrowserRouter([
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "admin-prize",
|
||||
path: "prize",
|
||||
id: "admin-charge",
|
||||
path: "charge",
|
||||
element: (
|
||||
<Suspense>
|
||||
<Prize />
|
||||
<Charge />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
|
@ -17,7 +17,7 @@ function Admin() {
|
||||
return (
|
||||
<div className={`admin-page`}>
|
||||
<MenuBar />
|
||||
<div className={`admin-content`}>
|
||||
<div className={`admin-content thin-scrollbar`}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
27
app/src/routes/admin/Charge.tsx
Normal file
27
app/src/routes/admin/Charge.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ChargeWidget from "@/components/admin/ChargeWidget.tsx";
|
||||
|
||||
function Charge() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`charge`}>
|
||||
<Card className={`charge-card`}>
|
||||
<CardHeader className={`select-none`}>
|
||||
<CardTitle>{t("admin.prize")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChargeWidget />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Charge;
|
@ -1,5 +0,0 @@
|
||||
function Prize() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default Prize;
|
38
auth/rule.go
38
auth/rule.go
@ -1,41 +1,23 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"chat/channel"
|
||||
"database/sql"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
// CanEnableModel returns whether the model can be enabled (without subscription)
|
||||
func CanEnableModel(db *sql.DB, user *User, model string) bool {
|
||||
switch model {
|
||||
case globals.GPT3Turbo, globals.GPT3TurboInstruct, globals.GPT3Turbo0301, globals.GPT3Turbo0613:
|
||||
return true
|
||||
case globals.GPT4, globals.GPT40613, globals.GPT40314, globals.GPT41106Preview, globals.GPT41106VisionPreview,
|
||||
globals.GPT4Dalle, globals.GPT4Vision, globals.Dalle3:
|
||||
return user != nil && user.GetQuota(db) >= 5
|
||||
case globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
||||
return user != nil && user.GetQuota(db) >= 50
|
||||
case globals.SparkDesk, globals.SparkDeskV2, globals.SparkDeskV3:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.Claude1100k, globals.Claude2100k, globals.Claude2200k:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMPro, globals.ZhiPuChatGLMStd:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.QwenTurbo, globals.QwenPlus, globals.QwenPlusNet, globals.QwenTurboNet:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.StableDiffusion, globals.Midjourney, globals.MidjourneyFast, globals.MidjourneyTurbo:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.LLaMa27B, globals.LLaMa213B, globals.LLaMa270B,
|
||||
globals.CodeLLaMa34B, globals.CodeLLaMa13B, globals.CodeLLaMa7B:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.Hunyuan, globals.GPT360V9, globals.Baichuan53B:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
case globals.SkylarkLite, globals.SkylarkPlus, globals.SkylarkPro, globals.SkylarkChat:
|
||||
return user != nil && user.GetQuota(db) >= 1
|
||||
default:
|
||||
return user != nil
|
||||
isAuth := user != nil
|
||||
charge := channel.ChargeInstance.GetCharge(model)
|
||||
|
||||
if !charge.IsBilling() {
|
||||
// return if is the user is authenticated or anonymous is allowed for this model
|
||||
return charge.SupportAnonymous() || isAuth
|
||||
}
|
||||
|
||||
// return if the user is authenticated and has enough quota
|
||||
return isAuth && user.GetQuota(db) >= charge.GetLimit()
|
||||
}
|
||||
|
||||
func CanEnableModelWithSubscription(db *sql.DB, cache *redis.Client, user *User, model string) (canEnable bool, usePlan bool) {
|
||||
|
179
channel/charge.go
Normal file
179
channel/charge.go
Normal file
@ -0,0 +1,179 @@
|
||||
package channel
|
||||
|
||||
import (
|
||||
"chat/globals"
|
||||
"chat/utils"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func NewChargeManager() *ChargeManager {
|
||||
var seq ChargeSequence
|
||||
if err := viper.UnmarshalKey("charge", &seq); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m := &ChargeManager{
|
||||
Sequence: seq,
|
||||
Models: map[string]*Charge{},
|
||||
NonBillingModels: []string{},
|
||||
}
|
||||
m.Load()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *ChargeManager) Load() {
|
||||
// init support models
|
||||
m.Models = map[string]*Charge{}
|
||||
for _, charge := range m.Sequence {
|
||||
for _, model := range charge.Models {
|
||||
if _, ok := m.Models[model]; !ok {
|
||||
m.Models[model] = charge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.NonBillingModels = []string{}
|
||||
for _, charge := range m.Sequence {
|
||||
if !charge.IsBilling() {
|
||||
for _, model := range charge.Models {
|
||||
m.NonBillingModels = append(m.NonBillingModels, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ChargeManager) GetModels() map[string]*Charge {
|
||||
return m.Models
|
||||
}
|
||||
|
||||
func (m *ChargeManager) GetNonBillingModels() []string {
|
||||
return m.NonBillingModels
|
||||
}
|
||||
|
||||
func (m *ChargeManager) IsBilling(model string) bool {
|
||||
return !utils.Contains(model, m.NonBillingModels)
|
||||
}
|
||||
|
||||
func (m *ChargeManager) GetCharge(model string) Charge {
|
||||
if charge, ok := m.Models[model]; ok {
|
||||
return *charge
|
||||
}
|
||||
return Charge{
|
||||
Type: globals.NonBilling,
|
||||
Anonymous: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ChargeManager) SaveConfig() error {
|
||||
viper.Set("charge", m.Sequence)
|
||||
m.Load()
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
func (m *ChargeManager) GetMaxId() int {
|
||||
max := 0
|
||||
for _, charge := range m.Sequence {
|
||||
if charge.Id > max {
|
||||
max = charge.Id
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func (m *ChargeManager) AddRule(charge Charge) error {
|
||||
charge.Id = m.GetMaxId() + 1
|
||||
m.Sequence = append(m.Sequence, &charge)
|
||||
return m.SaveConfig()
|
||||
}
|
||||
|
||||
func (m *ChargeManager) UpdateRule(charge Charge) error {
|
||||
for _, item := range m.Sequence {
|
||||
if item.Id == charge.Id {
|
||||
*item = charge
|
||||
break
|
||||
}
|
||||
}
|
||||
return m.SaveConfig()
|
||||
}
|
||||
|
||||
func (m *ChargeManager) SetRule(charge Charge) error {
|
||||
if charge.Id == -1 {
|
||||
return m.AddRule(charge)
|
||||
}
|
||||
return m.UpdateRule(charge)
|
||||
}
|
||||
|
||||
func (m *ChargeManager) DeleteRule(id int) error {
|
||||
for i, item := range m.Sequence {
|
||||
if item.Id == id {
|
||||
m.Sequence = append(m.Sequence[:i], m.Sequence[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return m.SaveConfig()
|
||||
}
|
||||
|
||||
func (m *ChargeManager) ListRules() ChargeSequence {
|
||||
return m.Sequence
|
||||
}
|
||||
|
||||
func (m *ChargeManager) GetRule(id int) *Charge {
|
||||
for _, item := range m.Sequence {
|
||||
if item.Id == id {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Charge) GetType() string {
|
||||
if c.Type == "" {
|
||||
return globals.NonBilling
|
||||
}
|
||||
return c.Type
|
||||
}
|
||||
|
||||
func (c *Charge) GetModels() []string {
|
||||
return c.Models
|
||||
}
|
||||
|
||||
func (c *Charge) GetInput() float32 {
|
||||
if c.Input <= 0 {
|
||||
return 0
|
||||
}
|
||||
return c.Input
|
||||
}
|
||||
|
||||
func (c *Charge) GetOutput() float32 {
|
||||
if c.Output <= 0 {
|
||||
return 0
|
||||
}
|
||||
return c.Output
|
||||
}
|
||||
|
||||
func (c *Charge) SupportAnonymous() bool {
|
||||
return c.Anonymous
|
||||
}
|
||||
|
||||
func (c *Charge) IsBilling() bool {
|
||||
return c.GetType() != globals.NonBilling
|
||||
}
|
||||
|
||||
func (c *Charge) IsBillingType(t string) bool {
|
||||
return c.GetType() == t
|
||||
}
|
||||
|
||||
func (c *Charge) GetLimit() float32 {
|
||||
switch c.GetType() {
|
||||
case globals.NonBilling:
|
||||
return 0
|
||||
case globals.TimesBilling:
|
||||
return c.GetOutput()
|
||||
case globals.TokenBilling:
|
||||
// 1k input tokens + 1k output tokens
|
||||
return c.GetInput() + c.GetOutput()
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
@ -89,3 +89,37 @@ func UpdateChannel(c *gin.Context) {
|
||||
"error": utils.GetError(state),
|
||||
})
|
||||
}
|
||||
|
||||
func SetCharge(c *gin.Context) {
|
||||
var charge Charge
|
||||
if err := c.ShouldBindJSON(&charge); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
state := ChargeInstance.SetRule(charge)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": state == nil,
|
||||
"error": utils.GetError(state),
|
||||
})
|
||||
}
|
||||
|
||||
func GetChargeList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"data": ChargeInstance.ListRules(),
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
state := ChargeInstance.DeleteRule(utils.ParseInt(id))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": state == nil,
|
||||
"error": utils.GetError(state),
|
||||
})
|
||||
}
|
||||
|
@ -7,9 +7,11 @@ import (
|
||||
)
|
||||
|
||||
var ManagerInstance *Manager
|
||||
var ChargeInstance *ChargeManager
|
||||
|
||||
func InitManager() {
|
||||
ManagerInstance = NewManager()
|
||||
ChargeInstance = NewChargeManager()
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
|
@ -10,4 +10,8 @@ func Register(app *gin.Engine) {
|
||||
app.GET("/admin/channel/delete/:id", DeleteChannel)
|
||||
app.GET("/admin/channel/activate/:id", ActivateChannel)
|
||||
app.GET("/admin/channel/deactivate/:id", DeactivateChannel)
|
||||
|
||||
app.GET("/admin/charge/list", GetChargeList)
|
||||
app.POST("/admin/charge/set", SetCharge)
|
||||
app.GET("/admin/charge/delete/:id", DeleteCharge)
|
||||
}
|
||||
|
@ -28,3 +28,20 @@ type Ticker struct {
|
||||
Sequence Sequence `json:"sequence"`
|
||||
Cursor int `json:"cursor"`
|
||||
}
|
||||
|
||||
type Charge struct {
|
||||
Id int `json:"id" mapstructure:"id"`
|
||||
Type string `json:"type" mapstructure:"type"`
|
||||
Models []string `json:"models" mapstructure:"models"`
|
||||
Input float32 `json:"input" mapstructure:"input"`
|
||||
Output float32 `json:"output" mapstructure:"output"`
|
||||
Anonymous bool `json:"anonymous" mapstructure:"anonymous"`
|
||||
}
|
||||
|
||||
type ChargeSequence []*Charge
|
||||
|
||||
type ChargeManager struct {
|
||||
Sequence ChargeSequence `json:"sequence"`
|
||||
Models map[string]*Charge `json:"models"`
|
||||
NonBillingModels []string `json:"non_billing_models"`
|
||||
}
|
||||
|
@ -3,13 +3,12 @@ package cli
|
||||
import (
|
||||
"chat/adapter/chatgpt"
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FilterApiKeyCommand(args []string) {
|
||||
data := strings.Trim(strings.TrimSpace(GetArgString(args, 0)), "\"")
|
||||
endpoint := viper.GetString("openai.test")
|
||||
endpoint := "https://api.openai.com"
|
||||
keys := strings.Split(data, "|")
|
||||
|
||||
available := chatgpt.FilterKeysNative(endpoint, keys)
|
||||
|
@ -23,3 +23,9 @@ const (
|
||||
MidjourneyChannelType = "midjourney"
|
||||
OneAPIChannelType = "oneapi"
|
||||
)
|
||||
|
||||
const (
|
||||
NonBilling = "non-billing"
|
||||
TimesBilling = "times-billing"
|
||||
TokenBilling = "token-billing"
|
||||
)
|
||||
|
@ -104,44 +104,6 @@ var GPT4Array = []string{
|
||||
GPT4Vision, GPT4Dalle, GPT4All,
|
||||
}
|
||||
|
||||
var GPT432kArray = []string{
|
||||
GPT432k,
|
||||
GPT432k0314,
|
||||
GPT432k0613,
|
||||
}
|
||||
|
||||
var SparkDeskModelArray = []string{
|
||||
SparkDesk,
|
||||
SparkDeskV2,
|
||||
SparkDeskV3,
|
||||
}
|
||||
|
||||
var LongContextModelArray = []string{
|
||||
GPT3Turbo16k, GPT3Turbo16k0613, GPT3Turbo16k0301,
|
||||
GPT41106Preview, GPT41106VisionPreview, GPT432k, GPT432k0314, GPT432k0613,
|
||||
Claude1, Claude1100k,
|
||||
CodeLLaMa34B, LLaMa270B,
|
||||
Claude2, Claude2100k, Claude2200k,
|
||||
}
|
||||
|
||||
var FreeModelArray = []string{
|
||||
GPT3Turbo,
|
||||
GPT3TurboInstruct,
|
||||
GPT3Turbo0613,
|
||||
GPT3Turbo0301,
|
||||
GPT3Turbo1106,
|
||||
GPT3Turbo16k,
|
||||
GPT3Turbo16k0613,
|
||||
GPT3Turbo16k0301,
|
||||
Claude1,
|
||||
Claude2,
|
||||
ChatBison001,
|
||||
BingCreative,
|
||||
BingBalanced,
|
||||
BingPrecise,
|
||||
ZhiPuChatGLMLite,
|
||||
}
|
||||
|
||||
func in(value string, slice []string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
@ -166,15 +128,3 @@ func IsClaude100KModel(model string) bool {
|
||||
func IsMidjourneyFastModel(model string) bool {
|
||||
return model == MidjourneyFast
|
||||
}
|
||||
|
||||
func IsSparkDeskModel(model string) bool {
|
||||
return in(model, SparkDeskModelArray)
|
||||
}
|
||||
|
||||
func IsLongContextModel(model string) bool {
|
||||
return in(model, LongContextModelArray)
|
||||
}
|
||||
|
||||
func IsFreeModel(model string) bool {
|
||||
return in(model, FreeModelArray)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"chat/channel"
|
||||
"chat/globals"
|
||||
"chat/utils"
|
||||
"fmt"
|
||||
@ -28,7 +29,7 @@ func ExtractCacheData(c *gin.Context, props *CacheProps) *CacheData {
|
||||
}
|
||||
|
||||
func SaveCacheData(c *gin.Context, props *CacheProps, data *CacheData) {
|
||||
if !globals.IsFreeModel(props.Model) {
|
||||
if channel.ChargeInstance.IsBilling(props.Model) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Limiter struct {
|
||||
@ -15,16 +14,10 @@ type Limiter struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (l *Limiter) RateLimit(ctx *gin.Context, rds *redis.Client, ip string, path string) bool {
|
||||
key := fmt.Sprintf("rate%s:%s", path, ip)
|
||||
count, err := rds.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if count == 1 {
|
||||
rds.Expire(ctx, key, time.Duration(l.Duration)*time.Second)
|
||||
}
|
||||
return count > l.Count
|
||||
func (l *Limiter) RateLimit(client *redis.Client, ip string, path string) bool {
|
||||
key := fmt.Sprintf("rate:%s:%s", path, ip)
|
||||
rate := utils.IncrWithLimit(client, key, 1, l.Count, int64(l.Duration))
|
||||
return !rate
|
||||
}
|
||||
|
||||
var limits = map[string]Limiter{
|
||||
@ -62,7 +55,7 @@ func ThrottleMiddleware() gin.HandlerFunc {
|
||||
cache := utils.GetCacheFromContext(c)
|
||||
admin.IncrRequest(cache)
|
||||
limiter := GetPrefixMap[Limiter](path, limits)
|
||||
if limiter != nil && limiter.RateLimit(c, cache, ip, path) {
|
||||
if limiter != nil && limiter.RateLimit(cache, ip, path) {
|
||||
c.JSON(200, gin.H{"status": false, "reason": "You have sent too many requests. Please try again later."})
|
||||
c.Abort()
|
||||
return
|
||||
|
@ -58,6 +58,7 @@ func IncrWithLimit(cache *redis.Client, key string, delta int64, limit int64, ex
|
||||
return false
|
||||
}
|
||||
if res > limit {
|
||||
// reset
|
||||
cache.Set(context.Background(), key, limit, time.Duration(expiration)*time.Second)
|
||||
return false
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"chat/channel"
|
||||
"chat/globals"
|
||||
"github.com/pkoukk/tiktoken-go"
|
||||
"strings"
|
||||
@ -18,19 +19,10 @@ func GetWeightByModel(model string) int {
|
||||
return 2
|
||||
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo1106,
|
||||
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613,
|
||||
globals.GPT4, globals.GPT4Vision, globals.GPT4Dalle, globals.GPT4All, globals.GPT40314, globals.GPT40613, globals.GPT41106Preview, globals.GPT41106VisionPreview,
|
||||
globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314,
|
||||
globals.LLaMa27B, globals.LLaMa213B, globals.LLaMa270B,
|
||||
globals.CodeLLaMa34B, globals.CodeLLaMa13B, globals.CodeLLaMa7B,
|
||||
|
||||
globals.SparkDesk, globals.SparkDeskV2, globals.SparkDeskV3,
|
||||
globals.QwenTurbo, globals.QwenPlus, globals.QwenTurboNet, globals.QwenPlusNet,
|
||||
globals.BingPrecise, globals.BingCreative, globals.BingBalanced,
|
||||
globals.Hunyuan, globals.GPT360V9, globals.Baichuan53B,
|
||||
globals.SkylarkLite, globals.SkylarkPlus, globals.SkylarkPro, globals.SkylarkChat:
|
||||
globals.GPT4, globals.GPT40314, globals.GPT40613, globals.GPT41106Preview, globals.GPT41106VisionPreview,
|
||||
globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
||||
return 3
|
||||
case globals.GPT3Turbo0301, globals.GPT3Turbo16k0301,
|
||||
globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMLite, globals.ZhiPuChatGLMStd, globals.ZhiPuChatGLMPro:
|
||||
case globals.GPT3Turbo0301, globals.GPT3Turbo16k0301:
|
||||
return 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||
default:
|
||||
if strings.Contains(model, globals.GPT3Turbo) {
|
||||
@ -47,7 +39,6 @@ func GetWeightByModel(model string) int {
|
||||
return GetWeightByModel(globals.Claude2100k)
|
||||
} else {
|
||||
// not implemented: See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens
|
||||
//panic(fmt.Errorf("not implemented for model %s", model))
|
||||
return 3
|
||||
}
|
||||
}
|
||||
@ -76,107 +67,23 @@ func CountTokenPrice(messages []globals.Message, model string) int {
|
||||
return NumTokensFromMessages(messages, model)
|
||||
}
|
||||
|
||||
func CountInputToken(model string, v []globals.Message) float32 {
|
||||
switch model {
|
||||
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo0301, globals.GPT3TurboInstruct, globals.GPT3Turbo1106,
|
||||
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613, globals.GPT3Turbo16k0301:
|
||||
return 0
|
||||
case globals.GPT41106Preview, globals.GPT41106VisionPreview:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.7 * 0.6
|
||||
case globals.GPT4, globals.GPT4Vision, globals.GPT4All, globals.GPT4Dalle, globals.GPT40314, globals.GPT40613:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 2.1 * 0.6
|
||||
case globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 4.2
|
||||
case globals.SparkDesk:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.15
|
||||
case globals.SparkDeskV2, globals.SparkDeskV3:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.3
|
||||
case globals.Claude1, globals.Claude2:
|
||||
return 0
|
||||
case globals.Claude1100k, globals.Claude2100k, globals.Claude2200k:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.8
|
||||
case globals.LLaMa270B, globals.CodeLLaMa34B:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.25
|
||||
case globals.LLaMa213B, globals.CodeLLaMa13B, globals.LLaMa27B, globals.CodeLLaMa7B:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.1
|
||||
case globals.ZhiPuChatGLMPro:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.1
|
||||
case globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMStd:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.05
|
||||
case globals.QwenTurbo, globals.QwenTurboNet:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.08
|
||||
case globals.QwenPlus, globals.QwenPlusNet:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.2
|
||||
case globals.Hunyuan:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 1
|
||||
case globals.GPT360V9:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.12
|
||||
case globals.Baichuan53B:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.2
|
||||
case globals.SkylarkLite:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.04
|
||||
case globals.SkylarkPlus:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.08
|
||||
case globals.SkylarkPro, globals.SkylarkChat:
|
||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.11
|
||||
default:
|
||||
return 0
|
||||
func CountInputToken(model string, message []globals.Message) float32 {
|
||||
charge := channel.ChargeInstance.GetCharge(model)
|
||||
|
||||
if charge.IsBillingType(globals.TokenBilling) {
|
||||
return float32(CountTokenPrice(message, model)) / 1000 * charge.GetInput()
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func CountOutputToken(model string, t int) float32 {
|
||||
switch model {
|
||||
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo0301, globals.GPT3TurboInstruct, globals.GPT3Turbo1106,
|
||||
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613, globals.GPT3Turbo16k0301:
|
||||
return 0
|
||||
case globals.GPT41106Preview, globals.GPT41106VisionPreview:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 2.1 * 0.6
|
||||
case globals.GPT4, globals.GPT4Vision, globals.GPT4All, globals.GPT4Dalle, globals.GPT40314, globals.GPT40613:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 4.3 * 0.6
|
||||
case globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 8.6
|
||||
case globals.SparkDesk:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.15
|
||||
case globals.SparkDeskV2, globals.SparkDeskV3:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.3
|
||||
case globals.Claude1, globals.Claude2:
|
||||
return 0
|
||||
case globals.Claude1100k, globals.Claude2100k, globals.Claude2200k:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 2.4
|
||||
case globals.LLaMa270B, globals.CodeLLaMa34B:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.25
|
||||
case globals.LLaMa213B, globals.CodeLLaMa13B, globals.LLaMa27B, globals.CodeLLaMa7B:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.1
|
||||
case globals.ZhiPuChatGLMPro:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.1
|
||||
case globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMStd:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.05
|
||||
case globals.QwenTurbo, globals.QwenTurboNet:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.08
|
||||
case globals.QwenPlus, globals.QwenPlusNet:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.2
|
||||
case globals.Hunyuan:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 1
|
||||
case globals.GPT360V9:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.12
|
||||
case globals.Baichuan53B:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.2
|
||||
case globals.SkylarkLite:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.04
|
||||
case globals.SkylarkPlus:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.08
|
||||
case globals.SkylarkPro, globals.SkylarkChat:
|
||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.11
|
||||
case globals.StableDiffusion:
|
||||
return 0.25
|
||||
case globals.Midjourney:
|
||||
return 0.1
|
||||
case globals.MidjourneyFast:
|
||||
return 0.5
|
||||
case globals.MidjourneyTurbo:
|
||||
return 1
|
||||
case globals.Dalle3:
|
||||
return 5.6
|
||||
func CountOutputToken(model string, token int) float32 {
|
||||
charge := channel.ChargeInstance.GetCharge(model)
|
||||
switch charge.GetType() {
|
||||
case globals.TokenBilling:
|
||||
return float32(token*GetWeightByModel(model)) / 1000 * charge.GetOutput()
|
||||
case globals.TimesBilling:
|
||||
return charge.GetOutput()
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user