Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/features/16749-central-model-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(dashboard):** hide/show models across all providers from the Models catalog — a Visible toggle, an All/Visible/Hidden filter and "Hide/Show filtered" bulk actions write the same per-provider visibility that `/v1/models` honors ([#16749](https://github.com/diegosouzapw/OmniRoute/pull/16749)) — thanks @MuhammadUsamaMX
41 changes: 41 additions & 0 deletions src/app/(dashboard)/dashboard/models/CatalogBulkActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@ function CatalogBulkProgress({ running, progress }: { running: boolean; progress
) : null;
}

/** Hide/show every filtered model; omitted when the catalog has no visibility data. */
export interface CatalogVisibilityActions {
hideCount: number;
showCount: number;
busy: boolean;
onHideFiltered: () => void;
onShowFiltered: () => void;
}

function CatalogVisibilityButtons({ actions }: { actions: CatalogVisibilityActions }) {
const t = useTranslations("modelCatalog");
return (
<>
<Button
variant="secondary"
size="sm"
icon="visibility_off"
disabled={actions.hideCount === 0 || actions.busy}
onClick={actions.onHideFiltered}
data-testid="hide-filtered-btn"
>
{t("hideFiltered", { count: actions.hideCount })}
</Button>
<Button
variant="secondary"
size="sm"
icon="visibility"
disabled={actions.showCount === 0 || actions.busy}
onClick={actions.onShowFiltered}
data-testid="show-filtered-btn"
>
{t("showFiltered", { count: actions.showCount })}
</Button>
</>
);
}

export default function CatalogBulkActionBar({
selectedCount,
filteredCount,
Expand All @@ -56,6 +93,7 @@ export default function CatalogBulkActionBar({
onTestFiltered,
onCancel,
onClearResults,
visibilityActions,
}: {
selectedCount: number;
filteredCount: number;
Expand All @@ -66,6 +104,7 @@ export default function CatalogBulkActionBar({
onTestFiltered: () => void;
onCancel: () => void;
onClearResults: () => void;
visibilityActions?: CatalogVisibilityActions;
}) {
const t = useTranslations("modelCatalog");

Expand Down Expand Up @@ -107,6 +146,8 @@ export default function CatalogBulkActionBar({
{t("clearResults")}
</Button>
)}

{visibilityActions && <CatalogVisibilityButtons actions={visibilityActions} />}
</div>

<CatalogBulkProgress running={running} progress={progress} />
Expand Down
12 changes: 11 additions & 1 deletion src/app/(dashboard)/dashboard/models/ModelCatalogFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function ModelCatalogOptionFilters({
);
}

/** Filters with fixed choices (pricing, provider health, test result) and numeric minimums. */
/** Fixed-choice filters (pricing, visibility, provider health, test result) and numeric minimums. */
function ModelCatalogFixedFilters({
filters,
onChange,
Expand All @@ -93,6 +93,16 @@ function ModelCatalogFixedFilters({
<option value="paid">{t("nonFree")}</option>
</FilterSelect>

<FilterSelect
label={t("visibility")}
value={filters.visibility ?? "all"}
onChange={(visibility) => onChange({ visibility })}
>
<option value="all">{t("allVisibility")}</option>
<option value="visible">{t("visibleOnly")}</option>
<option value="hidden">{t("hiddenOnly")}</option>
</FilterSelect>

<FilterSelect
label={t("providerHealth")}
value={filters.providerHealth ?? "all"}
Expand Down
67 changes: 65 additions & 2 deletions src/app/(dashboard)/dashboard/models/ModelCatalogTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function ModelCatalogTableHead({
someOnPageSelected,
onToggleSelectAll,
showActions,
showVisibility,
}: {
labels: ModelCatalogLabels;
sortField: CatalogSortField;
Expand All @@ -119,6 +120,7 @@ function ModelCatalogTableHead({
someOnPageSelected: boolean;
onToggleSelectAll?: () => void;
showActions: boolean;
showVisibility: boolean;
}) {
const t = useTranslations("modelCatalog");
return (
Expand Down Expand Up @@ -169,6 +171,7 @@ function ModelCatalogTableHead({
onSort={onSort}
/>
<PlainHeading>{labels.flags || t("flags")}</PlainHeading>
{showVisibility && <PlainHeading>{t("visibleColumn")}</PlainHeading>}
<PlainHeading>{t("healthTest")}</PlainHeading>
{showActions && <ActionsHeading />}
</TableRow>
Expand Down Expand Up @@ -265,7 +268,12 @@ function ModelFlagsCell({ model, labels }: { model: CatalogModelRow; labels: Mod
{labels.free || t("free")}
</Badge>
)}
{model.custom !== true && model.free !== true && (
{model.hidden === true && (
<Badge size="sm" variant="warning">
{t("hiddenBadge")}
</Badge>
)}
{model.custom !== true && model.free !== true && model.hidden !== true && (
<span className="text-text-muted" title={t("noFlags")}>
—
</span>
Expand All @@ -275,6 +283,44 @@ function ModelFlagsCell({ model, labels }: { model: CatalogModelRow; labels: Mod
);
}

/** Eye toggle: shows/hides the model in `/v1/models` via the provider-models store. */
function ModelVisibilityCell({
model,
busy,
onToggle,
}: {
model: CatalogModelRow;
busy: boolean;
onToggle: (model: CatalogModelRow) => void;
}) {
const t = useTranslations("modelCatalog");
const hidden = model.hidden === true;
return (
<TableCell className="h-auto px-4 py-3 align-top">
<button
type="button"
role="switch"
aria-checked={!hidden}
aria-label={
hidden ? t("showModel", { name: model.name }) : t("hideModel", { name: model.name })
}
title={hidden ? t("showModel", { name: model.name }) : t("hideModel", { name: model.name })}
disabled={busy || !model.visibility}
onClick={() => onToggle(model)}
data-testid={`toggle-visibility-${model.id}`}
className="inline-flex size-8 items-center justify-center rounded-md text-text-muted hover:bg-black/5 hover:text-text-main focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:opacity-40 dark:hover:bg-white/5"
>
<span
className={`material-symbols-outlined text-lg ${hidden ? "" : "text-primary"}`}
aria-hidden="true"
>
{hidden ? "visibility_off" : "visibility"}
</span>
</button>
</TableCell>
);
}

function ModelCatalogTableRow({
model,
labels,
Expand All @@ -285,6 +331,8 @@ function ModelCatalogTableRow({
onToggleSelect,
onTestModel,
bulkRunning,
onToggleVisibility,
visibilityBusy,
}: {
model: CatalogModelRow;
labels: ModelCatalogLabels;
Expand All @@ -295,6 +343,8 @@ function ModelCatalogTableRow({
onToggleSelect?: (id: string) => void;
onTestModel?: (providerId: string, modelId: string) => void;
bulkRunning: boolean;
onToggleVisibility?: (model: CatalogModelRow) => void;
visibilityBusy: boolean;
}) {
const t = useTranslations("modelCatalog");
const common = useTranslations("common");
Expand All @@ -309,7 +359,10 @@ function ModelCatalogTableRow({
output: common("output"),
});
return (
<TableRow data-state={isSelected ? "selected" : undefined} className="align-top">
<TableRow
data-state={isSelected ? "selected" : undefined}
className={`align-top ${model.hidden === true ? "opacity-60" : ""}`}
>
{onToggleSelect && (
<SelectRowCell
checked={isSelected}
Expand All @@ -331,6 +384,9 @@ function ModelCatalogTableRow({
{formatOptionalCount(model.max_output_tokens)}
</TableCell>
<ModelFlagsCell model={model} labels={labels} />
{onToggleVisibility && (
<ModelVisibilityCell model={model} busy={visibilityBusy} onToggle={onToggleVisibility} />
)}
<TableCell className="h-auto px-4 py-3 align-top">
<CatalogTestBadge result={result} loading={isTesting} />
</TableCell>
Expand Down Expand Up @@ -374,6 +430,8 @@ interface ModelCatalogTableProps {
providerHealthMap?: Record<string, "healthy" | "degraded" | "down">;
/** A bulk run owns the runner; per-row tests wait until it ends. */
bulkRunning?: boolean;
onToggleVisibility?: (model: CatalogModelRow) => void;
visibilityBusy?: boolean;
}

export default function ModelCatalogTable({
Expand All @@ -398,6 +456,8 @@ export default function ModelCatalogTable({
onTestModel,
providerHealthMap = {},
bulkRunning = false,
onToggleVisibility,
visibilityBusy = false,
}: ModelCatalogTableProps) {
const t = useTranslations("modelCatalog");
const firstResult = startIndex + 1;
Expand Down Expand Up @@ -425,6 +485,7 @@ export default function ModelCatalogTable({
someOnPageSelected={someOnPageSelected}
onToggleSelectAll={onToggleSelectAll}
showActions={Boolean(onTestModel)}
showVisibility={Boolean(onToggleVisibility)}
/>
<TableBody>
{rows.map((model) => (
Expand All @@ -439,6 +500,8 @@ export default function ModelCatalogTable({
onToggleSelect={onToggleSelect}
onTestModel={onTestModel}
bulkRunning={bulkRunning}
onToggleVisibility={onToggleVisibility}
visibilityBusy={visibilityBusy}
/>
))}
</TableBody>
Expand Down
5 changes: 5 additions & 0 deletions src/app/(dashboard)/dashboard/models/catalogUrlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const DEFAULT_MODEL_FILTERS: CatalogFilters = {
pricing: "all",
providerHealth: "all",
testResult: "all",
visibility: "all",
minContextLength: undefined,
minMaxOutputTokens: undefined,
};
Expand All @@ -29,6 +30,7 @@ const PRICING_VALUES = new Set(["free", "paid"]);
const HEALTH_VALUES = new Set(["healthy", "degraded", "down"]);
const TEST_RESULT_VALUES = new Set(["untested", "ok", "slow", "error"]);
const COMBO_STATUS_VALUES = new Set(["active", "paused"]);
const VISIBILITY_VALUES = new Set(["visible", "hidden"]);

/** Whole numbers ≥ 0 only; anything else (empty, NaN, negative, fractional) means "no filter". */
export function parseNonNegativeInt(raw: string | null | undefined): number | undefined {
Expand Down Expand Up @@ -58,6 +60,7 @@ export function parseModelFilters(params: URLSearchParams): CatalogFilters {
pricing: pickAllowed(params.get("pricing"), PRICING_VALUES),
providerHealth: pickAllowed(params.get("health"), HEALTH_VALUES),
testResult: pickAllowed(onModelsTab ? params.get("testResult") : null, TEST_RESULT_VALUES),
visibility: pickAllowed(params.get("visibility"), VISIBILITY_VALUES),
minContextLength: parseNonNegativeInt(params.get("minContext")),
minMaxOutputTokens: parseNonNegativeInt(params.get("minOutput")),
};
Expand Down Expand Up @@ -104,6 +107,7 @@ export function buildCatalogSearchParams(
setIfActive(params, "pricing", modelFilters.pricing);
setIfActive(params, "health", modelFilters.providerHealth);
setIfActive(params, "testResult", modelFilters.testResult);
setIfActive(params, "visibility", modelFilters.visibility);
setIfNumber(params, "minContext", modelFilters.minContextLength);
setIfNumber(params, "minOutput", modelFilters.minMaxOutputTokens);
return params;
Expand Down Expand Up @@ -176,6 +180,7 @@ export function hasActiveModelFilters(filters: CatalogFilters): boolean {
Boolean(filters.pricing && filters.pricing !== "all") ||
Boolean(filters.providerHealth && filters.providerHealth !== "all") ||
Boolean(filters.testResult && filters.testResult !== "all") ||
Boolean(filters.visibility && filters.visibility !== "all") ||
typeof filters.minContextLength === "number" ||
typeof filters.minMaxOutputTokens === "number"
);
Expand Down
42 changes: 42 additions & 0 deletions src/app/(dashboard)/dashboard/models/catalogVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { CatalogModelRow } from "./modelCatalogUtils";

/** One `PATCH /api/provider-models?provider=` request body, scoped to one modality. */
export interface CatalogVisibilityPatch {
provider: string;
modality: string;
modelIds: string[];
}

export interface CatalogVisibilityCounts {
visible: number;
hidden: number;
total: number;
}

export function countCatalogVisibility(rows: CatalogModelRow[]): CatalogVisibilityCounts {
const hidden = rows.filter((row) => row.hidden === true).length;
return { visible: rows.length - hidden, hidden, total: rows.length };
}

/**
* Group the rows whose visibility must change into one PATCH per provider + modality.
* Rows already in the requested state, and rows without a visibility target (combos,
* catalogs loaded without `includeHidden`), are skipped. Order follows the rows.
*/
export function groupVisibilityPatches(
rows: CatalogModelRow[],
hidden: boolean
): CatalogVisibilityPatch[] {
const patches = new Map<string, CatalogVisibilityPatch>();
for (const row of rows) {
if (!row.visibility || (row.hidden === true) === hidden) continue;
const { modality, targets } = row.visibility;
for (const { provider, modelId } of targets) {
const key = `${provider}\u0000${modality}`;
const patch = patches.get(key) ?? { provider, modality, modelIds: [] };
if (!patch.modelIds.includes(modelId)) patch.modelIds.push(modelId);
patches.set(key, patch);
}
}
return [...patches.values()];
}
Loading
Loading