import { IconSettings, IconLoader } from '@tabler/icons-react' import debounce from 'lodash.debounce' import { useState } from 'react' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet' import { Button } from '@/components/ui/button' import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting' import { useModelProvider } from '@/hooks/useModelProvider' import { useServiceHub } from '@/hooks/useServiceHub' import { cn, getModelDisplayName } from '@/lib/utils' import { useTranslation } from '@/i18n/react-i18next-compat' type ModelSettingProps = { provider: ProviderObject model: Model smallIcon?: boolean } export function ModelSetting({ model, provider, smallIcon, }: ModelSettingProps) { const { updateProvider } = useModelProvider() const { t } = useTranslation() const serviceHub = useServiceHub() const [isPlanning, setIsPlanning] = useState(false) // Create a debounced version of stopModel that waits 500ms after the last call const debouncedStopModel = debounce((modelId: string) => { serviceHub.models().stopModel(modelId) }, 500) const handlePlanModelLoad = async () => { if (provider.provider !== 'llamacpp') { console.warn('planModelLoad is only available for llamacpp provider') return } setIsPlanning(true) try { // Read the model config to get the actual model path and mmproj path const modelConfig = await serviceHub.app().readYaml<{ model_path: string mmproj_path?: string }>(`llamacpp/models/${model.id}/model.yml`) if (modelConfig && modelConfig.model_path) { const result = await serviceHub .models() .planModelLoad(modelConfig.model_path, modelConfig.mmproj_path) // Apply the recommended settings to the model sequentially to avoid race conditions const settingsToUpdate: Array<{ key: string value: number | boolean }> = [] if (model.settings?.ngl && result.gpuLayers !== undefined) { settingsToUpdate.push({ key: 'ngl', value: result.gpuLayers }) } if (model.settings?.ctx_len && result.maxContextLength !== undefined) { settingsToUpdate.push({ key: 'ctx_len', value: result.maxContextLength, }) } if ( model.settings?.no_kv_offload && result.noOffloadKVCache !== undefined ) { settingsToUpdate.push({ key: 'no_kv_offload', value: result.noOffloadKVCache, }) } if ( model.settings?.no_kv_offload && result.noOffloadKVCache !== undefined ) { settingsToUpdate.push({ key: 'no_kv_offload', value: result.noOffloadKVCache, }) } if ( model.settings?.mmproj_offload && result.offloadMmproj !== undefined ) { settingsToUpdate.push({ key: 'mmproj_offload', value: result.offloadMmproj, }) } if (model.settings?.batch_size && result.batchSize !== undefined) { settingsToUpdate.push({ key: 'batch_size', value: result.batchSize, }) } // Apply all settings in a single update to avoid race conditions if (settingsToUpdate.length > 0) { handleMultipleSettingsChange(settingsToUpdate) } } else { console.warn('No model_path found in config for', model.id) } } catch (error) { console.error('Error calling planModelLoad:', error) } finally { setIsPlanning(false) } } const handleMultipleSettingsChange = ( settingsToUpdate: Array<{ key: string; value: number | boolean }> ) => { if (!provider) return // Create a copy of the model with ALL updated settings at once let updatedModel = { ...model } settingsToUpdate.forEach(({ key, value }) => { const existingSetting = updatedModel.settings?.[key] as ProviderSetting updatedModel = { ...updatedModel, settings: { ...updatedModel.settings, [key]: { ...existingSetting, controller_props: { ...existingSetting?.controller_props, value: value, }, } as ProviderSetting, }, } }) // Find the model index in the provider's models array const modelIndex = provider.models.findIndex((m) => m.id === model.id) if (modelIndex !== -1) { // Create a copy of the provider's models array const updatedModels = [...provider.models] // Update the specific model in the array updatedModels[modelIndex] = updatedModel as Model // Update the provider with the new models array updateProvider(provider.provider, { models: updatedModels, }) // Check if any of the updated settings require a model restart const requiresRestart = settingsToUpdate.some( ({ key }) => key === 'ctx_len' || key === 'ngl' || key === 'chat_template' || key === 'offload_mmproj' || key === 'batch_size' ) if (requiresRestart) { // Check if model is running before stopping it serviceHub .models() .getActiveModels() .then((activeModels) => { if (activeModels.includes(model.id)) { debouncedStopModel(model.id) } }) } } } const handleSettingChange = ( key: string, value: string | boolean | number ) => { if (!provider) return // Create a copy of the model with updated settings const updatedModel = { ...model, settings: { ...model.settings, [key]: { ...(model.settings?.[key] != null ? model.settings?.[key] : {}), controller_props: { ...(model.settings?.[key]?.controller_props ?? {}), value: value, }, }, }, } // Find the model index in the provider's models array const modelIndex = provider.models.findIndex((m) => m.id === model.id) if (modelIndex !== -1) { // Create a copy of the provider's models array const updatedModels = [...provider.models] // Update the specific model in the array updatedModels[modelIndex] = updatedModel as Model // Update the provider with the new models array updateProvider(provider.provider, { models: updatedModels, }) // Call debounced stopModel only when updating ctx_len, ngl, chat_template, or offload_mmproj // and only if the model is currently running if ( key === 'ctx_len' || key === 'ngl' || key === 'chat_template' || key === 'offload_mmproj' || key === 'batch_size' ) { // Check if model is running before stopping it serviceHub .models() .getActiveModels() .then((activeModels) => { if (activeModels.includes(model.id)) { debouncedStopModel(model.id) } }) } } } return (
{t('common:modelSettings.title', { modelId: getModelDisplayName(model) })} {t('common:modelSettings.description')} {/* Model Load Planning Section - Only show for llamacpp provider */} {provider.provider === 'llamacpp' && (

Optimize Settings

{t('mcp-servers:experimental')}

Analyze your system and model, then apply optimal loading settings automatically

)}
{Object.entries(model.settings || {}).map(([key, value]) => { const config = value as ProviderSetting return (

{config.title}

{config.description}

handleSettingChange(key, newValue)} />
) })}
) }