From 15180a227b83763172fbf78ff1340f1131edf0ad Mon Sep 17 00:00:00 2001 From: LordMathis Date: Wed, 12 Nov 2025 22:50:15 +0100 Subject: [PATCH] Add support for extra arguments in frontend --- webui/src/components/BackendFormField.tsx | 20 +++- webui/src/components/form/EnvVarsInput.tsx | 27 +++++ webui/src/components/form/ExtraArgsInput.tsx | 27 +++++ ...ntVariablesInput.tsx => KeyValueInput.tsx} | 104 ++++++++++-------- .../instance/BackendConfiguration.tsx | 12 +- .../instance/BackendConfigurationCard.tsx | 10 ++ .../instance/InstanceSettingsCard.tsx | 4 +- webui/src/lib/zodFormUtils.ts | 2 +- webui/src/schemas/backends/llamacpp.ts | 3 + webui/src/schemas/backends/mlx.ts | 3 + webui/src/schemas/backends/vllm.ts | 3 + 11 files changed, 162 insertions(+), 53 deletions(-) create mode 100644 webui/src/components/form/EnvVarsInput.tsx create mode 100644 webui/src/components/form/ExtraArgsInput.tsx rename webui/src/components/form/{EnvironmentVariablesInput.tsx => KeyValueInput.tsx} (50%) diff --git a/webui/src/components/BackendFormField.tsx b/webui/src/components/BackendFormField.tsx index bb49fc1..7dfbf5a 100644 --- a/webui/src/components/BackendFormField.tsx +++ b/webui/src/components/BackendFormField.tsx @@ -3,17 +3,31 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { getBackendFieldType, basicBackendFieldsConfig } from '@/lib/zodFormUtils' +import ExtraArgsInput from '@/components/form/ExtraArgsInput' interface BackendFormFieldProps { fieldKey: string - value: string | number | boolean | string[] | undefined - onChange: (key: string, value: string | number | boolean | string[] | undefined) => void + value: string | number | boolean | string[] | Record | undefined + onChange: (key: string, value: string | number | boolean | string[] | Record | undefined) => void } const BackendFormField: React.FC = ({ fieldKey, value, onChange }) => { + // Special handling for extra_args + if (fieldKey === 'extra_args') { + return ( + | 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 const config = basicBackendFieldsConfig[fieldKey] || { label: fieldKey } - + // Get type from Zod schema const fieldType = getBackendFieldType(fieldKey) diff --git a/webui/src/components/form/EnvVarsInput.tsx b/webui/src/components/form/EnvVarsInput.tsx new file mode 100644 index 0000000..476a98a --- /dev/null +++ b/webui/src/components/form/EnvVarsInput.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import KeyValueInput from './KeyValueInput' + +interface EnvVarsInputProps { + id: string + label: string + value: Record | undefined + onChange: (value: Record | undefined) => void + description?: string + disabled?: boolean + className?: string +} + +const EnvVarsInput: React.FC = (props) => { + return ( + + ) +} + +export default EnvVarsInput diff --git a/webui/src/components/form/ExtraArgsInput.tsx b/webui/src/components/form/ExtraArgsInput.tsx new file mode 100644 index 0000000..70f11b8 --- /dev/null +++ b/webui/src/components/form/ExtraArgsInput.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import KeyValueInput from './KeyValueInput' + +interface ExtraArgsInputProps { + id: string + label: string + value: Record | undefined + onChange: (value: Record | undefined) => void + description?: string + disabled?: boolean + className?: string +} + +const ExtraArgsInput: React.FC = (props) => { + return ( + + ) +} + +export default ExtraArgsInput diff --git a/webui/src/components/form/EnvironmentVariablesInput.tsx b/webui/src/components/form/KeyValueInput.tsx similarity index 50% rename from webui/src/components/form/EnvironmentVariablesInput.tsx rename to webui/src/components/form/KeyValueInput.tsx index 47739f0..6ab9fd5 100644 --- a/webui/src/components/form/EnvironmentVariablesInput.tsx +++ b/webui/src/components/form/KeyValueInput.tsx @@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label' import { Button } from '@/components/ui/button' import { X, Plus } from 'lucide-react' -interface EnvironmentVariablesInputProps { +interface KeyValueInputProps { id: string label: string value: Record | undefined @@ -12,76 +12,88 @@ interface EnvironmentVariablesInputProps { description?: string disabled?: boolean 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 value: string } -const EnvironmentVariablesInput: React.FC = ({ +const KeyValueInput: React.FC = ({ id, label, value, onChange, description, 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 - const envVarsFromValue = value + const pairsFromValue = value ? Object.entries(value).map(([key, val]) => ({ key, value: val })) : [] - const [envVars, setEnvVars] = useState( - envVarsFromValue.length > 0 ? envVarsFromValue : [{ key: '', value: '' }] + const [pairs, setPairs] = useState( + pairsFromValue.length > 0 ? pairsFromValue : [{ key: '', value: '' }] ) - // Update parent component when env vars change - const updateParent = (newEnvVars: EnvVar[]) => { - // Filter out empty entries - const validVars = newEnvVars.filter(env => env.key.trim() !== '' && env.value.trim() !== '') + // Update parent component when pairs change + const updateParent = (newPairs: KeyValuePair[]) => { + // Filter based on validation rules + 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) } else { - const envObject = validVars.reduce((acc, env) => { - acc[env.key.trim()] = env.value.trim() + const pairsObject = validPairs.reduce((acc, pair) => { + acc[pair.key.trim()] = pair.value.trim() return acc }, {} as Record) - onChange(envObject) + onChange(pairsObject) } } const handleKeyChange = (index: number, newKey: string) => { - const newEnvVars = [...envVars] - newEnvVars[index].key = newKey - setEnvVars(newEnvVars) - updateParent(newEnvVars) + const newPairs = [...pairs] + newPairs[index].key = newKey + setPairs(newPairs) + updateParent(newPairs) } const handleValueChange = (index: number, newValue: string) => { - const newEnvVars = [...envVars] - newEnvVars[index].value = newValue - setEnvVars(newEnvVars) - updateParent(newEnvVars) + const newPairs = [...pairs] + newPairs[index].value = newValue + setPairs(newPairs) + updateParent(newPairs) } - const addEnvVar = () => { - const newEnvVars = [...envVars, { key: '', value: '' }] - setEnvVars(newEnvVars) + const addPair = () => { + const newPairs = [...pairs, { key: '', value: '' }] + setPairs(newPairs) } - const removeEnvVar = (index: number) => { - if (envVars.length === 1) { + const removePair = (index: number) => { + if (pairs.length === 1) { // Reset to empty if it's the last one - const newEnvVars = [{ key: '', value: '' }] - setEnvVars(newEnvVars) - updateParent(newEnvVars) + const newPairs = [{ key: '', value: '' }] + setPairs(newPairs) + updateParent(newPairs) } else { - const newEnvVars = envVars.filter((_, i) => i !== index) - setEnvVars(newEnvVars) - updateParent(newEnvVars) + const newPairs = pairs.filter((_, i) => i !== index) + setPairs(newPairs) + updateParent(newPairs) } } @@ -91,18 +103,18 @@ const EnvironmentVariablesInput: React.FC = ({ {label}
- {envVars.map((envVar, index) => ( + {pairs.map((pair, index) => (
handleKeyChange(index, e.target.value)} disabled={disabled} className="flex-1" /> handleValueChange(index, e.target.value)} disabled={disabled} className="flex-1" @@ -111,7 +123,7 @@ const EnvironmentVariablesInput: React.FC = ({ type="button" variant="outline" size="sm" - onClick={() => removeEnvVar(index)} + onClick={() => removePair(index)} disabled={disabled} className="shrink-0" > @@ -123,22 +135,22 @@ const EnvironmentVariablesInput: React.FC = ({ type="button" variant="outline" size="sm" - onClick={addEnvVar} + onClick={addPair} disabled={disabled} className="w-fit" > - Add Variable + {addButtonText}
{description && (

{description}

)} -

- Environment variables that will be passed to the backend process -

+ {helperText && ( +

{helperText}

+ )}
) } -export default EnvironmentVariablesInput \ No newline at end of file +export default KeyValueInput diff --git a/webui/src/components/instance/BackendConfiguration.tsx b/webui/src/components/instance/BackendConfiguration.tsx index cfcee86..8f10e41 100644 --- a/webui/src/components/instance/BackendConfiguration.tsx +++ b/webui/src/components/instance/BackendConfiguration.tsx @@ -47,8 +47,18 @@ const BackendConfiguration: React.FC = ({ ))} )} + + {/* Extra Args - Always visible as a separate section */} +
+ +
) } -export default BackendConfiguration \ No newline at end of file +export default BackendConfiguration diff --git a/webui/src/components/instance/BackendConfigurationCard.tsx b/webui/src/components/instance/BackendConfigurationCard.tsx index 5bf7c36..799ea2b 100644 --- a/webui/src/components/instance/BackendConfigurationCard.tsx +++ b/webui/src/components/instance/BackendConfigurationCard.tsx @@ -109,6 +109,16 @@ const BackendConfigurationCard: React.FC = ({ )} )} + + {/* Extra Arguments - Always visible */} +
+ )?.extra_args as Record | undefined} + onChange={onBackendFieldChange} + /> +
) diff --git a/webui/src/components/instance/InstanceSettingsCard.tsx b/webui/src/components/instance/InstanceSettingsCard.tsx index 1834eab..999e340 100644 --- a/webui/src/components/instance/InstanceSettingsCard.tsx +++ b/webui/src/components/instance/InstanceSettingsCard.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input' import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration' import NumberInput from '@/components/form/NumberInput' 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 { nodesApi, type NodesMap } from '@/lib/api' @@ -132,7 +132,7 @@ const InstanceSettingsCard: React.FC = ({ description="Start instance only when needed" /> - !(key in basicConfig)) + return fieldGetter().filter(key => !(key in basicConfig) && key !== 'extra_args') } // Combined backend fields config for use in BackendFormField diff --git a/webui/src/schemas/backends/llamacpp.ts b/webui/src/schemas/backends/llamacpp.ts index 7dead95..9383ec8 100644 --- a/webui/src/schemas/backends/llamacpp.ts +++ b/webui/src/schemas/backends/llamacpp.ts @@ -167,6 +167,9 @@ export const LlamaCppBackendOptionsSchema = z.object({ fim_qwen_7b_default: z.boolean().optional(), fim_qwen_7b_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 diff --git a/webui/src/schemas/backends/mlx.ts b/webui/src/schemas/backends/mlx.ts index 917ca81..4267ed9 100644 --- a/webui/src/schemas/backends/mlx.ts +++ b/webui/src/schemas/backends/mlx.ts @@ -25,6 +25,9 @@ export const MlxBackendOptionsSchema = z.object({ top_k: z.number().optional(), min_p: 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 diff --git a/webui/src/schemas/backends/vllm.ts b/webui/src/schemas/backends/vllm.ts index 7dd700f..0972a8f 100644 --- a/webui/src/schemas/backends/vllm.ts +++ b/webui/src/schemas/backends/vllm.ts @@ -125,6 +125,9 @@ export const VllmBackendOptionsSchema = z.object({ override_pooling_config: z.string().optional(), override_neuron_config: z.string().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