diff --git a/adapter/adapter.go b/adapter/adapter.go index 479c5db..2419f7e 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -32,7 +32,7 @@ type ChatProps struct { Model string Message []globals.Message - Token int + MaxTokens *int PresencePenalty *float32 FrequencyPenalty *float32 RepetitionPenalty *float32 @@ -50,13 +50,9 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global switch conf.GetType() { case globals.OpenAIChannelType: return chatgpt.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&chatgpt.ChatProps{ - Model: model, - Message: props.Message, - Token: utils.Multi( - props.Token == 0, - utils.ToPtr(2500), - &props.Token, - ), + Model: model, + Message: props.Message, + Token: props.MaxTokens, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, Temperature: props.Temperature, @@ -68,13 +64,9 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global case globals.AzureOpenAIChannelType: return azure.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&azure.ChatProps{ - Model: model, - Message: props.Message, - Token: utils.Multi( - props.Token == 0, - utils.ToPtr(2500), - &props.Token, - ), + Model: model, + Message: props.Message, + Token: props.MaxTokens, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, Temperature: props.Temperature, @@ -88,7 +80,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global return claude.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&claude.ChatProps{ Model: model, Message: props.Message, - Token: utils.Multi(props.Token == 0, 50000, props.Token), + Token: props.MaxTokens, TopP: props.TopP, TopK: props.TopK, Temperature: props.Temperature, @@ -115,7 +107,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global return sparkdesk.NewChatInstance(conf, model).CreateStreamChatRequest(&sparkdesk.ChatProps{ Model: model, Message: props.Message, - Token: utils.Multi(props.Token == 0, nil, utils.ToPtr(props.Token)), + Token: props.MaxTokens, Temperature: props.Temperature, TopK: props.TopK, Tools: props.Tools, @@ -134,7 +126,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global return dashscope.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&dashscope.ChatProps{ Model: model, Message: props.Message, - Token: props.Token, + Token: props.MaxTokens, Temperature: props.Temperature, TopP: props.TopP, TopK: props.TopK, @@ -162,7 +154,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global return skylark.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&skylark.ChatProps{ Model: model, Message: props.Message, - Token: props.Token, + Token: props.MaxTokens, TopP: props.TopP, TopK: props.TopK, Temperature: props.Temperature, @@ -176,7 +168,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global return zhinao.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&zhinao.ChatProps{ Model: model, Message: props.Message, - Token: &props.Token, + Token: props.MaxTokens, TopP: props.TopP, TopK: props.TopK, Temperature: props.Temperature, @@ -193,7 +185,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global return oneapi.NewChatInstanceFromConfig(conf).CreateStreamChatRequest(&oneapi.ChatProps{ Model: model, Message: props.Message, - Token: &props.Token, + Token: props.MaxTokens, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, Temperature: props.Temperature, diff --git a/adapter/bing/chat.go b/adapter/bing/chat.go index 3ec85a1..dc2bdec 100644 --- a/adapter/bing/chat.go +++ b/adapter/bing/chat.go @@ -30,8 +30,12 @@ func (c *ChatInstance) CreateStreamChatRequest(props *ChatProps, hook globals.Ho } for { - form := utils.ReadForm[ChatResponse](conn) - if form == nil { + form, err := utils.ReadForm[ChatResponse](conn) + if err != nil { + if strings.Contains(err.Error(), "websocket: close 1000") { + return nil + } + globals.Debug(fmt.Sprintf("bing error: %s", err.Error())) return nil } diff --git a/adapter/claude/chat.go b/adapter/claude/chat.go index 3f44ef6..1309bf5 100644 --- a/adapter/claude/chat.go +++ b/adapter/claude/chat.go @@ -7,10 +7,12 @@ import ( "strings" ) +const defaultTokens = 2500 + type ChatProps struct { Model string Message []globals.Message - Token int + Token *int Temperature *float32 TopP *float32 TopK *int @@ -50,10 +52,18 @@ func (c *ChatInstance) ConvertMessage(message []globals.Message) string { return fmt.Sprintf("%s\n\nAssistant:", result) } +func (c *ChatInstance) GetTokens(props *ChatProps) int { + if props.Token == nil || *props.Token <= 0 { + return defaultTokens + } + + return *props.Token +} + func (c *ChatInstance) GetChatBody(props *ChatProps, stream bool) *ChatBody { return &ChatBody{ Prompt: c.ConvertMessage(props.Message), - MaxTokensToSample: props.Token, + MaxTokensToSample: c.GetTokens(props), Model: props.Model, Stream: stream, Temperature: props.Temperature, diff --git a/adapter/dashscope/chat.go b/adapter/dashscope/chat.go index 5bcbe41..18cd079 100644 --- a/adapter/dashscope/chat.go +++ b/adapter/dashscope/chat.go @@ -7,9 +7,11 @@ import ( "strings" ) +const defaultMaxTokens = 1500 + type ChatProps struct { Model string - Token int + Token *int Temperature *float32 TopP *float32 TopK *int @@ -41,22 +43,55 @@ func (c *ChatInstance) FormatMessages(message []globals.Message) []Message { return messages } -func (c *ChatInstance) GetChatBody(props *ChatProps) ChatRequest { - if props.Token <= 0 || props.Token > 1500 { - props.Token = 1500 +func (c *ChatInstance) GetMaxTokens(props *ChatProps) int { + // dashscope has a restriction of 1500 tokens in completion + if props.Token == nil || *props.Token <= 0 || *props.Token > 1500 { + return defaultMaxTokens } + return *props.Token +} + +func (c *ChatInstance) GetTopP(props *ChatProps) *float32 { + // range of top_p should be (0.0, 1.0) + if props.TopP == nil { + return nil + } + + if *props.TopP <= 0.0 { + return utils.ToPtr[float32](0.1) + } else if *props.TopP >= 1.0 { + return utils.ToPtr[float32](0.9) + } + + return props.TopP +} + +func (c *ChatInstance) GetRepeatPenalty(props *ChatProps) *float32 { + // range of repetition_penalty should greater than 0.0 + if props.RepetitionPenalty == nil { + return nil + } + + if *props.RepetitionPenalty <= 0.0 { + return utils.ToPtr[float32](0.1) + } + + return props.RepetitionPenalty +} + +func (c *ChatInstance) GetChatBody(props *ChatProps) ChatRequest { return ChatRequest{ Model: strings.TrimSuffix(props.Model, "-net"), Input: ChatInput{ Messages: c.FormatMessages(props.Message), }, Parameters: ChatParam{ - MaxTokens: props.Token, + MaxTokens: c.GetMaxTokens(props), Temperature: props.Temperature, - TopP: props.TopP, + TopP: c.GetTopP(props), TopK: props.TopK, - RepetitionPenalty: props.RepetitionPenalty, + RepetitionPenalty: c.GetRepeatPenalty(props), EnableSearch: utils.ToPtr(strings.HasSuffix(props.Model, "-net")), IncrementalOutput: true, }, @@ -74,6 +109,12 @@ func (c *ChatInstance) CreateStreamChatRequest(props *ChatProps, callback global c.GetHeader(), c.GetChatBody(props), func(data string) error { + // example: + // id:1 + // event:result + // :HTTP_STATUS/200 + // data:{"output":{"finish_reason":"null","text":"hi"},"usage":{"total_tokens":15,"input_tokens":14,"output_tokens":1},"request_id":"08da1369-e009-9f8f-8363-54b966f80daf"} + data = strings.TrimSpace(data) if !strings.HasPrefix(data, "data:") { return nil diff --git a/adapter/skylark/chat.go b/adapter/skylark/chat.go index 063a852..ee95c7a 100644 --- a/adapter/skylark/chat.go +++ b/adapter/skylark/chat.go @@ -8,10 +8,12 @@ import ( "github.com/volcengine/volc-sdk-golang/service/maas/models/api" ) +const defaultMaxTokens int64 = 1500 + type ChatProps struct { Model string Message []globals.Message - Token int + Token *int PresencePenalty *float32 FrequencyPenalty *float32 @@ -37,6 +39,14 @@ func getMessages(messages []globals.Message) []*api.Message { }) } +func (c *ChatInstance) GetMaxTokens(token *int) int64 { + if token == nil || *token < 0 { + return defaultMaxTokens + } + + return int64(*token) +} + func (c *ChatInstance) CreateRequest(props *ChatProps) *api.ChatReq { return &api.ChatReq{ Model: &api.Model{ @@ -50,7 +60,7 @@ func (c *ChatInstance) CreateRequest(props *ChatProps) *api.ChatReq { PresencePenalty: utils.GetPtrVal(props.PresencePenalty, 0.), FrequencyPenalty: utils.GetPtrVal(props.FrequencyPenalty, 0.), RepetitionPenalty: utils.GetPtrVal(props.RepeatPenalty, 0.), - MaxTokens: int64(props.Token), + MaxTokens: c.GetMaxTokens(props.Token), }, Functions: getFunctions(props.Tools), } diff --git a/adapter/sparkdesk/chat.go b/adapter/sparkdesk/chat.go index c5bfdc1..583a7e6 100644 --- a/adapter/sparkdesk/chat.go +++ b/adapter/sparkdesk/chat.go @@ -4,6 +4,7 @@ import ( "chat/globals" "chat/utils" "fmt" + "strings" ) type ChatProps struct { @@ -116,8 +117,12 @@ func (c *ChatInstance) CreateStreamChatRequest(props *ChatProps, hook globals.Ho } for { - form := utils.ReadForm[ChatResponse](conn) - if form == nil { + form, err := utils.ReadForm[ChatResponse](conn) + if err != nil { + if strings.Contains(err.Error(), "websocket: close 1000") { + return nil + } + globals.Debug(fmt.Sprintf("sparkdesk error: %s", err.Error())) return nil } diff --git a/addition/article/api.go b/addition/article/api.go index 00a7dd8..7bf6fec 100644 --- a/addition/article/api.go +++ b/addition/article/api.go @@ -40,8 +40,8 @@ func GenerateAPI(c *gin.Context) { } defer conn.DeferClose() - var form *WebsocketArticleForm - if form = utils.ReadForm[WebsocketArticleForm](conn); form == nil { + form, err := utils.ReadForm[WebsocketArticleForm](conn) + if err != nil { return } diff --git a/addition/generation/api.go b/addition/generation/api.go index 4cdb8db..c75fe89 100644 --- a/addition/generation/api.go +++ b/addition/generation/api.go @@ -35,8 +35,8 @@ func GenerateAPI(c *gin.Context) { } defer conn.DeferClose() - var form *WebsocketGenerationForm - if form = utils.ReadForm[WebsocketGenerationForm](conn); form == nil { + form, err := utils.ReadForm[WebsocketGenerationForm](conn) + if err != nil { return } diff --git a/app/package.json b/app/package.json index 95450c7..ea2fd07 100644 --- a/app/package.json +++ b/app/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 62298d6..07d9eca 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: '@radix-ui/react-separator': 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-slider': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.33)(react@18.2.0) @@ -1482,6 +1485,37 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-slider@1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==} + 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/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 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-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-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-use-controllable-state': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 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-slot@1.0.0(react@18.2.0): resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} peerDependencies: diff --git a/app/src/api/connection.ts b/app/src/api/connection.ts index 3a3e4d5..04b92bc 100644 --- a/app/src/api/connection.ts +++ b/app/src/api/connection.ts @@ -3,7 +3,7 @@ import { getMemory } from "@/utils/memory.ts"; import { getErrorMessage } from "@/utils/base.ts"; export const endpoint = `${websocketEndpoint}/chat`; -export const maxRetry = 30; // 15s max websocket retry +export const maxRetry = 60; // 30s max websocket retry export type StreamMessage = { conversation?: number; @@ -21,6 +21,14 @@ export type ChatProps = { web?: boolean; context?: number; ignore_context?: boolean; + + max_tokens?: number; + temperature?: number; + top_p?: number; + top_k?: number; + presence_penalty?: number; + frequency_penalty?: number; + repetition_penalty?: number; }; type StreamCallback = (message: StreamMessage) => void; diff --git a/app/src/assets/pages/settings.less b/app/src/assets/pages/settings.less index 37e3a27..9854e8d 100644 --- a/app/src/assets/pages/settings.less +++ b/app/src/assets/pages/settings.less @@ -60,13 +60,21 @@ transition: .1s; } + .slider-value { + min-width: 2rem; + } + input { text-align: center; max-width: 4rem; max-height: 1.75rem; + + &.large-value { + max-width: 6rem; + } } - button { + button:not(.tips-trigger):not(.set-action) { margin: 0.25rem 1rem; } @@ -81,7 +89,14 @@ } .name { + display: flex; + flex-direction: row; + align-items: center; white-space: nowrap; + + .tips-trigger { + transform: translateY(1px); + } } } diff --git a/app/src/components/Tips.tsx b/app/src/components/Tips.tsx index fda099a..2a9bd6f 100644 --- a/app/src/components/Tips.tsx +++ b/app/src/components/Tips.tsx @@ -5,7 +5,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip.tsx"; import { HelpCircle } from "lucide-react"; -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useRef } from "react"; import { cn } from "@/components/ui/lib/utils.ts"; import { DropdownMenu, @@ -44,23 +44,30 @@ function Tips({ const [drop, setDrop] = React.useState(false); const [tooltip, setTooltip] = React.useState(false); + const task = useRef(); + useEffect(() => { - drop && setTimeout(() => setDrop(false), timeout); + drop + ? (task.current = setTimeout(() => setDrop(false), timeout)) + : clearTimeout(task.current); }, [drop]); useEffect(() => { - tooltip && drop && setTooltip(false); + if (!tooltip) return; + + setTooltip(false); + !drop && setDrop(true); }, [drop, tooltip]); return ( - + {trigger ?? } - {comp} + diff --git a/app/src/components/home/ChatWrapper.tsx b/app/src/components/home/ChatWrapper.tsx index cea4d42..df1aca1 100644 --- a/app/src/components/home/ChatWrapper.tsx +++ b/app/src/components/home/ChatWrapper.tsx @@ -21,7 +21,14 @@ import { ToastAction } from "@/components/ui/toast.tsx"; import { alignSelector, contextSelector, + frequencyPenaltySelector, historySelector, + maxTokensSelector, + presencePenaltySelector, + repetitionPenaltySelector, + temperatureSelector, + topKSelector, + topPSelector, } from "@/store/settings.ts"; import { FileArray } from "@/api/file.ts"; import { @@ -76,6 +83,14 @@ function ChatWrapper() { const context = useSelector(contextSelector); const align = useSelector(alignSelector); + const max_tokens = useSelector(maxTokensSelector); + const temperature = useSelector(temperatureSelector); + const top_p = useSelector(topPSelector); + const top_k = useSelector(topKSelector); + const presence_penalty = useSelector(presencePenaltySelector); + const frequency_penalty = useSelector(frequencyPenaltySelector); + const repetition_penalty = useSelector(repetitionPenaltySelector); + const requireAuth = useMemo( (): boolean => !!getModelFromId(model)?.auth, [model], @@ -113,6 +128,13 @@ function ChatWrapper() { model, context: history, ignore_context: !context, + max_tokens, + temperature, + top_p, + top_k, + presence_penalty, + frequency_penalty, + repetition_penalty, }) ) { forgetMemory("history"); diff --git a/app/src/components/ui/number-input.tsx b/app/src/components/ui/number-input.tsx index b1219f5..748fd03 100644 --- a/app/src/components/ui/number-input.tsx +++ b/app/src/components/ui/number-input.tsx @@ -32,6 +32,8 @@ const NumberInput = React.forwardRef( }; const formatValue = (v: string) => { + if (v.trim().length === 0) return v.trim(); + if (!/^[-+]?(?:[0-9]*(?:\.[0-9]*)?)?$/.test(v)) { const exp = /[-+]?[0-9]+(\.[0-9]+)?/g; return v.match(exp)?.join("") || ""; @@ -43,7 +45,7 @@ const NumberInput = React.forwardRef( const raw = getNumber(v, props.acceptNegative); let val = parseFloat(raw); - if (isNaN(val) && !props.acceptNaN) return "0"; + if (isNaN(val) && !props.acceptNaN) return (props.min ?? 0).toString(); if (props.max !== undefined && val > props.max) return props.max.toString(); else if (props.min !== undefined && val < props.min) diff --git a/app/src/components/ui/slider.tsx b/app/src/components/ui/slider.tsx new file mode 100644 index 0000000..1ea32b0 --- /dev/null +++ b/app/src/components/ui/slider.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import { cn } from "@/components/ui/lib/utils"; + +type SliderProps = { + classNameThumb?: string; +}; + +const Slider = React.forwardRef< + React.ElementRef & SliderProps, + React.ComponentPropsWithoutRef & SliderProps +>(({ className, classNameThumb, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/app/src/dialogs/SettingsDialog.tsx b/app/src/dialogs/SettingsDialog.tsx index 59357c0..1ca1d0c 100644 --- a/app/src/dialogs/SettingsDialog.tsx +++ b/app/src/dialogs/SettingsDialog.tsx @@ -1,19 +1,7 @@ import "@/assets/pages/settings.less"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import { - alignSelector, - contextSelector, - dialogSelector, - historySelector, - senderSelector, - sendKeys, - setAlign, - setContext, - setDialog, - setHistory, - setSender, -} from "@/store/settings.ts"; +import * as settings from "@/store/settings.ts"; import { Dialog, DialogContent, @@ -36,16 +24,39 @@ import { import { langsProps, setLanguage } from "@/i18n.ts"; import { cn } from "@/components/ui/lib/utils.ts"; import Github from "@/components/ui/icons/Github.tsx"; +import { Slider } from "@/components/ui/slider.tsx"; +import Tips from "@/components/Tips.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog.tsx"; function SettingsDialog() { const { t, i18n } = useTranslation(); const dispatch = useDispatch(); - const open = useSelector(dialogSelector); + const open = useSelector(settings.dialogSelector); + + const align = useSelector(settings.alignSelector); + const context = useSelector(settings.contextSelector); + const sender = useSelector(settings.senderSelector); + const history = useSelector(settings.historySelector); + + const temperature = useSelector(settings.temperatureSelector); + const maxTokens = useSelector(settings.maxTokensSelector); + const topP = useSelector(settings.topPSelector); + const topK = useSelector(settings.topKSelector); + const presencePenalty = useSelector(settings.presencePenaltySelector); + const frequencyPenalty = useSelector(settings.frequencyPenaltySelector); + const repetitionPenalty = useSelector(settings.repetitionPenaltySelector); - const align = useSelector(alignSelector); - const context = useSelector(contextSelector); - const sender = useSelector(senderSelector); - const history = useSelector(historySelector); const [memorySize, setMemorySize] = useState(getMemoryPerformance()); useEffect(() => { @@ -57,7 +68,10 @@ function SettingsDialog() { }, []); return ( - dispatch(setDialog(open))}> + dispatch(settings.setDialog(open))} + > {t("settings.title")} @@ -106,15 +120,21 @@ function SettingsDialog() { @@ -126,7 +146,7 @@ function SettingsDialog() { className={`value`} checked={align} onCheckedChange={(state: boolean) => { - dispatch(setAlign(state)); + dispatch(settings.setAlign(state)); }} /> @@ -137,7 +157,7 @@ function SettingsDialog() { className={`value`} checked={context} onCheckedChange={(state: boolean) => { - dispatch(setContext(state)); + dispatch(settings.setContext(state)); }} /> @@ -155,11 +175,187 @@ function SettingsDialog() { min={0} max={999} onValueChange={(value: number) => { - dispatch(setHistory(value)); + dispatch(settings.setHistory(value)); }} /> )} +
+
+ {t("settings.max-tokens")} + +
+
+ { + dispatch(settings.setMaxTokens(value)); + }} + /> +
+
+
+
+
+ {t("settings.temperature")} + +
+
+ { + dispatch(settings.setTemperature(value[0] / 10)); + }} + /> +

{temperature.toFixed(1)}

+
+
+
+ {t("settings.presence-penalty")} + +
+
+ { + dispatch(settings.setPresencePenalty(value[0] / 10)); + }} + /> +

+ {presencePenalty.toFixed(1)} +

+
+
+
+ {t("settings.frequency-penalty")} + +
+
+ { + dispatch(settings.setFrequencyPenalty(value[0] / 10)); + }} + /> +

+ {frequencyPenalty.toFixed(1)} +

+
+
+
+ {t("settings.repetition-penalty")} + +
+
+ { + dispatch(settings.setRepetitionPenalty(value[0] / 10)); + }} + /> +

+ {repetitionPenalty.toFixed(1)} +

+
+
+
+ {t("settings.top-p")} + +
+
+ { + dispatch(settings.setTopP(value[0] / 10)); + }} + /> +

{topP.toFixed(1)}

+
+
+
+ {t("settings.top-k")} + +
+
+ { + dispatch(settings.setTopK(value[0])); + }} + /> +

{topK.toFixed()}

+
+
+
+
+
{t("settings.reset-settings")}
+
+ + + + + + + + {t("settings.reset-settings")} + + + {t("settings.reset-settings-description")} + + + {t("cancel")} + { + dispatch(settings.resetSettings()); + }} + > + {t("confirm")} + + + + + +
diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index 40f1c55..71a0b3f 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -332,7 +332,23 @@ "context": "保留上下文", "history": "最大历史会话数", "align": "聊天框居中", - "memory": "内存占用" + "memory": "内存占用", + "max-tokens": "最大回复 Token 数", + "max-tokens-tip": "最大回复 Token 数,超过此数值将会被截断", + "temperature": "温度", + "temperature-tip": "随机采样的比例,高温度会产生更多的随机性,低温度会产生较集中和确定性的文本", + "top-p": "核采样概率阈值", + "top-p-tip": "(TopP) 概率取值越大,生成的随机性越高;取值越低,生成的确定性越高", + "top-k": "采样候选集大小", + "top-k-tip": "(TopK) 候选集大小,越大生成的随机性越高,越小生成的确定性越高", + "presence-penalty": "存在惩罚", + "presence-penalty-tip": "(PresencePenalty) 存在惩罚,控制模型生成的新话题的可能性,提高此值可以增加谈论新话题的可能性", + "frequency-penalty": "频率惩罚", + "frequency-penalty-tip": "(FrequencyPenalty) 频率惩罚,控制模型生成字词的重复程度,提高此值可以降低重复字词出现频率的可能性", + "repetition-penalty": "重复惩罚", + "repetition-penalty-tip": "(RepetitionPenalty) 控制模型生成的重复程度,提高此值可以减少重复,但是可能会导致模型生成不连贯的文本(与 FrequencyPenalty 相似)", + "reset-settings": "重置全部设置", + "reset-settings-description": "是否确定?此操作无法撤消。这将永久重置全部设置。" }, "article": { "title": "批量生成文章", diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index 77d8284..1d159a9 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -281,7 +281,23 @@ "context": "Keep Context", "history": "Max History Conversations", "align": "Chatbox Centered", - "memory": "Memory Usage" + "memory": "Memory Usage", + "temperature": "temperature", + "temperature-tip": "Random sampling ratio, high temperature produces more randomness, low temperature produces more concentrated and deterministic text", + "max-tokens": "Maximum number of response tokens", + "max-tokens-tip": "Maximum number of reply tokens, exceeding this value will be truncated", + "top-p": "Kernel Sampling Probability Threshold", + "top-p-tip": "(TopP) The higher the probability value, the higher the randomness generated; the lower the value, the higher the certainty generated", + "top-k": "Sample Candidate Set Size", + "top-k-tip": "(TopK) Candidate set size, the larger the randomness of the generation, the smaller the generation, the higher the certainty", + "presence-penalty": "Existence of penalties", + "presence-penalty-tip": "(PresencePenalty) There is a penalty for controlling the likelihood of new topics generated by the model, increasing this value can increase the likelihood of talking about new topics", + "frequency-penalty": "Frequency penalty", + "frequency-penalty-tip": "(FrequencyPenalty) Frequency penalty, control the degree of repetition of words generated by the model, increasing this value can reduce the possibility of repetition of words", + "repetition-penalty": "Duplicate Punishment", + "repetition-penalty-tip": "(RepetitionPenalty) Controls the degree of repetition generated by the model. Increasing this value can reduce repetition, but may cause the model to generate incoherent text (similar to FrequencyPenalty)", + "reset-settings": "Reset all settings", + "reset-settings-description": "Are you sure? This action cannot be undone. This will permanently reset all settings." }, "article": { "title": "Batch Generate Articles", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index 22f5a72..12464a2 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -281,7 +281,23 @@ "context": "コンテキストを保持", "history": "最大履歴セッション数", "align": "チャットボックスを中央に配置", - "memory": "メモリ使用量" + "memory": "メモリ使用量", + "temperature": "温度", + "temperature-tip": "ランダムサンプリング比、高温はよりランダム性を生み、低温はより集中的で決定論的なテキストを生成します", + "max-tokens": "レスポンストークンの最大数", + "max-tokens-tip": "この値を超える返信トークンの最大数は切り捨てられます", + "top-p": "カーネルサンプリング確率閾値", + "top-p-tip": "( TopP )確率値が高いほど生成されるランダム性が高く、値が低いほど生成される確実性が高くなります", + "top-k": "サンプル候補セットサイズ", + "top-k-tip": "(TopK)候補セットサイズ、生成のランダム性が大きいほど生成が小さいほど確実性が高い", + "presence-penalty": "ペナルティの存在", + "presence-penalty-tip": "(PresencePenalty)モデルによって生成された新しいトピックの可能性を制御するためのペナルティがあります。この値を増やすと、新しいトピックについて話す可能性が高くなります", + "frequency-penalty": "フリークエンシー・パニュメント", + "frequency-penalty-tip": "( FrequencyPenalty )周波数ペナルティ、モデルによって生成された単語の繰り返しの度合いを制御し、この値を増やすことで単語の繰り返しの可能性を減らすことができます", + "repetition-penalty": "重複した処罰", + "repetition-penalty-tip": "(RepetitionPenalty)モデルによって生成される繰り返しの度合いを制御します。この値を大きくすると、繰り返しを減らすことができますが、モデルが不整合のテキストを生成する可能性があります(FrequencyPenaltyと同様)", + "reset-settings": "すべての設定をリセット", + "reset-settings-description": "本当によろしいですか?この操作は元に戻せません。これにより、すべての設定が完全にリセットされます。" }, "article": { "title": "投稿の一括生成", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 6d853ba..f01581b 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -281,7 +281,23 @@ "context": "Сохранить контекст", "history": "Максимальное количество исторических разговоров", "align": "Выравнивание чата по центру", - "memory": "Использование памяти" + "memory": "Использование памяти", + "temperature": "Температура", + "temperature-tip": "Коэффициент случайной выборки, высокая температура создает больше случайности, низкая температура создает более концентрированный и детерминированный текст", + "max-tokens": "Максимальное количество маркеров ответа", + "max-tokens-tip": "Максимальное количество маркеров ответа, превышающее это значение, будет усечено", + "top-p": "Порог вероятности отбора проб ядра", + "top-p-tip": "(TopP) Чем выше значение вероятности, тем выше генерируемая случайность; чем ниже значение, тем выше генерируемая определенность", + "top-k": "Размер набора образцов-кандидатов", + "top-k-tip": "(TopK) Размер набора кандидатов, чем больше случайность генерации, чем меньше генерация, тем выше определенность", + "presence-penalty": "Наличие штрафных санкций", + "presence-penalty-tip": "(PresencePenalty) Существует штраф за контроль вероятности появления новых тем, генерируемых моделью, увеличение этого значения может увеличить вероятность разговора о новых темах", + "frequency-penalty": "Частотное наказание", + "frequency-penalty-tip": "(FrequencyPenalty) Штраф за частоту, контроль степени повторения слов, генерируемых моделью, увеличение этого значения может снизить возможность повторения слов", + "repetition-penalty": "Повторяющееся наказание", + "repetition-penalty-tip": "(RepetitionPenalty) Управляет степенью повторяемости, генерируемой моделью. Увеличение этого значения может уменьшить повторение, но может привести к тому, что модель будет генерировать некогерентный текст (аналогично FrequencyPenalty)", + "reset-settings": "Сбросить все настройки", + "reset-settings-description": "Вы уверены? Это действие нельзя отменить. Это приведет к окончательному сбросу всех настроек." }, "article": { "title": "Пакет генерации статей", diff --git a/app/src/store/settings.ts b/app/src/store/settings.ts index 527e16b..da14476 100644 --- a/app/src/store/settings.ts +++ b/app/src/store/settings.ts @@ -8,6 +8,20 @@ import { import { RootState } from "@/store/index.ts"; export const sendKeys = ["Ctrl + Enter", "Enter"]; +export const initialSettings = { + context: true, + align: false, + history: 8, + sender: false, + max_tokens: 2000, + temperature: 0.6, + top_p: 1, + top_k: 5, + presence_penalty: 0, + frequency_penalty: 0, + repetition_penalty: 1, +}; + export const settingsSlice = createSlice({ name: "settings", initialState: { @@ -16,6 +30,13 @@ export const settingsSlice = createSlice({ align: getBooleanMemory("align", false), // chat textarea align center history: getNumberMemory("history_context", 8), // max history context length sender: getBooleanMemory("sender", false), // sender (false: Ctrl + Enter, true: Enter) + max_tokens: getNumberMemory("max_tokens", 2000), // max tokens + temperature: getNumberMemory("temperature", 0.6), // temperature + top_p: getNumberMemory("top_p", 1), // top_p + top_k: getNumberMemory("top_k", 5), // top_k + presence_penalty: getNumberMemory("presence_penalty", 0), // presence_penalty + frequency_penalty: getNumberMemory("frequency_penalty", 0), // frequency_penalty + repetition_penalty: getNumberMemory("repetition_penalty", 1), // repetition_penalty }, reducers: { toggleDialog: (state) => { @@ -46,6 +67,59 @@ export const settingsSlice = createSlice({ state.sender = action.payload as boolean; setBooleanMemory("sender", action.payload); }, + setMaxTokens: (state, action) => { + state.max_tokens = action.payload as number; + setNumberMemory("max_tokens", action.payload); + }, + setTemperature: (state, action) => { + state.temperature = action.payload as number; + setNumberMemory("temperature", action.payload); + }, + setTopP: (state, action) => { + state.top_p = action.payload as number; + setNumberMemory("top_p", action.payload); + }, + setTopK: (state, action) => { + state.top_k = action.payload as number; + setNumberMemory("top_k", action.payload); + }, + setPresencePenalty: (state, action) => { + state.presence_penalty = action.payload as number; + setNumberMemory("presence_penalty", action.payload); + }, + setFrequencyPenalty: (state, action) => { + state.frequency_penalty = action.payload as number; + setNumberMemory("frequency_penalty", action.payload); + }, + setRepetitionPenalty: (state, action) => { + state.repetition_penalty = action.payload as number; + setNumberMemory("repetition_penalty", action.payload); + }, + resetSettings: (state) => { + state.context = initialSettings.context; + state.align = initialSettings.align; + state.history = initialSettings.history; + state.sender = initialSettings.sender; + state.max_tokens = initialSettings.max_tokens; + state.temperature = initialSettings.temperature; + state.top_p = initialSettings.top_p; + state.top_k = initialSettings.top_k; + state.presence_penalty = initialSettings.presence_penalty; + state.frequency_penalty = initialSettings.frequency_penalty; + state.repetition_penalty = initialSettings.repetition_penalty; + + setBooleanMemory("context", initialSettings.context); + setBooleanMemory("align", initialSettings.align); + setNumberMemory("history_context", initialSettings.history); + setBooleanMemory("sender", initialSettings.sender); + setNumberMemory("max_tokens", initialSettings.max_tokens); + setNumberMemory("temperature", initialSettings.temperature); + setNumberMemory("top_p", initialSettings.top_p); + setNumberMemory("top_k", initialSettings.top_k); + setNumberMemory("presence_penalty", initialSettings.presence_penalty); + setNumberMemory("frequency_penalty", initialSettings.frequency_penalty); + setNumberMemory("repetition_penalty", initialSettings.repetition_penalty); + }, }, }); @@ -58,6 +132,14 @@ export const { setAlign, setHistory, setSender, + setMaxTokens, + setTemperature, + setTopP, + setTopK, + setPresencePenalty, + setFrequencyPenalty, + setRepetitionPenalty, + resetSettings, } = settingsSlice.actions; export default settingsSlice.reducer; @@ -71,3 +153,15 @@ export const historySelector = (state: RootState): number => state.settings.history; export const senderSelector = (state: RootState): boolean => state.settings.sender; +export const maxTokensSelector = (state: RootState): number => + state.settings.max_tokens; +export const temperatureSelector = (state: RootState): number => + state.settings.temperature; +export const topPSelector = (state: RootState): number => state.settings.top_p; +export const topKSelector = (state: RootState): number => state.settings.top_k; +export const presencePenaltySelector = (state: RootState): number => + state.settings.presence_penalty; +export const frequencyPenaltySelector = (state: RootState): number => + state.settings.frequency_penalty; +export const repetitionPenaltySelector = (state: RootState): number => + state.settings.repetition_penalty; diff --git a/channel/channel.go b/channel/channel.go index ce1edd4..7ae9dd1 100644 --- a/channel/channel.go +++ b/channel/channel.go @@ -11,7 +11,8 @@ import ( var defaultMaxRetries = 1 var defaultReplacer = []string{ "openai_api", "anthropic_api", - "api2d", "closeai_api", "one_api", "new_api", + "api2d", "closeai_api", + "one_api", "new_api", "shell_api", } func (c *Channel) GetId() int { diff --git a/manager/chat.go b/manager/chat.go index f5da7a0..ae9c53a 100644 --- a/manager/chat.go +++ b/manager/chat.go @@ -95,9 +95,16 @@ func ChatHandler(conn *Connection, user *auth.User, instance *conversation.Conve err := channel.NewChatRequest( auth.GetGroup(db, user), &adapter.ChatProps{ - Model: model, - Message: segment, - Buffer: *buffer, + Model: model, + Message: segment, + Buffer: *buffer, + MaxTokens: instance.GetMaxTokens(), + Temperature: instance.GetTemperature(), + TopP: instance.GetTopP(), + TopK: instance.GetTopK(), + PresencePenalty: instance.GetPresencePenalty(), + FrequencyPenalty: instance.GetFrequencyPenalty(), + RepetitionPenalty: instance.GetRepetitionPenalty(), }, func(data string) error { if signal := conn.PeekWithType(StopType); signal != nil { diff --git a/manager/chat_completions.go b/manager/chat_completions.go index 89ae41a..918fd78 100644 --- a/manager/chat_completions.go +++ b/manager/chat_completions.go @@ -71,7 +71,7 @@ func getChatProps(form RelayForm, messages []globals.Message, buffer *utils.Buff return &adapter.ChatProps{ Model: form.Model, Message: messages, - Token: utils.Multi(form.MaxTokens == 0, 2500, form.MaxTokens), + MaxTokens: form.MaxTokens, PresencePenalty: form.PresencePenalty, FrequencyPenalty: form.FrequencyPenalty, RepetitionPenalty: form.RepetitionPenalty, diff --git a/manager/connection.go b/manager/connection.go index 94821b4..d800f8d 100644 --- a/manager/connection.go +++ b/manager/connection.go @@ -56,8 +56,8 @@ func (c *Connection) ReadWorker() { break } - form := utils.ReadForm[conversation.FormMessage](c.conn) - if form == nil { + form, err := utils.ReadForm[conversation.FormMessage](c.conn) + if err != nil { break } diff --git a/manager/conversation/conversation.go b/manager/conversation/conversation.go index 1d4420c..d891e4a 100644 --- a/manager/conversation/conversation.go +++ b/manager/conversation/conversation.go @@ -22,6 +22,14 @@ type Conversation struct { EnableWeb bool `json:"enable_web"` Shared bool `json:"shared"` Context int `json:"context"` + + MaxTokens *int `json:"max_tokens,omitempty"` + Temperature *float32 `json:"temperature,omitempty"` + TopP *float32 `json:"top_p,omitempty"` + TopK *int `json:"top_k,omitempty"` + PresencePenalty *float32 `json:"presence_penalty,omitempty"` + FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` + RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` } type FormMessage struct { @@ -31,6 +39,15 @@ type FormMessage struct { Model string `json:"model"` IgnoreContext bool `json:"ignore_context"` Context int `json:"context"` + + // request params + MaxTokens *int `json:"max_tokens,omitempty"` + Temperature *float32 `json:"temperature,omitempty"` + TopP *float32 `json:"top_p,omitempty"` + TopK *int `json:"top_k,omitempty"` + PresencePenalty *float32 `json:"presence_penalty,omitempty"` + FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` + RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` } func NewAnonymousConversation() *Conversation { @@ -106,7 +123,69 @@ func (c *Conversation) SetEnableWeb(enable bool) { c.EnableWeb = enable } -func (c *Conversation) SetContextLength(context int) { +func (c *Conversation) GetTemperature() *float32 { + return c.Temperature +} + +func (c *Conversation) SetTemperature(temperature *float32) { + c.Temperature = temperature +} + +func (c *Conversation) GetTopP() *float32 { + return c.TopP +} + +func (c *Conversation) SetTopP(topP *float32) { + c.TopP = topP +} + +func (c *Conversation) GetTopK() *int { + return c.TopK +} + +func (c *Conversation) SetTopK(topK *int) { + c.TopK = topK +} + +func (c *Conversation) GetPresencePenalty() *float32 { + return c.PresencePenalty +} + +func (c *Conversation) SetPresencePenalty(presencePenalty *float32) { + c.PresencePenalty = presencePenalty +} + +func (c *Conversation) GetFrequencyPenalty() *float32 { + return c.FrequencyPenalty +} + +func (c *Conversation) SetFrequencyPenalty(frequencyPenalty *float32) { + c.FrequencyPenalty = frequencyPenalty +} + +func (c *Conversation) GetRepetitionPenalty() *float32 { + return c.RepetitionPenalty +} + +func (c *Conversation) SetRepetitionPenalty(repetitionPenalty *float32) { + c.RepetitionPenalty = repetitionPenalty +} + +func (c *Conversation) GetMaxTokens() *int { + return c.MaxTokens +} + +func (c *Conversation) SetMaxTokens(maxTokens *int) { + c.MaxTokens = maxTokens +} + +func (c *Conversation) SetContextLength(context int, ignore bool) { + if ignore { + context = 1 + } else if context <= 0 { + context = defaultConversationContext + } + c.Context = context } @@ -211,6 +290,20 @@ func GetMessage(data []byte) (string, error) { return form.Message, nil } +func (c *Conversation) ApplyParam(form *FormMessage) { + c.SetModel(form.Model) + c.SetEnableWeb(form.Web) + c.SetContextLength(form.Context, form.IgnoreContext) + + c.SetMaxTokens(form.MaxTokens) + c.SetTemperature(form.Temperature) + c.SetTopP(form.TopP) + c.SetTopK(form.TopK) + c.SetPresencePenalty(form.PresencePenalty) + c.SetFrequencyPenalty(form.FrequencyPenalty) + c.SetRepetitionPenalty(form.RepetitionPenalty) +} + func (c *Conversation) AddMessageFromByte(data []byte) (string, error) { form, err := utils.Unmarshal[FormMessage](data) if err != nil { @@ -220,16 +313,8 @@ func (c *Conversation) AddMessageFromByte(data []byte) (string, error) { } c.AddMessageFromUser(form.Message) - c.SetModel(form.Model) - c.SetEnableWeb(form.Web) + c.ApplyParam(&form) - if form.IgnoreContext { - form.Context = 1 - } else if form.Context <= 0 { - form.Context = defaultConversationContext - } - - c.SetContextLength(form.Context) return form.Message, nil } @@ -239,15 +324,8 @@ func (c *Conversation) AddMessageFromForm(form *FormMessage) error { } c.AddMessageFromUser(form.Message) - c.SetModel(form.Model) - c.SetEnableWeb(form.Web) - if form.IgnoreContext { - form.Context = 1 - } else if form.Context <= 0 { - form.Context = defaultConversationContext - } + c.ApplyParam(form) - c.SetContextLength(form.Context) return nil } diff --git a/manager/images.go b/manager/images.go index 0f43e1c..b925e73 100644 --- a/manager/images.go +++ b/manager/images.go @@ -59,10 +59,10 @@ func ImagesRelayAPI(c *gin.Context) { func getImageProps(form RelayImageForm, messages []globals.Message, buffer *utils.Buffer) *adapter.ChatProps { return &adapter.ChatProps{ - Model: form.Model, - Message: messages, - Token: 2500, - Buffer: *buffer, + Model: form.Model, + Message: messages, + MaxTokens: utils.ToPtr(-1), + Buffer: *buffer, } } diff --git a/manager/manager.go b/manager/manager.go index 7d9b9af..1e7305b 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -62,8 +62,8 @@ func ChatAPI(c *gin.Context) { db := utils.GetDBFromContext(c) - var form *WebsocketAuthForm - if form = utils.ReadForm[WebsocketAuthForm](conn); form == nil { + form, err := utils.ReadForm[WebsocketAuthForm](conn) + if err != nil { return } diff --git a/manager/types.go b/manager/types.go index 1aa0016..0437b76 100644 --- a/manager/types.go +++ b/manager/types.go @@ -30,7 +30,7 @@ type RelayForm struct { Model string `json:"model" binding:"required"` Messages []Message `json:"messages" binding:"required"` Stream bool `json:"stream"` - MaxTokens int `json:"max_tokens"` + MaxTokens *int `json:"max_tokens"` PresencePenalty *float32 `json:"presence_penalty"` FrequencyPenalty *float32 `json:"frequency_penalty"` RepetitionPenalty *float32 `json:"repetition_penalty"` diff --git a/utils/websocket.go b/utils/websocket.go index 130b9da..e0cc945 100644 --- a/utils/websocket.go +++ b/utils/websocket.go @@ -168,19 +168,19 @@ func (w *WebSocket) IsClosed() bool { return w.Closed } -func ReadForm[T interface{}](w *WebSocket) *T { +func ReadForm[T interface{}](w *WebSocket) (*T, error) { // golang cannot use generic type in class-like struct // except ping _, message, err := w.Read() if err != nil { - return nil + return nil, err } else if string(message) == "{\"type\":\"ping\"}" { return ReadForm[T](w) } form, err := Unmarshal[T](message) if err != nil { - return nil + return nil, err } - return &form + return &form, nil }