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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user