fix: vision slot filters to image-capable + custom models (was relaxing for all providers)

The prior change made the vision slot show every model (image-capable first),
which relaxed the filter for built-in providers too — built-in text-only models
became selectable for Vision. Centralize the vision rule in ModelMenuList:
built-ins must report image input; custom-endpoint models stay selectable (their
capability may be unreported). Image-capable still float first.
This commit is contained in:
Daniel Buckner
2026-10-27 15:56:17 -07:00
parent dbf80803cc
commit 5b1fd81815
4 changed files with 27 additions and 21 deletions
+2 -6
View File
@@ -600,10 +600,6 @@ export function AgentSection({
const chosen = coord ? catalog?.find((m) => m.id === coord.id && m.provider === coord.provider) : undefined;
const levels = chosen ? chosen.supportedLevels : THINKING_LEVELS;
const thisMenuOpen = openMenu?.target === slot ? openMenu.kind : null;
// Vision slot: image-capable models float first, but all stay
// selectable (a custom endpoint that doesn't report modality must
// remain pickable) — so this orders, never filters.
const slotCatalog = catalog;
return (
<SettingsRow
key={slot}
@@ -648,10 +644,10 @@ export function AgentSection({
menuClass="flex w-[22rem] flex-col rounded-xl border border-border bg-card p-2 text-xs shadow-xl"
>
<ModelMenuList
models={slotCatalog ?? undefined}
models={catalog ?? undefined}
effectiveId={modelId}
effectiveProvider={provider}
prioritizeImage={slot === "vision"}
vision={slot === "vision"}
onPick={(entry) => {
// Auto-populate (chunk 2, owner: "populate everywhere"): the
// fresh pick writes a level — the stored one rides as-is
+14 -8
View File
@@ -21,7 +21,7 @@
import { useEffect, useRef, useState } from "react";
import type { ModelCatalogEntry } from "@/src/core";
import { fuzzyScore, subscriptionLabel } from "@/src/core";
import { CUSTOM_PROVIDER, fuzzyScore, subscriptionLabel } from "@/src/core";
import { formatTokens } from "@/components/usage-format";
import { SearchMenuList } from "./search-menu-list";
@@ -39,7 +39,7 @@ export function ModelMenuList({
effectiveProvider,
onPick,
footer,
prioritizeImage,
vision,
}: {
/** The full catalog — undefined while it loads. */
models: ModelCatalogEntry[] | undefined;
@@ -54,19 +54,25 @@ export function ModelMenuList({
onPick: (entry: ModelCatalogEntry) => void;
/** Quiet footer note under the list (context-specific). */
footer?: string;
/** Vision slot: image-capable models float to the top (still all
* selectable — a custom endpoint that doesn't report modality must
* stay pickable, so this orders rather than filters). */
prioritizeImage?: boolean;
/** Vision slot: only image-capable models are selectable — plus every
* custom-endpoint model, whose capability the endpoint may not report.
* Image-capable ones float to the top. */
vision?: boolean;
}) {
// Vision slot: built-ins must report image input; custom-endpoint models
// stay selectable (the endpoint may not report a modality). Image-capable
// ones float to the top of the list.
const visionItems = vision
? models?.filter((m) => m.input.includes("image") || m.provider === CUSTOM_PROVIDER.id)
: models;
return (
<>
<SearchMenuList
items={models}
items={visionItems}
itemKey={(m) => `${m.provider}/${m.id}`}
searchTerms={(m) => [m.provider, m.id, m.name]}
compare={(a, b) => a.id.localeCompare(b.id)}
prioritize={prioritizeImage ? (m) => m.input.includes("image") : undefined}
prioritize={vision ? (m) => m.input.includes("image") : undefined}
isSelected={(m) => m.id === effectiveId && m.provider === (effectiveProvider ?? m.provider)}
onPick={onPick}
searchTestid="model-search"
+4 -2
View File
@@ -519,7 +519,9 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
// Unavailable metadata cannot erase or clamp a saved preference.
const levelsForEntry = metadata?.supportedLevels ?? (entry ? Object.keys(THINKING_LABEL) : []);
const available = catalog?.models ?? (failed ? [] : undefined);
const slotCatalog = slot === "vision" ? available?.filter((m) => m.input.includes("image")) : available;
const slotCatalog = slot === "vision"
? available?.filter((m) => m.input.includes("image") || m.provider === CUSTOM_PROVIDER.id)
: available;
return (
<div key={slot} data-testid={`setup-slot-${slot}`} className="flex flex-col gap-1.5">
<span className="flex items-center gap-1.5">
@@ -573,7 +575,7 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
models={slotCatalog}
effectiveId={entry?.id}
effectiveProvider={entry?.provider}
prioritizeImage={slot === "vision"}
vision={slot === "vision"}
onPick={(m) => {
editedSlots.current.add(slot);
setPicked((prev) => ({ ...prev, [slot]: m }));