diff --git a/README.md b/README.md index d1bad7f..c69d783 100644 --- a/README.md +++ b/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) [![code-stats](https://stats.deeptrain.net/repo/Deeptrain-Community/chatnio)](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) (全栈开发) diff --git a/adapter/adapter.go b/adapter/adapter.go index 8ad5724..35a502e 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -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, diff --git a/addition/web/call.go b/addition/web/call.go index a3c4131..e0a6af1 100644 --- a/addition/web/call.go +++ b/addition/web/call.go @@ -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."+ diff --git a/addition/web/utils.go b/addition/web/utils.go index c7af04d..70603ed 100644 --- a/addition/web/utils.go +++ b/addition/web/utils.go @@ -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 } diff --git a/admin/types.go b/admin/types.go index 5b0f388..4786f2a 100644 --- a/admin/types.go +++ b/admin/types.go @@ -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"` } diff --git a/admin/user.go b/admin/user.go index 670e209..f5b0810 100644 --- a/admin/user.go +++ b/admin/user.go @@ -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()) diff --git a/app/components.json b/app/components.json index 382f160..683037b 100644 --- a/app/components.json +++ b/app/components.json @@ -11,6 +11,6 @@ }, "aliases": { "components": "src/components", - "utils": "src/components/ui/lib/utils" + "utils": "@/components/ui/lib/utils" } } diff --git a/app/package.json b/app/package.json index 2e8f433..6ce2dcc 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index d9c6ca6..e22b74c 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -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: diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 6e531b6..1f41283 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -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 }); -} diff --git a/app/src/admin/charge.ts b/app/src/admin/charge.ts new file mode 100644 index 0000000..e3803b0 --- /dev/null +++ b/app/src/admin/charge.ts @@ -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; +}; diff --git a/app/src/admin/types.ts b/app/src/admin/types.ts index 58c9524..ea5dbb7 100644 --- a/app/src/admin/types.ts +++ b/app/src/admin/types.ts @@ -66,6 +66,7 @@ export type UserData = { used_quota: number; is_subscribed: boolean; total_month: number; + level: number; enterprise: boolean; }; diff --git a/app/src/admin/utils.ts b/app/src/admin/utils.ts new file mode 100644 index 0000000..d74da65 --- /dev/null +++ b/app/src/admin/utils.ts @@ -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 }); +} diff --git a/app/src/assets/admin/all.less b/app/src/assets/admin/all.less index 5adf7ba..3dbc8c4 100644 --- a/app/src/assets/admin/all.less +++ b/app/src/assets/admin/all.less @@ -3,6 +3,7 @@ @import "management"; @import "broadcast"; @import "channel"; +@import "charge"; .admin-page { position: relative; diff --git a/app/src/assets/admin/broadcast.less b/app/src/assets/admin/broadcast.less index 847de62..c9f01a1 100644 --- a/app/src/assets/admin/broadcast.less +++ b/app/src/assets/admin/broadcast.less @@ -1,6 +1,6 @@ .broadcast { width: 100%; - height: 100%; + height: max-content; padding: 2rem; display: flex; flex-direction: column; diff --git a/app/src/assets/admin/channel.less b/app/src/assets/admin/channel.less index 518d313..3de235d 100644 --- a/app/src/assets/admin/channel.less +++ b/app/src/assets/admin/channel.less @@ -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 { diff --git a/app/src/assets/admin/charge.less b/app/src/assets/admin/charge.less new file mode 100644 index 0000000..bbb740a --- /dev/null +++ b/app/src/assets/admin/charge.less @@ -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); +} diff --git a/app/src/assets/admin/dashboard.less b/app/src/assets/admin/dashboard.less index a9780a6..712fadd 100644 --- a/app/src/assets/admin/dashboard.less +++ b/app/src/assets/admin/dashboard.less @@ -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%; diff --git a/app/src/assets/admin/management.less b/app/src/assets/admin/management.less index bcb9472..8d0b165 100644 --- a/app/src/assets/admin/management.less +++ b/app/src/assets/admin/management.less @@ -2,8 +2,7 @@ display: flex; flex-direction: column; width: 100%; - height: 100%; - max-height: calc(100vh - 56px); + height: calc(100vh - 56px); padding: 2rem; & > * { diff --git a/app/src/assets/admin/menu.less b/app/src/assets/admin/menu.less index 3798aad..d5e58a2 100644 --- a/app/src/assets/admin/menu.less +++ b/app/src/assets/admin/menu.less @@ -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)); } diff --git a/app/src/assets/globals.less b/app/src/assets/globals.less index 46a2a96..e7aa49b 100644 --- a/app/src/assets/globals.less +++ b/app/src/assets/globals.less @@ -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); } } diff --git a/app/src/assets/main.less b/app/src/assets/main.less index 5091ea7..086c483 100644 --- a/app/src/assets/main.less +++ b/app/src/assets/main.less @@ -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) { diff --git a/app/src/assets/markdown/all.less b/app/src/assets/markdown/all.less index cf123c3..a2f2aa7 100644 --- a/app/src/assets/markdown/all.less +++ b/app/src/assets/markdown/all.less @@ -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) { diff --git a/app/src/assets/pages/auth.less b/app/src/assets/pages/auth.less index 3421f3d..4a9f9e3 100644 --- a/app/src/assets/pages/auth.less +++ b/app/src/assets/pages/auth.less @@ -2,5 +2,5 @@ width: 100%; height: calc(100vh - 56px); overflow: hidden; - background: hsl(var(--background-container)); + background: hsla(var(--background-container)); } diff --git a/app/src/assets/pages/chat.less b/app/src/assets/pages/chat.less index 1b329da..f74169b 100644 --- a/app/src/assets/pages/chat.less +++ b/app/src/assets/pages/chat.less @@ -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); } } } diff --git a/app/src/assets/pages/generation.less b/app/src/assets/pages/generation.less index a252985..fe67642 100644 --- a/app/src/assets/pages/generation.less +++ b/app/src/assets/pages/generation.less @@ -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)); diff --git a/app/src/assets/pages/home.less b/app/src/assets/pages/home.less index 4a4a2cc..cd6e34a 100644 --- a/app/src/assets/pages/home.less +++ b/app/src/assets/pages/home.less @@ -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 { diff --git a/app/src/assets/pages/quota.less b/app/src/assets/pages/quota.less index 788def9..ac6de7a 100644 --- a/app/src/assets/pages/quota.less +++ b/app/src/assets/pages/quota.less @@ -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; diff --git a/app/src/assets/pages/sharing.less b/app/src/assets/pages/sharing.less index 3840231..cde45c6 100644 --- a/app/src/assets/pages/sharing.less +++ b/app/src/assets/pages/sharing.less @@ -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; diff --git a/app/src/assets/ui.less b/app/src/assets/ui.less index fc1cb58..10bd83e 100644 --- a/app/src/assets/ui.less +++ b/app/src/assets/ui.less @@ -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)); + } +} diff --git a/app/src/components/OperationAction.tsx b/app/src/components/OperationAction.tsx index 26f93eb..c5f755d 100644 --- a/app/src/components/OperationAction.tsx +++ b/app/src/components/OperationAction.tsx @@ -36,7 +36,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) { + setModel(e.target.value)} + placeholder={t("admin.channels.model")} + /> + + + + + + + + + {unusedModels.map((model, idx) => ( + dispatch({ type: "add-model", payload: value })} + className={`px-2`} + > + {model} + + ))} + + + + + +
+ {form.models.map((model, index) => ( +
+ + +
+ ))} +
+ + { + form.type === nonBilling && ( +
+ + + dispatch({ type: "set-anonymous", payload: checked })} + /> +
+ ) + } + + { + form.type === timesBilling && ( +
+ + + dispatch({ type: "set-output", payload: value })} + acceptNegative={false} + className={`w-20`} + min={0} + max={99999} + /> +
+ ) + } + + { + form.type === tokenBilling && ( +
+
+ + + dispatch({ type: "set-input", payload: value })} + acceptNegative={false} + className={`w-20`} + min={0} + max={99999} + /> +
+
+ + + dispatch({ type: "set-output", payload: value })} + acceptNegative={false} + className={`w-20`} + min={0} + max={99999} + /> +
+
+ ) + } + +
+
+ + +
+
+ ); +} + +function ChargeTable() { + const { t } = useTranslation(); + const { toast } = useToast(); + const [data, setData] = useState([ + { + id: 1, + type: nonBilling, + models: ["gpt-4", "gpt-4-0613"], + anonymous: true, + input: 0, + output: 0, + }, + ]); + + function refresh() { + + } + + return ( +
+ + + + {t("admin.charge.id")} + {t("admin.charge.type")} + {t("admin.charge.model")} + {t("admin.charge.input")} + {t("admin.charge.output")} + {t("admin.charge.support-anonymous")} + {t("admin.charge.action")} + + + + {data.map((charge, idx) => ( + + {charge.id} + + + {charge.type.split("-")[0]} + + + +
{charge.models.join("\n")}
+
+ {charge.input === 0 ? 0 : charge.input.toFixed(2)} + {charge.output === 0 ? 0 : charge.output.toFixed(2)} + + {t(String(charge.anonymous))} + + + + + + + + + +
+ ))} +
+
+
+
+ +
+
+ ) +} + +function ChargeWidget() { + return ( +
+ + +
+ ); +} + +export default ChargeWidget; diff --git a/app/src/components/admin/MenuBar.tsx b/app/src/components/admin/MenuBar.tsx index 427adb3..a2223d8 100644 --- a/app/src/components/admin/MenuBar.tsx +++ b/app/src/components/admin/MenuBar.tsx @@ -66,7 +66,7 @@ function MenuBar() { } - path={"/prize"} + path={"/charge"} />
); diff --git a/app/src/components/admin/UserTable.tsx b/app/src/components/admin/UserTable.tsx index c4d2722..fd749ba 100644 --- a/app/src/components/admin/UserTable.tsx +++ b/app/src/components/admin/UserTable.tsx @@ -156,6 +156,7 @@ function UserTable() { {t("admin.quota")} {t("admin.used-quota")} {t("admin.is-subscribed")} + {t("admin.level")} {t("admin.total-month")} {t("admin.enterprise")} {t("admin.is-admin")} @@ -166,10 +167,11 @@ function UserTable() { {(data.data || []).map((user, idx) => ( {user.id} - {user.username} + {user.username} {user.quota} {user.used_quota} {t(user.is_subscribed.toString())} + {user.level} {user.total_month} {t(user.enterprise.toString())} {t(user.is_admin.toString())} diff --git a/app/src/components/admin/assemblies/ChannelEditor.tsx b/app/src/components/admin/assemblies/ChannelEditor.tsx index 446b5a3..9cdb3bf 100644 --- a/app/src/components/admin/assemblies/ChannelEditor.tsx +++ b/app/src/components/admin/assemblies/ChannelEditor.tsx @@ -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) { - + ) : ( - + )} - + { diff --git a/app/src/components/ui/radio-group.tsx b/app/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..a90fdbe --- /dev/null +++ b/app/src/components/ui/radio-group.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { RadioGroup, RadioGroupItem }; diff --git a/app/src/dialogs/SettingsDialog.tsx b/app/src/dialogs/SettingsDialog.tsx index 67024e7..54da92e 100644 --- a/app/src/dialogs/SettingsDialog.tsx +++ b/app/src/dialogs/SettingsDialog.tsx @@ -141,7 +141,7 @@ function SettingsDialog() {
{t("settings.history")}
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: ( - + ), }, diff --git a/app/src/routes/Admin.tsx b/app/src/routes/Admin.tsx index 24bc237..a5bf668 100644 --- a/app/src/routes/Admin.tsx +++ b/app/src/routes/Admin.tsx @@ -17,7 +17,7 @@ function Admin() { return (
-
+
diff --git a/app/src/routes/admin/Charge.tsx b/app/src/routes/admin/Charge.tsx new file mode 100644 index 0000000..88144d5 --- /dev/null +++ b/app/src/routes/admin/Charge.tsx @@ -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 ( +
+ + + {t("admin.prize")} + + + + + +
+ ); +} + +export default Charge; diff --git a/app/src/routes/admin/Prize.tsx b/app/src/routes/admin/Prize.tsx deleted file mode 100644 index d338af6..0000000 --- a/app/src/routes/admin/Prize.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function Prize() { - return <>; -} - -export default Prize; diff --git a/auth/rule.go b/auth/rule.go index 50f58e4..d1f0ddb 100644 --- a/auth/rule.go +++ b/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) { diff --git a/channel/charge.go b/channel/charge.go new file mode 100644 index 0000000..9e1aff9 --- /dev/null +++ b/channel/charge.go @@ -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 + } +} diff --git a/channel/controller.go b/channel/controller.go index 33d5517..8dd5fb8 100644 --- a/channel/controller.go +++ b/channel/controller.go @@ -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), + }) +} diff --git a/channel/manager.go b/channel/manager.go index d710a10..51c70ab 100644 --- a/channel/manager.go +++ b/channel/manager.go @@ -7,9 +7,11 @@ import ( ) var ManagerInstance *Manager +var ChargeInstance *ChargeManager func InitManager() { ManagerInstance = NewManager() + ChargeInstance = NewChargeManager() } func NewManager() *Manager { diff --git a/channel/router.go b/channel/router.go index 68b61ff..0166d4a 100644 --- a/channel/router.go +++ b/channel/router.go @@ -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) } diff --git a/channel/types.go b/channel/types.go index 61a2e1d..0290410 100644 --- a/channel/types.go +++ b/channel/types.go @@ -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"` +} diff --git a/cli/filter.go b/cli/filter.go index 4b6ba91..7b6850c 100644 --- a/cli/filter.go +++ b/cli/filter.go @@ -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) diff --git a/globals/constant.go b/globals/constant.go index fa2c115..131c399 100644 --- a/globals/constant.go +++ b/globals/constant.go @@ -23,3 +23,9 @@ const ( MidjourneyChannelType = "midjourney" OneAPIChannelType = "oneapi" ) + +const ( + NonBilling = "non-billing" + TimesBilling = "times-billing" + TokenBilling = "token-billing" +) diff --git a/globals/variables.go b/globals/variables.go index a4a2d86..030e698 100644 --- a/globals/variables.go +++ b/globals/variables.go @@ -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) -} diff --git a/manager/cache.go b/manager/cache.go index 1852a4b..a5361f9 100644 --- a/manager/cache.go +++ b/manager/cache.go @@ -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 } diff --git a/middleware/throttle.go b/middleware/throttle.go index b70fe68..1d5123f 100644 --- a/middleware/throttle.go +++ b/middleware/throttle.go @@ -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 diff --git a/utils/cache.go b/utils/cache.go index 3769a4f..55f0721 100644 --- a/utils/cache.go +++ b/utils/cache.go @@ -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 } diff --git a/utils/tokenizer.go b/utils/tokenizer.go index 2abeb08..6f01e7b 100644 --- a/utils/tokenizer.go +++ b/utils/tokenizer.go @@ -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 }