Skip to content

Commit e1847b4

Browse files
committed
chore: add UI auto optimize model setting
1 parent 076b5fc commit e1847b4

File tree

5 files changed

+290
-47
lines changed

5 files changed

+290
-47
lines changed

extensions/llamacpp-extension/src/index.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,39 +2167,42 @@ export default class llamacpp_extension extends AIEngine {
21672167
async validateGgufFile(filePath: string): Promise<{
21682168
isValid: boolean
21692169
error?: string
2170-
metadata?: GgufMetadata
2170+
metadata?: any
21712171
}> {
21722172
try {
21732173
logger.info(`Validating GGUF file: ${filePath}`)
21742174
const metadata = await readGgufMetadata(filePath)
2175-
2175+
21762176
// Log full metadata for debugging
21772177
logger.info('Full GGUF metadata:', JSON.stringify(metadata, null, 2))
2178-
2178+
21792179
// Check if architecture is 'clip' which is not supported for text generation
21802180
const architecture = metadata.metadata?.['general.architecture']
21812181
logger.info(`Model architecture: ${architecture}`)
2182-
2182+
21832183
if (architecture === 'clip') {
2184-
const errorMessage = 'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.'
2184+
const errorMessage =
2185+
'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.'
21852186
logger.error('CLIP architecture detected:', architecture)
21862187
return {
21872188
isValid: false,
21882189
error: errorMessage,
2189-
metadata
2190+
metadata,
21902191
}
21912192
}
2192-
2193+
21932194
logger.info('Model validation passed. Architecture:', architecture)
21942195
return {
21952196
isValid: true,
2196-
metadata
2197+
metadata,
21972198
}
21982199
} catch (error) {
21992200
logger.error('Failed to validate GGUF file:', error)
22002201
return {
22012202
isValid: false,
2202-
error: `Failed to read model metadata: ${error instanceof Error ? error.message : 'Unknown error'}`
2203+
error: `Failed to read model metadata: ${
2204+
error instanceof Error ? error.message : 'Unknown error'
2205+
}`,
22032206
}
22042207
}
22052208
}

web-app/src/containers/ModelSetting.tsx

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { IconSettings } from '@tabler/icons-react'
1+
import { IconSettings, IconLoader } from '@tabler/icons-react'
22
import debounce from 'lodash.debounce'
3+
import { useState } from 'react'
34

45
import {
56
Sheet,
@@ -9,6 +10,7 @@ import {
910
SheetTitle,
1011
SheetTrigger,
1112
} from '@/components/ui/sheet'
13+
import { Button } from '@/components/ui/button'
1214
import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting'
1315
import { useModelProvider } from '@/hooks/useModelProvider'
1416
import { 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

web-app/src/routes/settings/providers/$providerName.tsx

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,19 @@ function ProviderDetail() {
9393
const handleModelImportSuccess = async (importedModelName?: string) => {
9494
// Refresh the provider to update the models list
9595
await serviceHub.providers().getProviders().then(setProviders)
96-
96+
9797
// If a model was imported and it might have vision capabilities, check and update
9898
if (importedModelName && providerName === 'llamacpp') {
9999
try {
100-
const mmprojExists = await serviceHub.models().checkMmprojExists(importedModelName)
100+
const mmprojExists = await serviceHub
101+
.models()
102+
.checkMmprojExists(importedModelName)
101103
if (mmprojExists) {
102104
// Get the updated provider after refresh
103-
const { getProviderByName, updateProvider: updateProviderState } = useModelProvider.getState()
105+
const { getProviderByName, updateProvider: updateProviderState } =
106+
useModelProvider.getState()
104107
const llamacppProvider = getProviderByName('llamacpp')
105-
108+
106109
if (llamacppProvider) {
107110
const modelIndex = llamacppProvider.models.findIndex(
108111
(m: Model) => m.id === importedModelName
@@ -120,7 +123,9 @@ function ProviderDetail() {
120123
}
121124

122125
updateProviderState('llamacpp', { models: updatedModels })
123-
console.log(`Vision capability added to model after provider refresh: ${importedModelName}`)
126+
console.log(
127+
`Vision capability added to model after provider refresh: ${importedModelName}`
128+
)
124129
}
125130
}
126131
}
@@ -245,33 +250,36 @@ function ProviderDetail() {
245250
}
246251
}
247252

248-
const handleStartModel = (modelId: string) => {
253+
const handleStartModel = async (modelId: string) => {
249254
// Add model to loading state
250255
setLoadingModels((prev) => [...prev, modelId])
251-
if (provider)
252-
// Original: startModel(provider, modelId).then(() => { setActiveModels((prevModels) => [...prevModels, modelId]) })
253-
serviceHub
254-
.models()
255-
.startModel(provider, modelId)
256-
.then(() => {
257-
// Refresh active models after starting
258-
serviceHub
259-
.models()
260-
.getActiveModels()
261-
.then((models) => setActiveModels(models || []))
262-
})
263-
.catch((error) => {
264-
console.error('Error starting model:', error)
265-
if (error && typeof error === 'object' && 'message' in error) {
266-
setModelLoadError(error)
267-
} else {
268-
setModelLoadError(`${error}`)
269-
}
270-
})
271-
.finally(() => {
272-
// Remove model from loading state
273-
setLoadingModels((prev) => prev.filter((id) => id !== modelId))
274-
})
256+
if (provider) {
257+
try {
258+
// Start the model with plan result
259+
await serviceHub.models().startModel(provider, modelId)
260+
261+
// Refresh active models after starting
262+
serviceHub
263+
.models()
264+
.getActiveModels()
265+
.then((models) => setActiveModels(models || []))
266+
} catch (error) {
267+
console.error('Error starting model:', error)
268+
if (
269+
error &&
270+
typeof error === 'object' &&
271+
'message' in error &&
272+
typeof error.message === 'string'
273+
) {
274+
setModelLoadError({ message: error.message })
275+
} else {
276+
setModelLoadError(typeof error === 'string' ? error : `${error}`)
277+
}
278+
} finally {
279+
// Remove model from loading state
280+
setLoadingModels((prev) => prev.filter((id) => id !== modelId))
281+
}
282+
}
275283
}
276284

277285
const handleStopModel = (modelId: string) => {

0 commit comments

Comments
 (0)