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:
@@ -600,10 +600,6 @@ export function AgentSection({
|
|||||||
const chosen = coord ? catalog?.find((m) => m.id === coord.id && m.provider === coord.provider) : undefined;
|
const chosen = coord ? catalog?.find((m) => m.id === coord.id && m.provider === coord.provider) : undefined;
|
||||||
const levels = chosen ? chosen.supportedLevels : THINKING_LEVELS;
|
const levels = chosen ? chosen.supportedLevels : THINKING_LEVELS;
|
||||||
const thisMenuOpen = openMenu?.target === slot ? openMenu.kind : null;
|
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 (
|
return (
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
key={slot}
|
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"
|
menuClass="flex w-[22rem] flex-col rounded-xl border border-border bg-card p-2 text-xs shadow-xl"
|
||||||
>
|
>
|
||||||
<ModelMenuList
|
<ModelMenuList
|
||||||
models={slotCatalog ?? undefined}
|
models={catalog ?? undefined}
|
||||||
effectiveId={modelId}
|
effectiveId={modelId}
|
||||||
effectiveProvider={provider}
|
effectiveProvider={provider}
|
||||||
prioritizeImage={slot === "vision"}
|
vision={slot === "vision"}
|
||||||
onPick={(entry) => {
|
onPick={(entry) => {
|
||||||
// Auto-populate (chunk 2, owner: "populate everywhere"): the
|
// Auto-populate (chunk 2, owner: "populate everywhere"): the
|
||||||
// fresh pick writes a level — the stored one rides as-is
|
// fresh pick writes a level — the stored one rides as-is
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { ModelCatalogEntry } from "@/src/core";
|
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 { formatTokens } from "@/components/usage-format";
|
||||||
import { SearchMenuList } from "./search-menu-list";
|
import { SearchMenuList } from "./search-menu-list";
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export function ModelMenuList({
|
|||||||
effectiveProvider,
|
effectiveProvider,
|
||||||
onPick,
|
onPick,
|
||||||
footer,
|
footer,
|
||||||
prioritizeImage,
|
vision,
|
||||||
}: {
|
}: {
|
||||||
/** The full catalog — undefined while it loads. */
|
/** The full catalog — undefined while it loads. */
|
||||||
models: ModelCatalogEntry[] | undefined;
|
models: ModelCatalogEntry[] | undefined;
|
||||||
@@ -54,19 +54,25 @@ export function ModelMenuList({
|
|||||||
onPick: (entry: ModelCatalogEntry) => void;
|
onPick: (entry: ModelCatalogEntry) => void;
|
||||||
/** Quiet footer note under the list (context-specific). */
|
/** Quiet footer note under the list (context-specific). */
|
||||||
footer?: string;
|
footer?: string;
|
||||||
/** Vision slot: image-capable models float to the top (still all
|
/** Vision slot: only image-capable models are selectable — plus every
|
||||||
* selectable — a custom endpoint that doesn't report modality must
|
* custom-endpoint model, whose capability the endpoint may not report.
|
||||||
* stay pickable, so this orders rather than filters). */
|
* Image-capable ones float to the top. */
|
||||||
prioritizeImage?: boolean;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchMenuList
|
<SearchMenuList
|
||||||
items={models}
|
items={visionItems}
|
||||||
itemKey={(m) => `${m.provider}/${m.id}`}
|
itemKey={(m) => `${m.provider}/${m.id}`}
|
||||||
searchTerms={(m) => [m.provider, m.id, m.name]}
|
searchTerms={(m) => [m.provider, m.id, m.name]}
|
||||||
compare={(a, b) => a.id.localeCompare(b.id)}
|
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)}
|
isSelected={(m) => m.id === effectiveId && m.provider === (effectiveProvider ?? m.provider)}
|
||||||
onPick={onPick}
|
onPick={onPick}
|
||||||
searchTestid="model-search"
|
searchTestid="model-search"
|
||||||
|
|||||||
@@ -519,7 +519,9 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
|
|||||||
// Unavailable metadata cannot erase or clamp a saved preference.
|
// Unavailable metadata cannot erase or clamp a saved preference.
|
||||||
const levelsForEntry = metadata?.supportedLevels ?? (entry ? Object.keys(THINKING_LABEL) : []);
|
const levelsForEntry = metadata?.supportedLevels ?? (entry ? Object.keys(THINKING_LABEL) : []);
|
||||||
const available = catalog?.models ?? (failed ? [] : undefined);
|
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 (
|
return (
|
||||||
<div key={slot} data-testid={`setup-slot-${slot}`} className="flex flex-col gap-1.5">
|
<div key={slot} data-testid={`setup-slot-${slot}`} className="flex flex-col gap-1.5">
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
@@ -573,7 +575,7 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
|
|||||||
models={slotCatalog}
|
models={slotCatalog}
|
||||||
effectiveId={entry?.id}
|
effectiveId={entry?.id}
|
||||||
effectiveProvider={entry?.provider}
|
effectiveProvider={entry?.provider}
|
||||||
prioritizeImage={slot === "vision"}
|
vision={slot === "vision"}
|
||||||
onPick={(m) => {
|
onPick={(m) => {
|
||||||
editedSlots.current.add(slot);
|
editedSlots.current.add(slot);
|
||||||
setPicked((prev) => ({ ...prev, [slot]: m }));
|
setPicked((prev) => ({ ...prev, [slot]: m }));
|
||||||
|
|||||||
@@ -172,13 +172,15 @@ describe("setup polish interactions", () => {
|
|||||||
assert.equal(closed, 0);
|
assert.equal(closed, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("the vision slot lists unknown-capability models (image-capable first, nothing hidden)", async () => {
|
it("the vision slot filters out built-in text-only models (image-capable or custom only)", async () => {
|
||||||
await mount({ keyed: true });
|
await mount({ keyed: true });
|
||||||
await click(element('[data-testid="setup-trigger-vision"]'));
|
await click(element('[data-testid="setup-trigger-vision"]'));
|
||||||
// The text-only fixture stays selectable in the vision slot: the slot
|
// The text-only openrouter fixture is neither image-capable nor a custom-
|
||||||
// orders image-capable first but no longer filters, so a custom endpoint
|
// endpoint model, so the vision slot filters it out (built-ins must report
|
||||||
// that reports no modality can still be picked for Vision.
|
// image input; only custom models stay selectable when capability is unknown).
|
||||||
assert.ok(element('[data-testid="setup-menu-vision"]').textContent?.includes("fixture-text"));
|
const menu = element('[data-testid="setup-menu-vision"]');
|
||||||
|
assert.ok(!menu.textContent?.includes("fixture-text"));
|
||||||
|
assert.ok(menu.textContent?.includes("No models match."));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("catalog failure uses the approved two-sentence error", async () => {
|
it("catalog failure uses the approved two-sentence error", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user