From 13a87275528940331efb9b976bdf81add6209d59 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 2 Sep 2025 22:16:43 +0700 Subject: [PATCH] feat: enable reasoning configuration --- .../browser/extensions/engines/AIEngine.ts | 3 +- extensions/llamacpp-extension/src/index.ts | 11 +- web-app/src/containers/Capabilities.tsx | 4 +- web-app/src/containers/ChatInput.tsx | 138 +++++++++++++++--- .../src/containers/DropdownModelProvider.tsx | 2 +- web-app/src/hooks/useChat.ts | 4 + .../src/services/__tests__/providers.test.ts | 2 +- web-app/src/services/models.ts | 28 ++-- web-app/src/services/providers.ts | 85 ++++++----- web-app/src/types/modelProviders.d.ts | 2 + web-app/src/types/models.ts | 1 + 11 files changed, 210 insertions(+), 70 deletions(-) diff --git a/core/src/browser/extensions/engines/AIEngine.ts b/core/src/browser/extensions/engines/AIEngine.ts index 7a223e468..29daf837b 100644 --- a/core/src/browser/extensions/engines/AIEngine.ts +++ b/core/src/browser/extensions/engines/AIEngine.ts @@ -8,6 +8,7 @@ export interface chatCompletionRequestMessage { content: string | null | Content[] // Content can be a string OR an array of content parts reasoning?: string | null // Some models return reasoning in completed responses reasoning_content?: string | null // Some models return reasoning in completed responses + reasoning_effort?: string | null name?: string tool_calls?: any[] // Simplified tool_call_id?: string } @@ -281,5 +282,5 @@ export abstract class AIEngine extends BaseExtension { * Check if a tool is supported by the model * @param modelId */ - abstract isToolSupported(modelId: string): Promise + abstract isModelCapabilitySupported(modelId: string, capability: string): Promise } diff --git a/extensions/llamacpp-extension/src/index.ts b/extensions/llamacpp-extension/src/index.ts index d584b3d08..5c2298f8e 100644 --- a/extensions/llamacpp-extension/src/index.ts +++ b/extensions/llamacpp-extension/src/index.ts @@ -71,6 +71,7 @@ type LlamacppConfig = { rope_scale: number rope_freq_base: number rope_freq_scale: number + reasoning_budget: boolean ctx_shift: boolean } @@ -1389,6 +1390,9 @@ export default class llamacpp_extension extends AIEngine { // This is an expert level settings and should only be used by people // who knows what they are doing. // Takes a regex with matching tensor name as input + if (!cfg.reasoning_budget) { + args.push('--reasoning-budget', '0') + } if (cfg.override_tensor_buffer_t) args.push('--override-tensor', cfg.override_tensor_buffer_t) // offload multimodal projector model to the GPU by default. if there is not enough memory @@ -1827,7 +1831,10 @@ export default class llamacpp_extension extends AIEngine { * @param modelId * @returns */ - async isToolSupported(modelId: string): Promise { + async isModelCapabilitySupported( + modelId: string, + capability: string + ): Promise { const janDataFolderPath = await getJanDataFolderPath() const modelConfigPath = await joinPath([ this.providerPath, @@ -1846,7 +1853,7 @@ export default class llamacpp_extension extends AIEngine { ]) return (await readGgufMetadata(modelPath)).metadata?.[ 'tokenizer.chat_template' - ]?.includes('tools') + ]?.includes(capability) } /** diff --git a/web-app/src/containers/Capabilities.tsx b/web-app/src/containers/Capabilities.tsx index e2e09030a..fa8bcf5b5 100644 --- a/web-app/src/containers/Capabilities.tsx +++ b/web-app/src/containers/Capabilities.tsx @@ -7,7 +7,7 @@ import { import { IconEye, IconTool, - IconAtom, + IconBrain, IconWorld, IconCodeCircle2, } from '@tabler/icons-react' @@ -30,7 +30,7 @@ const Capabilities = ({ capabilities }: CapabilitiesProps) => { } else if (capability === 'tools') { icon = } else if (capability === 'reasoning') { - icon = + icon = } else if (capability === 'embeddings') { icon = } else if (capability === 'web_search') { diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index f799f6b50..b8f61e2b4 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -12,15 +12,22 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' import { ArrowRight } from 'lucide-react' import { IconPhoto, IconWorld, - IconAtom, + IconBrain, IconTool, IconCodeCircle2, IconPlayerStopFilled, IconX, + IconChevronUp, + IconChevronDown, } from '@tabler/icons-react' import { useTranslation } from '@/i18n/react-i18next-compat' import { useGeneralSetting } from '@/hooks/useGeneralSetting' @@ -33,7 +40,7 @@ import DropdownModelProvider from '@/containers/DropdownModelProvider' import { ModelLoader } from '@/containers/loaders/ModelLoader' import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable' import { getConnectedServers } from '@/services/mcp' -import { checkMmprojExists } from '@/services/models' +import { checkMmprojExists, stopModel } from '@/services/models' type ChatInputProps = { className?: string @@ -61,6 +68,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const maxRows = 10 const { selectedModel, selectedProvider } = useModelProvider() + const { sendMessage } = useChat() const [message, setMessage] = useState('') const [dropdownToolsAvailable, setDropdownToolsAvailable] = useState(false) @@ -77,6 +85,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const [connectedServers, setConnectedServers] = useState([]) const [isDragOver, setIsDragOver] = useState(false) const [hasMmproj, setHasMmproj] = useState(false) + const [reasoningEffortOpen, setReasoningEffortOpen] = useState(false) // Check for connected MCP servers useEffect(() => { @@ -654,6 +663,114 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { )} + {selectedModel?.capabilities?.includes('reasoning') && ( +
+ + + +
{ + if ( + selectedModel?.reasoning && + selectedProvider + ) { + // Toggle reasoning budget + selectedModel.reasoning.reasoning_budget = + !selectedModel.reasoning.reasoning_budget + + // If model is loaded, restart it with new settings + try { + await stopModel(selectedModel.id) + } catch (error) { + console.error( + 'Error restarting model with new reasoning budget:', + error + ) + } + } + }} + > + +
+
+ +

+ {t('reasoning')}:{' '} + {selectedModel?.reasoning?.reasoning_budget + ? 'On' + : 'Off'} +

+
+
+
+ {selectedModel?.reasoning?.reasoning_budget && + selectedModel?.reasoning?.reasoning_effort && ( + + +
+ + {selectedModel?.reasoning?.reasoning_effort || + 'auto'} + + {reasoningEffortOpen ? ( + + ) : ( + + )} +
+
+ +
+ {['auto', 'low', 'medium', 'high'].map( + (effort) => ( +
{ + if (selectedModel?.reasoning) { + selectedModel.reasoning.reasoning_effort = + effort + setReasoningEffortOpen(false) + // Restart model with new reasoning effort + try { + await stopModel(selectedModel.id) + } catch (error) { + console.error( + 'Error restarting model with new reasoning effort:', + error + ) + } + } + }} + > + {effort} +
+ ) + )} +
+
+
+ )} +
+ )} + {selectedModel?.capabilities?.includes('tools') && hasActiveMCPServers && ( @@ -728,23 +845,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { )} - {selectedModel?.capabilities?.includes('reasoning') && ( - - - -
- -
-
- -

{t('reasoning')}

-
-
-
- )} diff --git a/web-app/src/containers/DropdownModelProvider.tsx b/web-app/src/containers/DropdownModelProvider.tsx index e47c31503..1973fc9c5 100644 --- a/web-app/src/containers/DropdownModelProvider.tsx +++ b/web-app/src/containers/DropdownModelProvider.tsx @@ -393,7 +393,7 @@ const DropdownModelProvider = ({ return ( -
+