1- import { IconSettings } from '@tabler/icons-react'
1+ import { IconSettings , IconLoader } from '@tabler/icons-react'
22import debounce from 'lodash.debounce'
3+ import { useState } from 'react'
34
45import {
56 Sheet ,
@@ -9,6 +10,7 @@ import {
910 SheetTitle ,
1011 SheetTrigger ,
1112} from '@/components/ui/sheet'
13+ import { Button } from '@/components/ui/button'
1214import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting'
1315import { useModelProvider } from '@/hooks/useModelProvider'
1416import { useServiceHub } from '@/hooks/useServiceHub'
@@ -30,11 +32,134 @@ export function ModelSetting({
3032 const { t } = useTranslation ( )
3133 const serviceHub = useServiceHub ( )
3234
35+ const [ isPlanning , setIsPlanning ] = useState ( false )
36+
3337 // Create a debounced version of stopModel that waits 500ms after the last call
3438 const debouncedStopModel = debounce ( ( modelId : string ) => {
3539 serviceHub . models ( ) . stopModel ( modelId )
3640 } , 500 )
3741
42+ const handlePlanModelLoad = async ( ) => {
43+ if ( provider . provider !== 'llamacpp' ) {
44+ console . warn ( 'planModelLoad is only available for llamacpp provider' )
45+ return
46+ }
47+ setIsPlanning ( true )
48+ try {
49+ // Read the model config to get the actual model path
50+ const modelConfig = await serviceHub . app ( ) . readYaml < {
51+ model_path : string
52+ } > ( `llamacpp/models/${ model . id } /model.yml` )
53+
54+ if ( modelConfig && modelConfig . model_path ) {
55+ const result = await serviceHub
56+ . models ( )
57+ . planModelLoad ( modelConfig . model_path )
58+
59+ // Apply the recommended settings to the model sequentially to avoid race conditions
60+ const settingsToUpdate : Array < {
61+ key : string
62+ value : number | boolean
63+ } > = [ ]
64+
65+ if ( model . settings ?. ngl && result . gpuLayers !== undefined ) {
66+ settingsToUpdate . push ( { key : 'ngl' , value : result . gpuLayers } )
67+ }
68+
69+ if ( model . settings ?. ctx_len && result . maxContextLength !== undefined ) {
70+ settingsToUpdate . push ( {
71+ key : 'ctx_len' ,
72+ value : result . maxContextLength ,
73+ } )
74+ }
75+
76+ if (
77+ model . settings ?. no_kv_offload &&
78+ result . noOffloadKVCache !== undefined
79+ ) {
80+ settingsToUpdate . push ( {
81+ key : 'no_kv_offload' ,
82+ value : result . noOffloadKVCache ,
83+ } )
84+ }
85+
86+ // Apply all settings in a single update to avoid race conditions
87+ if ( settingsToUpdate . length > 0 ) {
88+ handleMultipleSettingsChange ( settingsToUpdate )
89+ }
90+ } else {
91+ console . warn ( 'No model_path found in config for' , model . id )
92+ }
93+ } catch ( error ) {
94+ console . error ( 'Error calling planModelLoad:' , error )
95+ } finally {
96+ setIsPlanning ( false )
97+ }
98+ }
99+
100+ const handleMultipleSettingsChange = (
101+ settingsToUpdate : Array < { key : string ; value : number | boolean } >
102+ ) => {
103+ if ( ! provider ) return
104+
105+ // Create a copy of the model with ALL updated settings at once
106+ let updatedModel = { ...model }
107+
108+ settingsToUpdate . forEach ( ( { key, value } ) => {
109+ const existingSetting = updatedModel . settings ?. [ key ] as ProviderSetting
110+ updatedModel = {
111+ ...updatedModel ,
112+ settings : {
113+ ...updatedModel . settings ,
114+ [ key ] : {
115+ ...existingSetting ,
116+ controller_props : {
117+ ...existingSetting ?. controller_props ,
118+ value : value ,
119+ } ,
120+ } as ProviderSetting ,
121+ } ,
122+ }
123+ } )
124+
125+ // Find the model index in the provider's models array
126+ const modelIndex = provider . models . findIndex ( ( m ) => m . id === model . id )
127+
128+ if ( modelIndex !== - 1 ) {
129+ // Create a copy of the provider's models array
130+ const updatedModels = [ ...provider . models ]
131+
132+ // Update the specific model in the array
133+ updatedModels [ modelIndex ] = updatedModel as Model
134+
135+ // Update the provider with the new models array
136+ updateProvider ( provider . provider , {
137+ models : updatedModels ,
138+ } )
139+
140+ // Check if any of the updated settings require a model restart
141+ const requiresRestart = settingsToUpdate . some (
142+ ( { key } ) =>
143+ key === 'ctx_len' ||
144+ key === 'ngl' ||
145+ key === 'chat_template' ||
146+ key === 'offload_mmproj'
147+ )
148+
149+ if ( requiresRestart ) {
150+ // Check if model is running before stopping it
151+ serviceHub
152+ . models ( )
153+ . getActiveModels ( )
154+ . then ( ( activeModels ) => {
155+ if ( activeModels . includes ( model . id ) ) {
156+ debouncedStopModel ( model . id )
157+ }
158+ } )
159+ }
160+ }
161+ }
162+
38163 const handleSettingChange = (
39164 key : string ,
40165 value : string | boolean | number
@@ -72,8 +197,22 @@ export function ModelSetting({
72197 } )
73198
74199 // Call debounced stopModel only when updating ctx_len, ngl, chat_template, or offload_mmproj
75- if ( key === 'ctx_len' || key === 'ngl' || key === 'chat_template' || key === 'offload_mmproj' ) {
76- debouncedStopModel ( model . id )
200+ // and only if the model is currently running
201+ if (
202+ key === 'ctx_len' ||
203+ key === 'ngl' ||
204+ key === 'chat_template' ||
205+ key === 'offload_mmproj'
206+ ) {
207+ // Check if model is running before stopping it
208+ serviceHub
209+ . models ( )
210+ . getActiveModels ( )
211+ . then ( ( activeModels ) => {
212+ if ( activeModels . includes ( model . id ) ) {
213+ debouncedStopModel ( model . id )
214+ }
215+ } )
77216 }
78217 }
79218 }
@@ -98,7 +237,36 @@ export function ModelSetting({
98237 < SheetDescription >
99238 { t ( 'common:modelSettings.description' ) }
100239 </ SheetDescription >
240+
241+ { /* Model Load Planning Section - Only show for llamacpp provider */ }
242+ { provider . provider === 'llamacpp' && (
243+ < div className = "pb-4 border-b border-main-view-fg/10 my-4" >
244+ < div >
245+ < h3 className = "font-medium mb-1" > Optimize Settings</ h3 >
246+ < p className = "text-main-view-fg/70 text-xs mb-3" >
247+ Analyze your system and model, then apply optimal loading
248+ settings automatically
249+ </ p >
250+ < Button
251+ onClick = { handlePlanModelLoad }
252+ disabled = { isPlanning }
253+ variant = "default"
254+ className = "w-full"
255+ >
256+ { isPlanning ? (
257+ < >
258+ < IconLoader size = { 16 } className = "mr-2 animate-spin" />
259+ Optimizing...
260+ </ >
261+ ) : (
262+ < > Auto-Optimize Settings</ >
263+ ) }
264+ </ Button >
265+ </ div >
266+ </ div >
267+ ) }
101268 </ SheetHeader >
269+
102270 < div className = "px-4 space-y-6" >
103271 { Object . entries ( model . settings || { } ) . map ( ( [ key , value ] ) => {
104272 const config = value as ProviderSetting
0 commit comments