mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-12-23 17:44:24 +00:00
Add support for extra arguments in frontend
This commit is contained in:
@@ -3,17 +3,31 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { getBackendFieldType, basicBackendFieldsConfig } from '@/lib/zodFormUtils'
|
import { getBackendFieldType, basicBackendFieldsConfig } from '@/lib/zodFormUtils'
|
||||||
|
import ExtraArgsInput from '@/components/form/ExtraArgsInput'
|
||||||
|
|
||||||
interface BackendFormFieldProps {
|
interface BackendFormFieldProps {
|
||||||
fieldKey: string
|
fieldKey: string
|
||||||
value: string | number | boolean | string[] | undefined
|
value: string | number | boolean | string[] | Record<string, string> | undefined
|
||||||
onChange: (key: string, value: string | number | boolean | string[] | undefined) => void
|
onChange: (key: string, value: string | number | boolean | string[] | Record<string, string> | undefined) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, onChange }) => {
|
const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, onChange }) => {
|
||||||
|
// Special handling for extra_args
|
||||||
|
if (fieldKey === 'extra_args') {
|
||||||
|
return (
|
||||||
|
<ExtraArgsInput
|
||||||
|
id={fieldKey}
|
||||||
|
label="Extra Arguments"
|
||||||
|
value={value as Record<string, string> | undefined}
|
||||||
|
onChange={(newValue) => onChange(fieldKey, newValue)}
|
||||||
|
description="Additional command line arguments to pass to the backend"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Get configuration for basic fields, or use field name for advanced fields
|
// Get configuration for basic fields, or use field name for advanced fields
|
||||||
const config = basicBackendFieldsConfig[fieldKey] || { label: fieldKey }
|
const config = basicBackendFieldsConfig[fieldKey] || { label: fieldKey }
|
||||||
|
|
||||||
// Get type from Zod schema
|
// Get type from Zod schema
|
||||||
const fieldType = getBackendFieldType(fieldKey)
|
const fieldType = getBackendFieldType(fieldKey)
|
||||||
|
|
||||||
|
|||||||
27
webui/src/components/form/EnvVarsInput.tsx
Normal file
27
webui/src/components/form/EnvVarsInput.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import KeyValueInput from './KeyValueInput'
|
||||||
|
|
||||||
|
interface EnvVarsInputProps {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: Record<string, string> | undefined
|
||||||
|
onChange: (value: Record<string, string> | undefined) => void
|
||||||
|
description?: string
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvVarsInput: React.FC<EnvVarsInputProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<KeyValueInput
|
||||||
|
{...props}
|
||||||
|
keyPlaceholder="Variable name"
|
||||||
|
valuePlaceholder="Variable value"
|
||||||
|
addButtonText="Add Variable"
|
||||||
|
helperText="Environment variables that will be passed to the backend process"
|
||||||
|
allowEmptyValues={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnvVarsInput
|
||||||
27
webui/src/components/form/ExtraArgsInput.tsx
Normal file
27
webui/src/components/form/ExtraArgsInput.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import KeyValueInput from './KeyValueInput'
|
||||||
|
|
||||||
|
interface ExtraArgsInputProps {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: Record<string, string> | undefined
|
||||||
|
onChange: (value: Record<string, string> | undefined) => void
|
||||||
|
description?: string
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtraArgsInput: React.FC<ExtraArgsInputProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<KeyValueInput
|
||||||
|
{...props}
|
||||||
|
keyPlaceholder="Flag name (without --)"
|
||||||
|
valuePlaceholder="Value (empty for boolean flags)"
|
||||||
|
addButtonText="Add Argument"
|
||||||
|
helperText="Additional command line arguments to pass to the backend. Leave value empty for boolean flags."
|
||||||
|
allowEmptyValues={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtraArgsInput
|
||||||
@@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { X, Plus } from 'lucide-react'
|
import { X, Plus } from 'lucide-react'
|
||||||
|
|
||||||
interface EnvironmentVariablesInputProps {
|
interface KeyValueInputProps {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
value: Record<string, string> | undefined
|
value: Record<string, string> | undefined
|
||||||
@@ -12,76 +12,88 @@ interface EnvironmentVariablesInputProps {
|
|||||||
description?: string
|
description?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
keyPlaceholder?: string
|
||||||
|
valuePlaceholder?: string
|
||||||
|
addButtonText?: string
|
||||||
|
helperText?: string
|
||||||
|
allowEmptyValues?: boolean // If true, entries with empty values are considered valid
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EnvVar {
|
interface KeyValuePair {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnvironmentVariablesInput: React.FC<EnvironmentVariablesInputProps> = ({
|
const KeyValueInput: React.FC<KeyValueInputProps> = ({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
description,
|
description,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className
|
className,
|
||||||
|
keyPlaceholder = 'Key',
|
||||||
|
valuePlaceholder = 'Value',
|
||||||
|
addButtonText = 'Add Entry',
|
||||||
|
helperText,
|
||||||
|
allowEmptyValues = false
|
||||||
}) => {
|
}) => {
|
||||||
// Convert the value object to an array of key-value pairs for editing
|
// Convert the value object to an array of key-value pairs for editing
|
||||||
const envVarsFromValue = value
|
const pairsFromValue = value
|
||||||
? Object.entries(value).map(([key, val]) => ({ key, value: val }))
|
? Object.entries(value).map(([key, val]) => ({ key, value: val }))
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
const [pairs, setPairs] = useState<KeyValuePair[]>(
|
||||||
envVarsFromValue.length > 0 ? envVarsFromValue : [{ key: '', value: '' }]
|
pairsFromValue.length > 0 ? pairsFromValue : [{ key: '', value: '' }]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update parent component when env vars change
|
// Update parent component when pairs change
|
||||||
const updateParent = (newEnvVars: EnvVar[]) => {
|
const updateParent = (newPairs: KeyValuePair[]) => {
|
||||||
// Filter out empty entries
|
// Filter based on validation rules
|
||||||
const validVars = newEnvVars.filter(env => env.key.trim() !== '' && env.value.trim() !== '')
|
const validPairs = allowEmptyValues
|
||||||
|
? newPairs.filter(pair => pair.key.trim() !== '')
|
||||||
|
: newPairs.filter(pair => pair.key.trim() !== '' && pair.value.trim() !== '')
|
||||||
|
|
||||||
if (validVars.length === 0) {
|
if (validPairs.length === 0) {
|
||||||
onChange(undefined)
|
onChange(undefined)
|
||||||
} else {
|
} else {
|
||||||
const envObject = validVars.reduce((acc, env) => {
|
const pairsObject = validPairs.reduce((acc, pair) => {
|
||||||
acc[env.key.trim()] = env.value.trim()
|
acc[pair.key.trim()] = pair.value.trim()
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, string>)
|
}, {} as Record<string, string>)
|
||||||
onChange(envObject)
|
onChange(pairsObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyChange = (index: number, newKey: string) => {
|
const handleKeyChange = (index: number, newKey: string) => {
|
||||||
const newEnvVars = [...envVars]
|
const newPairs = [...pairs]
|
||||||
newEnvVars[index].key = newKey
|
newPairs[index].key = newKey
|
||||||
setEnvVars(newEnvVars)
|
setPairs(newPairs)
|
||||||
updateParent(newEnvVars)
|
updateParent(newPairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValueChange = (index: number, newValue: string) => {
|
const handleValueChange = (index: number, newValue: string) => {
|
||||||
const newEnvVars = [...envVars]
|
const newPairs = [...pairs]
|
||||||
newEnvVars[index].value = newValue
|
newPairs[index].value = newValue
|
||||||
setEnvVars(newEnvVars)
|
setPairs(newPairs)
|
||||||
updateParent(newEnvVars)
|
updateParent(newPairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addEnvVar = () => {
|
const addPair = () => {
|
||||||
const newEnvVars = [...envVars, { key: '', value: '' }]
|
const newPairs = [...pairs, { key: '', value: '' }]
|
||||||
setEnvVars(newEnvVars)
|
setPairs(newPairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvVar = (index: number) => {
|
const removePair = (index: number) => {
|
||||||
if (envVars.length === 1) {
|
if (pairs.length === 1) {
|
||||||
// Reset to empty if it's the last one
|
// Reset to empty if it's the last one
|
||||||
const newEnvVars = [{ key: '', value: '' }]
|
const newPairs = [{ key: '', value: '' }]
|
||||||
setEnvVars(newEnvVars)
|
setPairs(newPairs)
|
||||||
updateParent(newEnvVars)
|
updateParent(newPairs)
|
||||||
} else {
|
} else {
|
||||||
const newEnvVars = envVars.filter((_, i) => i !== index)
|
const newPairs = pairs.filter((_, i) => i !== index)
|
||||||
setEnvVars(newEnvVars)
|
setPairs(newPairs)
|
||||||
updateParent(newEnvVars)
|
updateParent(newPairs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,18 +103,18 @@ const EnvironmentVariablesInput: React.FC<EnvironmentVariablesInputProps> = ({
|
|||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{envVars.map((envVar, index) => (
|
{pairs.map((pair, index) => (
|
||||||
<div key={index} className="flex gap-2 items-center">
|
<div key={index} className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Variable name"
|
placeholder={keyPlaceholder}
|
||||||
value={envVar.key}
|
value={pair.key}
|
||||||
onChange={(e) => handleKeyChange(index, e.target.value)}
|
onChange={(e) => handleKeyChange(index, e.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Variable value"
|
placeholder={valuePlaceholder}
|
||||||
value={envVar.value}
|
value={pair.value}
|
||||||
onChange={(e) => handleValueChange(index, e.target.value)}
|
onChange={(e) => handleValueChange(index, e.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -111,7 +123,7 @@ const EnvironmentVariablesInput: React.FC<EnvironmentVariablesInputProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeEnvVar(index)}
|
onClick={() => removePair(index)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
@@ -123,22 +135,22 @@ const EnvironmentVariablesInput: React.FC<EnvironmentVariablesInputProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addEnvVar}
|
onClick={addPair}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Add Variable
|
{addButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
{helperText && (
|
||||||
Environment variables that will be passed to the backend process
|
<p className="text-xs text-muted-foreground">{helperText}</p>
|
||||||
</p>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EnvironmentVariablesInput
|
export default KeyValueInput
|
||||||
@@ -47,8 +47,18 @@ const BackendConfiguration: React.FC<BackendConfigurationProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Extra Args - Always visible as a separate section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<BackendFormField
|
||||||
|
key="extra_args"
|
||||||
|
fieldKey="extra_args"
|
||||||
|
value={(formData.backend_options as any)?.extra_args}
|
||||||
|
onChange={onBackendFieldChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BackendConfiguration
|
export default BackendConfiguration
|
||||||
|
|||||||
@@ -109,6 +109,16 @@ const BackendConfigurationCard: React.FC<BackendConfigurationCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Extra Arguments - Always visible */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<BackendFormField
|
||||||
|
key="extra_args"
|
||||||
|
fieldKey="extra_args"
|
||||||
|
value={(formData.backend_options as Record<string, unknown>)?.extra_args as Record<string, string> | undefined}
|
||||||
|
onChange={onBackendFieldChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration'
|
import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration'
|
||||||
import NumberInput from '@/components/form/NumberInput'
|
import NumberInput from '@/components/form/NumberInput'
|
||||||
import CheckboxInput from '@/components/form/CheckboxInput'
|
import CheckboxInput from '@/components/form/CheckboxInput'
|
||||||
import EnvironmentVariablesInput from '@/components/form/EnvironmentVariablesInput'
|
import EnvVarsInput from '@/components/form/EnvironmentVariablesInput'
|
||||||
import SelectInput from '@/components/form/SelectInput'
|
import SelectInput from '@/components/form/SelectInput'
|
||||||
import { nodesApi, type NodesMap } from '@/lib/api'
|
import { nodesApi, type NodesMap } from '@/lib/api'
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ const InstanceSettingsCard: React.FC<InstanceSettingsCardProps> = ({
|
|||||||
description="Start instance only when needed"
|
description="Start instance only when needed"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EnvironmentVariablesInput
|
<EnvVarsInput
|
||||||
id="environment"
|
id="environment"
|
||||||
label="Environment Variables"
|
label="Environment Variables"
|
||||||
value={formData.environment}
|
value={formData.environment}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function getAdvancedBackendFields(backendType?: string): string[] {
|
|||||||
const fieldGetter = backendFieldGetters[normalizedType] || getAllLlamaCppFieldKeys
|
const fieldGetter = backendFieldGetters[normalizedType] || getAllLlamaCppFieldKeys
|
||||||
const basicConfig = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig
|
const basicConfig = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig
|
||||||
|
|
||||||
return fieldGetter().filter(key => !(key in basicConfig))
|
return fieldGetter().filter(key => !(key in basicConfig) && key !== 'extra_args')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined backend fields config for use in BackendFormField
|
// Combined backend fields config for use in BackendFormField
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ export const LlamaCppBackendOptionsSchema = z.object({
|
|||||||
fim_qwen_7b_default: z.boolean().optional(),
|
fim_qwen_7b_default: z.boolean().optional(),
|
||||||
fim_qwen_7b_spec: z.boolean().optional(),
|
fim_qwen_7b_spec: z.boolean().optional(),
|
||||||
fim_qwen_14b_spec: z.boolean().optional(),
|
fim_qwen_14b_spec: z.boolean().optional(),
|
||||||
|
|
||||||
|
// Extra args
|
||||||
|
extra_args: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Infer the TypeScript type from the schema
|
// Infer the TypeScript type from the schema
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export const MlxBackendOptionsSchema = z.object({
|
|||||||
top_k: z.number().optional(),
|
top_k: z.number().optional(),
|
||||||
min_p: z.number().optional(),
|
min_p: z.number().optional(),
|
||||||
max_tokens: z.number().optional(),
|
max_tokens: z.number().optional(),
|
||||||
|
|
||||||
|
// Extra args
|
||||||
|
extra_args: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Infer the TypeScript type from the schema
|
// Infer the TypeScript type from the schema
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export const VllmBackendOptionsSchema = z.object({
|
|||||||
override_pooling_config: z.string().optional(),
|
override_pooling_config: z.string().optional(),
|
||||||
override_neuron_config: z.string().optional(),
|
override_neuron_config: z.string().optional(),
|
||||||
override_kv_cache_align_size: z.number().optional(),
|
override_kv_cache_align_size: z.number().optional(),
|
||||||
|
|
||||||
|
// Extra args
|
||||||
|
extra_args: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Infer the TypeScript type from the schema
|
// Infer the TypeScript type from the schema
|
||||||
|
|||||||
Reference in New Issue
Block a user