From b210268d8bec4f6f70fcfd33cb3155ea6cbaa763 Mon Sep 17 00:00:00 2001 From: Charlie Ruan <53290280+CharlieFRuan@users.noreply.github.com> Date: Mon, 5 May 2025 01:37:59 -0400 Subject: [PATCH 1/5] [Model] Add Qwen3 that allows enabling thinking --- app/client/api.ts | 1 + app/client/webllm.ts | 41 +++++++++++++++++++- app/components/settings.tsx | 14 +++++++ app/constant.ts | 75 +++++++++++++++++++++++++++++++------ app/locales/en.ts | 4 ++ app/store/chat.ts | 3 ++ app/store/config.ts | 2 + package.json | 4 +- yarn.lock | 8 ++-- 9 files changed, 133 insertions(+), 19 deletions(-) diff --git a/app/client/api.ts b/app/client/api.ts index b04cf9b8..402bac43 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -33,6 +33,7 @@ export interface LLMConfig { stream?: boolean; presence_penalty?: number; frequency_penalty?: number; + enable_thinking?: boolean; } export interface ChatOptions { diff --git a/app/client/webllm.ts b/app/client/webllm.ts index e177ca81..6b0ac302 100644 --- a/app/client/webllm.ts +++ b/app/client/webllm.ts @@ -84,6 +84,18 @@ export class WebLLMApi implements LLMApi { async chat(options: ChatOptions): Promise { if (!this.initialized || this.isDifferentConfig(options.config)) { this.llmConfig = { ...(this.llmConfig || {}), ...options.config }; + // Check if this is a Qwen3 model with thinking mode enabled + const isQwen3Model = this.llmConfig?.model?.startsWith("Qwen3"); + const isThinkingEnabled = this.llmConfig?.enable_thinking === true; + + // Apply special config for Qwen3 models with thinking mode enabled + if (isQwen3Model && isThinkingEnabled && this.llmConfig) { + this.llmConfig = { + ...this.llmConfig, + temperature: 0.6, + top_p: 0.95, + }; + } try { await this.initModel(options.onUpdate); } catch (err: any) { @@ -184,10 +196,37 @@ export class WebLLMApi implements LLMApi { usage?: CompletionUsage, ) => void, ) { + // For Qwen3 models, we need to filter out the ... content + // Do not do it inplace, create a new messages array + let newMessages: RequestMessage[] | undefined; + const isQwen3Model = this.llmConfig?.model?.startsWith("Qwen3"); + if (isQwen3Model) { + newMessages = messages.map((message) => { + const newMessage = { ...message }; + if ( + message.role === "assistant" && + typeof message.content === "string" + ) { + newMessage.content = message.content.replace( + /^[\s\S]*?<\/think>\n?\n?/, + "", + ); + } + return newMessage; + }); + } + + // Prepare extra_body with enable_thinking option for Qwen3 models + const extraBody: Record = {}; + if (isQwen3Model) { + extraBody.enable_thinking = this.llmConfig?.enable_thinking ?? false; + } + const completion = await this.webllm.engine.chatCompletion({ stream: stream, - messages: messages as ChatCompletionMessageParam[], + messages: (newMessages || messages) as ChatCompletionMessageParam[], ...(stream ? { stream_options: { include_usage: true } } : {}), + ...(Object.keys(extraBody).length > 0 ? { extra_body: extraBody } : {}), }); if (stream) { diff --git a/app/components/settings.tsx b/app/components/settings.tsx index db642511..4657ee52 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -537,6 +537,20 @@ export function Settings() { + + + updateConfig( + (config) => (config.enableThinking = e.currentTarget.checked), + ) + } + > + Date: Mon, 5 May 2025 01:52:35 -0400 Subject: [PATCH 2/5] Fix isDifferentConfig --- app/client/webllm.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/webllm.ts b/app/client/webllm.ts index 6b0ac302..141389c4 100644 --- a/app/client/webllm.ts +++ b/app/client/webllm.ts @@ -172,13 +172,14 @@ export class WebLLMApi implements LLMApi { "stream", "presence_penalty", "frequency_penalty", + "enable_thinking", ]; for (const field of optionalFields) { if ( this.llmConfig[field] !== undefined && config[field] !== undefined && - config[field] !== config[field] + this.llmConfig[field] !== config[field] ) { return true; } From bc984283f3cab33f19575698c5a8e61d8ce9c27f Mon Sep 17 00:00:00 2001 From: Nestor Qin Date: Mon, 5 May 2025 01:09:35 -0700 Subject: [PATCH 3/5] Redesign thinking toggle UIs and fix tags in chat titles --- app/components/chat.module.scss | 4 ++++ app/components/chat.tsx | 18 ++++++++++++++++-- app/components/model-config.tsx | 18 ++++++++++++++++++ app/components/settings.tsx | 14 -------------- app/locales/cn.ts | 4 ++++ app/locales/en.ts | 5 +++-- app/store/config.ts | 4 ++-- app/utils.ts | 7 +++++-- package.json | 2 +- 9 files changed, 53 insertions(+), 23 deletions(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index d9bcc00b..bd462d10 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -71,6 +71,10 @@ width: var(--icon-width); overflow: hidden; + &.selected { + background-color: var(--second); + } + &:not(:last-child) { margin-right: 5px; } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 830564b2..0cee8814 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -27,6 +27,7 @@ import DeleteIcon from "../icons/clear.svg"; import EditIcon from "../icons/rename.svg"; import ConfirmIcon from "../icons/confirm.svg"; import ImageIcon from "../icons/image.svg"; +import BrainIcon from "../icons/brain.svg"; import BottomIcon from "../icons/bottom.svg"; import StopIcon from "../icons/pause.svg"; @@ -385,6 +386,7 @@ function ChatAction(props: { icon: JSX.Element; onClick: () => void; fullWidth?: boolean; + selected?: boolean; }) { const iconRef = useRef(null); const textRef = useRef(null); @@ -406,7 +408,7 @@ function ChatAction(props: { return props.fullWidth ? (
@@ -418,7 +420,7 @@ function ChatAction(props: {
) : (
{ props.onClick(); setTimeout(updateWidth, 1); @@ -535,6 +537,18 @@ export function ChatActions(props: { }); }} /> + {config.modelConfig.model.startsWith("Qwen3") && ( + + config.update( + (config) => (config.enableThinking = !config.enableThinking), + ) + } + text={Locale.Settings.THINKING} + icon={} + selected={config.enableThinking} + /> + )} setShowModelSelector(true)} text={currentModel} diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 2ea8a068..c12154e5 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -83,6 +83,24 @@ export function ModelConfigList() { + {config.modelConfig.model.toLowerCase().startsWith("qwen3") && ( + + + config.update( + (config) => + (config.enableThinking = e.currentTarget.checked), + ) + } + > + + )} + {/* New setting item for LLM model context window length */} - - - updateConfig( - (config) => (config.enableThinking = e.currentTarget.checked), - ) - } - > - { - if (version < 0.62) { + if (version < 0.64) { return { ...DEFAULT_CONFIG, ...(persistedState as any), diff --git a/app/utils.ts b/app/utils.ts index 4d184369..35ea6ffd 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -10,11 +10,14 @@ export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language // This will remove the specified punctuation from the end of the string // and also trim quotes from both the start and end if they exist. + console.log("TrimTopic", topic); return ( topic // fix for gemini - .replace(/^["“”*]+|["“”*]+$/g, "") - .replace(/[,。!?”“"、,.!?*]*$/, "") + .replace(/^["""*]+|["""*]+$/g, "") + .replace(/[,。!?"""、,.!?*]*$/, "") + // remove think tags and content between them, including across multiple lines + .replace(/[\s\S]*?<\/think>/g, "") ); } diff --git a/package.json b/package.json index 9e54f423..699fdfbd 100644 --- a/package.json +++ b/package.json @@ -81,4 +81,4 @@ "util": false, "assert": false } -} \ No newline at end of file +} From 979162e230df360a83fb872805da46cbe40b8bf0 Mon Sep 17 00:00:00 2001 From: Nestor Qin Date: Mon, 5 May 2025 01:17:21 -0700 Subject: [PATCH 4/5] Remove empty tags in non-thinking mode --- app/store/chat.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/store/chat.ts b/app/store/chat.ts index 79fc03c3..33b713bb 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -363,6 +363,9 @@ export const useChatStore = createPersistStore( botMessage.usage = usage; botMessage.stopReason = stopReason; if (message) { + if (!this.config.enable_thinking) { + message = message.replace(/\s*<\/think>/g, ""); + } botMessage.content = message; get().onNewMessage(botMessage, llm); } From 9b628de6a4b5bdc5187bb225f98929bfb1107b9e Mon Sep 17 00:00:00 2001 From: Nestor Qin Date: Mon, 5 May 2025 01:20:59 -0700 Subject: [PATCH 5/5] Use lowercase model name checks --- app/client/webllm.ts | 8 ++++++-- app/components/chat.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/client/webllm.ts b/app/client/webllm.ts index 141389c4..9e739db1 100644 --- a/app/client/webllm.ts +++ b/app/client/webllm.ts @@ -85,7 +85,9 @@ export class WebLLMApi implements LLMApi { if (!this.initialized || this.isDifferentConfig(options.config)) { this.llmConfig = { ...(this.llmConfig || {}), ...options.config }; // Check if this is a Qwen3 model with thinking mode enabled - const isQwen3Model = this.llmConfig?.model?.startsWith("Qwen3"); + const isQwen3Model = this.llmConfig?.model + ?.toLowerCase() + .startsWith("qwen3"); const isThinkingEnabled = this.llmConfig?.enable_thinking === true; // Apply special config for Qwen3 models with thinking mode enabled @@ -200,7 +202,9 @@ export class WebLLMApi implements LLMApi { // For Qwen3 models, we need to filter out the ... content // Do not do it inplace, create a new messages array let newMessages: RequestMessage[] | undefined; - const isQwen3Model = this.llmConfig?.model?.startsWith("Qwen3"); + const isQwen3Model = this.llmConfig?.model + ?.toLowerCase() + .startsWith("qwen3"); if (isQwen3Model) { newMessages = messages.map((message) => { const newMessage = { ...message }; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 0cee8814..a363a18a 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -537,7 +537,7 @@ export function ChatActions(props: { }); }} /> - {config.modelConfig.model.startsWith("Qwen3") && ( + {config.modelConfig.model.toLowerCase().startsWith("qwen3") && ( config.update(