Settings: real model dropdown with cost hints + Custom escape hatch

The previous datalist-on-input approach was fragile — Safari hid the
suggestions, and there was no visual cue that a dropdown existed.
Replace with a proper Select populated from the catalog. Each option
shows the per-1M-token rates inline so operators see cost while
choosing. "Custom…" switches to free-text for models the catalog
doesn't know about, with a "Catalog" button to flip back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-02 18:25:49 +10:00
parent baf42c4cec
commit 8e07f4b9c0

View File

@@ -224,17 +224,11 @@ export function LlmConfigurationsPanel() {
</Select>
</Field>
<Field label="Model">
<Input
<ModelPicker
value={draft.model}
onChange={(e) => setDraft({ ...draft, model: e.target.value })}
list="llm-config-models"
placeholder="gpt-4o-mini"
models={modelsForProvider}
onChange={(model) => setDraft({ ...draft, model })}
/>
<datalist id="llm-config-models">
{modelsForProvider.map((m) => (
<option key={m.model} value={m.model} />
))}
</datalist>
</Field>
<Field label="Vault secret name (optional)">
<Input
@@ -332,6 +326,89 @@ export function LlmConfigurationsPanel() {
)
}
/**
* Model picker: dropdown of catalog entries for the selected provider, plus
* a "Custom…" escape hatch that switches to free-text for models the
* catalog doesn't know about. The custom branch is sticky — once the user
* types a custom name, switching providers resets to the dropdown.
*/
function ModelPicker({
value,
models,
onChange,
}: {
value: string
models: CatalogEntry[]
onChange: (model: string) => void
}) {
const known = useMemo(() => new Set(models.map((m) => m.model)), [models])
const isCustom = value !== "" && !known.has(value)
const [customMode, setCustomMode] = useState(isCustom)
// Reset to dropdown if the available models change (e.g. provider switched)
// and we're not actively in custom mode.
useEffect(() => {
if (!isCustom) setCustomMode(false)
}, [models, isCustom])
if (customMode) {
return (
<div className="flex gap-2">
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="custom-model-id"
autoFocus
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setCustomMode(false)
onChange("")
}}
>
Catalog
</Button>
</div>
)
}
return (
<Select
value={known.has(value) ? value : ""}
onValueChange={(v) => {
if (v === "__custom__") {
setCustomMode(true)
onChange("")
} else {
onChange(v)
}
}}
>
<SelectTrigger>
<SelectValue placeholder={models.length ? "Pick a model…" : "No models for this provider"} />
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.model} value={m.model}>
<span className="flex items-center justify-between gap-3">
<span>{m.model}</span>
<span className="text-[10px] text-muted-foreground">
${m.input_cost_per_million.toFixed(2)} / ${m.output_cost_per_million.toFixed(2)} per 1M
</span>
</span>
</SelectItem>
))}
<SelectItem value="__custom__">
<span className="text-muted-foreground">Custom</span>
</SelectItem>
</SelectContent>
</Select>
)
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-1">