mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge 56a80c8125 into 3beb02e512
This commit is contained in:
commit
8ebaa32c53
13 changed files with 244 additions and 26 deletions
|
|
@ -260,6 +260,27 @@ 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", () => {
|
||||
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().search).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets a typed search override the selected model group in the server query", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AllModelsTab {...defaultProps} selectedModelGroup="claude-opus" />);
|
||||
|
||||
await user.type(screen.getByPlaceholderText("Search model names…"), "gpt");
|
||||
|
||||
await waitFor(() => expect(lastModelsInfoCall().search).toBe("gpt"));
|
||||
});
|
||||
|
||||
it("resets search, filters, team and sorting from the drawer reset button", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AllModelsTab {...defaultProps} selectedModelGroup="gpt-4" />);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ const AllModelsTab = ({
|
|||
}, [modelNameSearch, debouncedUpdateSearch]);
|
||||
|
||||
const teamIdForQuery = selectedTeamValue === PERSONAL_TEAM_VALUE ? undefined : selectedTeamValue;
|
||||
const isConcreteModelGroup =
|
||||
Boolean(selectedModelGroup) &&
|
||||
selectedModelGroup !== ALL_MODEL_GROUPS_VALUE &&
|
||||
selectedModelGroup !== WILDCARD_MODEL_GROUP_VALUE;
|
||||
const searchForQuery = debouncedSearch || (isConcreteModelGroup ? selectedModelGroup ?? undefined : undefined);
|
||||
|
||||
const sortBy = useMemo(() => {
|
||||
if (sorting.length === 0) return undefined;
|
||||
|
|
@ -100,7 +105,7 @@ const AllModelsTab = ({
|
|||
} = useModelsInfo(
|
||||
pagination.pageIndex + 1,
|
||||
pagination.pageSize,
|
||||
debouncedSearch || undefined,
|
||||
searchForQuery,
|
||||
undefined,
|
||||
teamIdForQuery,
|
||||
sortBy,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { withNuqsTestingAdapter, type UrlUpdateEvent } from "nuqs/adapters/testing";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { useModelDetailRouting } from "./detailNavigation";
|
||||
import { useModelDetailRouting, useModelGroupFilterRouting } from "./detailNavigation";
|
||||
|
||||
describe("useModelDetailRouting", () => {
|
||||
it("openModel sets ?model= with a history push", async () => {
|
||||
|
|
@ -54,3 +54,29 @@ describe("useModelDetailRouting", () => {
|
|||
expect(result.current.teamId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useModelGroupFilterRouting", () => {
|
||||
it("reads the selected group from ?model_group=", () => {
|
||||
const { result } = renderHook(() => useModelGroupFilterRouting(), {
|
||||
wrapper: withNuqsTestingAdapter({ searchParams: "?model_group=gpt-4.1" }),
|
||||
});
|
||||
expect(result.current.modelGroup).toBe("gpt-4.1");
|
||||
});
|
||||
|
||||
it("writes the selected group to ?model_group= and clears it on null", async () => {
|
||||
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
|
||||
const { result } = renderHook(() => useModelGroupFilterRouting(), {
|
||||
wrapper: withNuqsTestingAdapter({ onUrlUpdate }),
|
||||
});
|
||||
await act(async () => {
|
||||
result.current.setModelGroup("claude-sonnet-5");
|
||||
});
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get("model_group")).toBe("claude-sonnet-5");
|
||||
|
||||
await act(async () => {
|
||||
result.current.setModelGroup(null);
|
||||
});
|
||||
await waitFor(() => expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.has("model_group")).toBe(false));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseAsString, useQueryStates } from "nuqs";
|
||||
import { parseAsString, useQueryState, useQueryStates } from "nuqs";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export interface ModelDetailRouting {
|
||||
|
|
@ -41,3 +41,22 @@ export function useModelDetailRouting(): ModelDetailRouting {
|
|||
close,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModelGroupFilterRouting {
|
||||
modelGroup: string | null;
|
||||
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);
|
||||
|
||||
const setModelGroup = useCallback(
|
||||
(next: string | null) => {
|
||||
void setParam(next);
|
||||
},
|
||||
[setParam],
|
||||
);
|
||||
|
||||
return { modelGroup, setModelGroup };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab";
|
||||
import { ALL_MODEL_GROUPS_VALUE } from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTable";
|
||||
import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData";
|
||||
import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
|
||||
import {
|
||||
useModelDetailRouting,
|
||||
useModelGroupFilterRouting,
|
||||
} from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
|
||||
|
||||
export default function AllModelsPanel() {
|
||||
const [selectedModelGroup, setSelectedModelGroup] = useState<string | null>(null);
|
||||
const { modelGroup, setModelGroup } = useModelGroupFilterRouting();
|
||||
const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData();
|
||||
const { openModel, openTeam } = useModelDetailRouting();
|
||||
|
||||
return (
|
||||
<AllModelsTab
|
||||
selectedModelGroup={selectedModelGroup}
|
||||
setSelectedModelGroup={setSelectedModelGroup}
|
||||
selectedModelGroup={modelGroup}
|
||||
setSelectedModelGroup={(group) => setModelGroup(group === ALL_MODEL_GROUPS_VALUE ? null : group)}
|
||||
availableModelGroups={availableModelGroups}
|
||||
availableModelAccessGroups={availableModelAccessGroups}
|
||||
setSelectedModelId={openModel}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { StatusBadge, type StatusTone } from "./status_badge";
|
||||
|
||||
const push = vi.fn();
|
||||
vi.mock("next/navigation", () => ({ useRouter: () => ({ push }) }));
|
||||
|
||||
describe("StatusBadge", () => {
|
||||
const toneClasses: Record<StatusTone, string[]> = {
|
||||
success: ["border-success/20", "bg-success/10", "text-success"],
|
||||
|
|
@ -39,4 +42,19 @@ describe("StatusBadge", () => {
|
|||
await user.hover(screen.getByText("Blocked"));
|
||||
expect(await screen.findByText("This key was blocked by SCIM")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a tinted anchor that navigates client-side when href is given", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatusBadge tone="info" label="gpt-4.1" href="/models-and-endpoints?model_group=gpt-4.1" />);
|
||||
const link = screen.getByRole("link", { name: "gpt-4.1" });
|
||||
expect(link).toHaveAttribute("href", "/models-and-endpoints?model_group=gpt-4.1");
|
||||
expect(link.className).toContain("text-info");
|
||||
await user.click(link);
|
||||
expect(push).toHaveBeenCalledWith("/models-and-endpoints?model_group=gpt-4.1");
|
||||
});
|
||||
|
||||
it("renders no anchor without an href", () => {
|
||||
render(<StatusBadge tone="info" label="gpt-4.1" />);
|
||||
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import * as React from "react";
|
||||
|
||||
import { useEntityLinkClick } from "@/components/shared/EntityLink";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
|
||||
|
|
@ -23,15 +24,17 @@ interface StatusBadgeProps {
|
|||
tooltip?: React.ReactNode;
|
||||
dataTestId?: string;
|
||||
className?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function StatusBadge({ tone, label, tooltip, dataTestId, className }: StatusBadgeProps) {
|
||||
const badge = (
|
||||
<Badge
|
||||
variant="outline"
|
||||
data-testid={dataTestId}
|
||||
className={cn("whitespace-nowrap font-normal", TONE_CLASS[tone], className)}
|
||||
>
|
||||
export function StatusBadge({ tone, label, tooltip, dataTestId, className, href }: StatusBadgeProps) {
|
||||
const badgeClassName = cn("whitespace-nowrap font-normal", TONE_CLASS[tone], className);
|
||||
const badge = href ? (
|
||||
<LinkedStatusBadge href={href} dataTestId={dataTestId} className={badgeClassName}>
|
||||
{label}
|
||||
</LinkedStatusBadge>
|
||||
) : (
|
||||
<Badge variant="outline" data-testid={dataTestId} className={badgeClassName}>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
|
|
@ -41,3 +44,25 @@ export function StatusBadge({ tone, label, tooltip, dataTestId, className }: Sta
|
|||
}
|
||||
return <CellTooltip content={tooltip} trigger={badge} />;
|
||||
}
|
||||
|
||||
interface LinkedStatusBadgeProps {
|
||||
href: string;
|
||||
dataTestId?: string;
|
||||
className: string;
|
||||
children: string;
|
||||
}
|
||||
|
||||
function LinkedStatusBadge({ href, dataTestId, className, children }: LinkedStatusBadgeProps) {
|
||||
const handleClick = useEntityLinkClick(href);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
data-testid={dataTestId}
|
||||
className={cn("cursor-pointer hover:underline", className)}
|
||||
render={<a href={href} onClick={handleClick} />}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
serverRootPath: "",
|
||||
teamInfoCall: vi.fn(),
|
||||
teamMemberDeleteCall: vi.fn(),
|
||||
teamMemberAddCall: vi.fn(),
|
||||
|
|
@ -278,6 +281,36 @@ describe("TeamInfoView", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("links direct and access-group model badges to the models page filtered to that group", async () => {
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue(
|
||||
createMockTeamData({
|
||||
models: ["gpt-4.1"],
|
||||
access_group_models: ["claude-sonnet-5"],
|
||||
access_group_details: [{ access_group_id: "ag-1", access_group_name: "prod", models: ["claude-sonnet-5"] }],
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<TeamInfoView {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByRole("link", { name: "gpt-4.1" })).toHaveAttribute(
|
||||
"href",
|
||||
expect.stringContaining("/models-and-endpoints?model_group=gpt-4.1"),
|
||||
);
|
||||
expect(screen.getByRole("link", { name: "claude-sonnet-5" })).toHaveAttribute(
|
||||
"href",
|
||||
expect.stringContaining("/models-and-endpoints?model_group=claude-sonnet-5"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the all-proxy-models badge non-clickable", async () => {
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData({ models: ["all-proxy-models"] }));
|
||||
|
||||
renderWithProviders(<TeamInfoView {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByText("All proxy models")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("link", { name: "All proxy models" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display loading state while fetching team data", () => {
|
||||
vi.mocked(networking.teamInfoCall).mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import type { ObjectPermission } from "@/components/object_permission_types";
|
|||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells/status_badge";
|
||||
import { BadgeLink } from "@/components/shared/BadgeLink";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { modelGroupHref } from "@/utils/entityLinks";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Input as UIInput } from "@/components/ui/input";
|
||||
|
|
@ -53,6 +55,7 @@ import {
|
|||
computeTeamModelBadges,
|
||||
normalizeTeamModelSelection,
|
||||
TeamAccessGroupModelGrant,
|
||||
TeamModelBadge,
|
||||
TeamModelBadgeKind,
|
||||
} from "./teamModelAccess";
|
||||
import MetadataKeyValueFields, {
|
||||
|
|
@ -111,6 +114,9 @@ const TEAM_MODEL_BADGE_TONES: Record<TeamModelBadgeKind, StatusTone> = {
|
|||
"access-group": "success",
|
||||
};
|
||||
|
||||
const teamModelBadgeHref = (badge: TeamModelBadge): string | undefined =>
|
||||
badge.kind === "direct" || badge.kind === "access-group" ? modelGroupHref(badge.label) : undefined;
|
||||
|
||||
export interface TeamMembership {
|
||||
user_id: string;
|
||||
team_id: string;
|
||||
|
|
@ -1006,7 +1012,11 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
(badge, index) => (
|
||||
<SimpleTooltip key={`${badge.kind}-${badge.label}-${index}`} content={badge.tooltip}>
|
||||
<span>
|
||||
<StatusBadge tone={TEAM_MODEL_BADGE_TONES[badge.kind]} label={badge.label} />
|
||||
<StatusBadge
|
||||
tone={TEAM_MODEL_BADGE_TONES[badge.kind]}
|
||||
label={badge.label}
|
||||
href={teamModelBadgeHref(badge)}
|
||||
/>
|
||||
</span>
|
||||
</SimpleTooltip>
|
||||
),
|
||||
|
|
@ -1727,9 +1737,9 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<p className="font-medium">Models</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{info.models.map((model, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
<BadgeLink key={index} href={modelGroupHref(model)}>
|
||||
{model}
|
||||
</Badge>
|
||||
</BadgeLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1738,9 +1748,9 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<p className="font-medium">Default Member Models</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{info.default_team_member_models.map((model, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
<BadgeLink key={index} href={modelGroupHref(model)}>
|
||||
{model}
|
||||
</Badge>
|
||||
</BadgeLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -561,6 +561,32 @@ describe("KeyInfoView", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("links each model chip to the models page filtered to that model group", async () => {
|
||||
const keyData = { ...MOCK_KEY_DATA, models: ["gpt-4.1", "anthropic/*"] };
|
||||
renderWithProviders(
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId="test-key-id" onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole("link", { name: "gpt-4.1" })).toHaveAttribute(
|
||||
"href",
|
||||
expect.stringContaining("/models-and-endpoints?model_group=gpt-4.1"),
|
||||
);
|
||||
expect(screen.getByRole("link", { name: "anthropic/*" })).toHaveAttribute(
|
||||
"href",
|
||||
expect.stringContaining("/models-and-endpoints?model_group=anthropic%2F*"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the all-proxy-models grant chip non-clickable", async () => {
|
||||
const keyData = { ...MOCK_KEY_DATA, models: ["all-proxy-models"] };
|
||||
renderWithProviders(
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId="test-key-id" onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
expect((await screen.findAllByText("all-proxy-models")).length).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole("link", { name: "all-proxy-models" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders no team link when the key has no team", async () => {
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import { Card } from "@/components/ui/card";
|
|||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { EntityLink } from "@/components/shared/EntityLink";
|
||||
import { teamDetailHref } from "@/utils/entityLinks";
|
||||
import { modelGroupHref, teamDetailHref } from "@/utils/entityLinks";
|
||||
import { BadgeLink } from "@/components/shared/BadgeLink";
|
||||
import { KeyInfoHeader } from "./KeyInfoHeader";
|
||||
import KeySavingsTab from "./KeySavingsTab";
|
||||
import { useEffect, useState } from "react";
|
||||
|
|
@ -660,9 +661,9 @@ export default function KeyInfoView({
|
|||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<Badge key={index} variant="secondary" className="min-w-0 break-words">
|
||||
<BadgeLink key={index} href={modelGroupHref(model)} className="min-w-0 break-words">
|
||||
{model}
|
||||
</Badge>
|
||||
</BadgeLink>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm">No models specified</p>
|
||||
|
|
@ -996,9 +997,9 @@ export default function KeyInfoView({
|
|||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-info/15 rounded-sm text-xs">
|
||||
<BadgeLink key={index} href={modelGroupHref(model)} className="min-w-0 break-words">
|
||||
{model}
|
||||
</span>
|
||||
</BadgeLink>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm">No models specified</p>
|
||||
|
|
|
|||
19
ui/litellm-dashboard/src/utils/entityLinks.test.ts
Normal file
19
ui/litellm-dashboard/src/utils/entityLinks.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
|
||||
|
||||
import { modelGroupHref } from "./entityLinks";
|
||||
|
||||
describe("modelGroupHref", () => {
|
||||
it("targets the models page filtered to the encoded model group", () => {
|
||||
expect(modelGroupHref("gpt-4.1")).toMatch(/\/models-and-endpoints\?model_group=gpt-4\.1$/);
|
||||
expect(modelGroupHref("openai/*")).toMatch(/\?model_group=openai%2F\*$/);
|
||||
});
|
||||
|
||||
it.each(["all-proxy-models", "all-team-models", "no-default-models"])(
|
||||
"returns no href for the %s grant sentinel",
|
||||
(sentinel) => {
|
||||
expect(modelGroupHref(sentinel)).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
import { migratedHref } from "@/utils/migratedPages";
|
||||
|
||||
const MODEL_GRANT_SENTINELS: ReadonlySet<string> = new Set([
|
||||
"all-proxy-models",
|
||||
"all-team-models",
|
||||
"no-default-models",
|
||||
]);
|
||||
|
||||
export function teamDetailHref(teamId: string): string {
|
||||
return `${migratedHref("teams")}?team=${encodeURIComponent(teamId)}`;
|
||||
}
|
||||
|
|
@ -15,3 +21,9 @@ export function userDetailHref(userId: string): string {
|
|||
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)}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue