fix(ui): filter the models page by exact model group instead of substring search

Pass the selected group as the exact model= param on /v2/model/info rather than as the substring search, so a group like gpt-4 no longer pulls gpt-4o rows into the page and count. Drop two comments that restated helper behavior.
This commit is contained in:
ryan-crabbe-berri 2026-08-29 10:18:06 -07:00
parent 56a80c8125
commit fe63ebdb19
7 changed files with 66 additions and 17 deletions

View file

@ -38,6 +38,7 @@ export const useModelsInfo = (
sortBy?: string,
sortOrder?: string,
excludeAutoRouters: boolean = false,
modelName?: string,
) => {
const { accessToken, userId, userRole } = useAuthorized();
return useQuery<PaginatedModelInfoResponse>({
@ -48,6 +49,7 @@ export const useModelsInfo = (
page,
size,
...(search && { search }),
...(modelName && { modelName }),
...(modelId && { modelId }),
...(teamId && { teamId }),
...(sortBy && { sortBy }),
@ -70,6 +72,7 @@ export const useModelsInfo = (
sortBy,
sortOrder,
excludeAutoRouters,
modelName,
),
enabled: Boolean(accessToken && userId && userRole),
});

View file

@ -33,6 +33,7 @@ interface ModelsInfoArgs {
teamId?: string;
sortBy?: string;
sortOrder?: string;
modelName?: string;
}
const modelsInfoCalls: ModelsInfoArgs[] = [];
@ -47,12 +48,14 @@ type UseModelsInfoArgs = [
teamId?: string,
sortBy?: string,
sortOrder?: string,
excludeAutoRouters?: boolean,
modelName?: string,
];
vi.mock("../../hooks/models/useModels", () => ({
useModelsInfo: (...args: UseModelsInfoArgs) => {
const [page, size, search, , teamId, sortBy, sortOrder] = args;
const call: ModelsInfoArgs = { page, size, search, teamId, sortBy, sortOrder };
const [page, size, search, , teamId, sortBy, sortOrder, , modelName] = args;
const call: ModelsInfoArgs = { page, size, search, teamId, sortBy, sortOrder, modelName };
modelsInfoCalls.push(call);
return { ...modelsInfoResult, refetch: mockRefetch };
},
@ -260,25 +263,26 @@ describe("AllModelsTab", () => {
expect(within(table).queryByText("gpt-4")).not.toBeInTheDocument();
});
it("queries the server for the selected model group so deployments beyond the first page are found", () => {
it("asks the server for the exact selected model group so deployments beyond the first page are found", () => {
render(<AllModelsTab {...defaultProps} selectedModelGroup="claude-opus" />);
expect(lastModelsInfoCall().search).toBe("claude-opus");
});
it.each(["all", "wildcard"])("does not seed the server search from the %s pseudo group", (group) => {
render(<AllModelsTab {...defaultProps} selectedModelGroup={group} />);
expect(lastModelsInfoCall().modelName).toBe("claude-opus");
expect(lastModelsInfoCall().search).toBeUndefined();
});
it("lets a typed search override the selected model group in the server query", async () => {
const user = userEvent.setup();
it.each(["all", "wildcard"])("sends no exact model name for the %s pseudo group", (group) => {
render(<AllModelsTab {...defaultProps} selectedModelGroup={group} />);
expect(lastModelsInfoCall().modelName).toBeUndefined();
});
it("keeps the exact model group alongside a typed search", async () => {
render(<AllModelsTab {...defaultProps} selectedModelGroup="claude-opus" />);
await user.type(screen.getByPlaceholderText("Search model names…"), "gpt");
fireEvent.change(screen.getByPlaceholderText("Search model names…"), { target: { value: "opus" } });
await waitFor(() => expect(lastModelsInfoCall().search).toBe("gpt"));
await waitFor(() => expect(lastModelsInfoCall().search).toBe("opus"));
expect(lastModelsInfoCall().modelName).toBe("claude-opus");
});
it("resets search, filters, team and sorting from the drawer reset button", async () => {

View file

@ -85,7 +85,7 @@ const AllModelsTab = ({
Boolean(selectedModelGroup) &&
selectedModelGroup !== ALL_MODEL_GROUPS_VALUE &&
selectedModelGroup !== WILDCARD_MODEL_GROUP_VALUE;
const searchForQuery = debouncedSearch || (isConcreteModelGroup ? selectedModelGroup ?? undefined : undefined);
const modelNameForQuery = isConcreteModelGroup ? selectedModelGroup ?? undefined : undefined;
const sortBy = useMemo(() => {
if (sorting.length === 0) return undefined;
@ -105,7 +105,7 @@ const AllModelsTab = ({
} = useModelsInfo(
pagination.pageIndex + 1,
pagination.pageSize,
searchForQuery,
debouncedSearch || undefined,
undefined,
teamIdForQuery,
sortBy,
@ -113,6 +113,7 @@ const AllModelsTab = ({
// Auto-routers are routing constructs, not deployments; the sibling Auto-Routers tab
// lists and manages them. Excluded server-side so total_count stays honest.
true,
modelNameForQuery,
);
const isLoading = isLoadingModelsInfo || isLoadingModelCostMap;

View file

@ -47,7 +47,6 @@ export interface ModelGroupFilterRouting {
setModelGroup: (modelGroup: string | null) => void;
}
/** `?model_group=` backs the All Models group filter so other pages can deep-link to one group. */
export function useModelGroupFilterRouting(): ModelGroupFilterRouting {
const [modelGroup, setParam] = useQueryState("model_group", parseAsString);

View file

@ -104,6 +104,45 @@ describe("loginCall - storeLoginToken integration", () => {
});
});
describe("modelInfoCall", () => {
let currentFetch: typeof global.fetch;
beforeEach(() => {
currentFetch = global.fetch;
});
afterEach(() => {
global.fetch = currentFetch;
});
it("sends the exact model name as the model query param and leaves search alone", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ data: [] }) } as any);
global.fetch = mockFetch as any;
await Networking.modelInfoCall(
"token",
"user",
"Admin",
2,
25,
undefined,
undefined,
undefined,
undefined,
undefined,
true,
"gpt-4",
);
const parsed = new URL(mockFetch.mock.calls[0][0] as string, "http://example.com");
expect(parsed.pathname).toBe("/v2/model/info");
expect(parsed.searchParams.get("model")).toBe("gpt-4");
expect(parsed.searchParams.has("search")).toBe(false);
expect(parsed.searchParams.get("page")).toBe("2");
expect(parsed.searchParams.get("exclude_auto_routers")).toBe("true");
});
});
describe("daily activity helpers", () => {
const startTime = new Date("2025-02-12T00:00:00.000Z");
const endTime = new Date("2025-02-19T00:00:00.000Z");

View file

@ -1656,6 +1656,7 @@ export const modelInfoCall = async (
sortBy?: string,
sortOrder?: string,
excludeAutoRouters?: boolean,
modelName?: string,
) => {
/**
* Get all models on proxy
@ -1669,6 +1670,9 @@ export const modelInfoCall = async (
if (search && search.trim()) {
params.append("search", search.trim());
}
if (modelName && modelName.trim()) {
params.append("model", modelName.trim());
}
if (modelId && modelId.trim()) {
params.append("modelId", modelId.trim());
}

View file

@ -22,7 +22,6 @@ export function orgDetailHref(orgId: string): string {
return `${migratedHref("organizations")}?org=${encodeURIComponent(orgId)}`;
}
/** Models page filtered to one model group; undefined for grant sentinels that name no deployment. */
export function modelGroupHref(modelGroup: string): string | undefined {
if (MODEL_GRANT_SENTINELS.has(modelGroup)) return undefined;
return `${migratedHref("models-and-endpoints")}?model_group=${encodeURIComponent(modelGroup)}`;