Add support for extra arguments in frontend

This commit is contained in:
2025-11-12 22:50:15 +01:00
parent a2740055c2
commit 15180a227b
11 changed files with 162 additions and 53 deletions

View File

@@ -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)

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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>
) )

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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