mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(ui): status filter + sort and provider-aware search on Models & Endpoints
The models table mixes active and paused deployments with no way to tell them apart beyond the pause toggle, and the drawer only filters by public model name and access group. Backend (/v2/model/info): - new optional `blocked` query param to filter by routing status - new `blocked` sort field (asc = active first); the existing `status` field keeps sorting by config-vs-DB source - `search` now also matches litellm_params.model, so typing a provider or upstream model id (e.g. "nvidia_nim", "openai/gpt-4") finds deployments whose public name doesn't mention them Frontend (Models & Endpoints): - visible Status column with Active/Paused badges, sortable (active first on asc) - Status filter (All/Active/Paused) in the Filters drawer, wired to the new server param through URL state The routing-status filter ignores non-bool sentinels the same way exclude_auto_routers does, so direct calls bypassing FastAPI keep their no-filter behavior.
This commit is contained in:
parent
7f95b5f361
commit
5f8705bcdc
8 changed files with 263 additions and 14 deletions
|
|
@ -14466,11 +14466,24 @@ async def _fetch_db_models_for_search(
|
|||
filter for `team_public_model_name` instead and keep the DB cost
|
||||
bounded by `search`.
|
||||
"""
|
||||
db_where_condition: Final[dict[str, Any]] = {
|
||||
"model_name": {"contains": search_lower, "mode": "insensitive"} if model_name is None else model_name
|
||||
}
|
||||
if db_model_ids_in_router:
|
||||
db_where_condition["model_id"] = {"not": {"in": list(db_model_ids_in_router)}}
|
||||
db_where_condition: Final[dict[str, Any]] = (
|
||||
{
|
||||
"AND": [
|
||||
{
|
||||
"OR": [
|
||||
{"model_name": {"contains": search_lower, "mode": "insensitive"}},
|
||||
# JSON string_contains is case-sensitive on Postgres (see
|
||||
# note above); router-side matching below covers the
|
||||
# case-insensitive path for rows already in the router.
|
||||
{"litellm_params": {"path": ["model"], "string_contains": search_lower}},
|
||||
]
|
||||
},
|
||||
*( [{"model_id": {"not": {"in": list(db_model_ids_in_router)}}}] if db_model_ids_in_router else [] ),
|
||||
]
|
||||
}
|
||||
if model_name is None
|
||||
else {"model_name": model_name}
|
||||
)
|
||||
|
||||
# Unsorted searches only need enough DB rows to fill the current
|
||||
# page after counting router-side matches. Sorted searches need
|
||||
|
|
@ -14566,7 +14579,13 @@ async def _apply_search_filter_to_models(
|
|||
if search_lower in (m.get("model_name") or "").lower():
|
||||
return True
|
||||
team_public_model_name: Final = (m.get("model_info") or {}).get("team_public_model_name") or ""
|
||||
return search_lower in team_public_model_name.lower()
|
||||
if search_lower in team_public_model_name.lower():
|
||||
return True
|
||||
# Also match the underlying LiteLLM model name (e.g.
|
||||
# "openrouter/deepseek/deepseek-chat"), so users can find
|
||||
# deployments by typing the provider or the upstream model id.
|
||||
litellm_model: Final = (m.get("litellm_params") or {}).get("model") or ""
|
||||
return search_lower in litellm_model.lower()
|
||||
|
||||
# Filter models in router by search term, dropping BYOK rows that
|
||||
# belong to teams the caller is not a member of so search can't leak
|
||||
|
|
@ -14687,6 +14706,7 @@ def _sort_models(
|
|||
"updated_at",
|
||||
"costs",
|
||||
"status",
|
||||
"blocked",
|
||||
]:
|
||||
return all_models
|
||||
|
||||
|
|
@ -14744,6 +14764,11 @@ def _sort_models(
|
|||
db_model: Final = model_info.get("db_model", False)
|
||||
return db_model
|
||||
|
||||
elif sort_by == "blocked":
|
||||
# Routing status: False (active) comes before True (paused) for asc,
|
||||
# so `sortBy=blocked&sortOrder=asc` surfaces the active deployments.
|
||||
return bool(model_info.get("blocked", False))
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
|
|
@ -14783,11 +14808,20 @@ def _matches_model_info_filters(
|
|||
exclude_auto_routers: bool | None,
|
||||
access_group: str | None,
|
||||
wildcard_only: bool | None,
|
||||
blocked: bool | None = None,
|
||||
) -> bool:
|
||||
if exclude_auto_routers is True and _is_auto_router_model(model):
|
||||
return False
|
||||
if isinstance(access_group, str) and not _model_in_access_group(model, access_group):
|
||||
return False
|
||||
# Routing-status filter. Guarded on `is True` / `is False` because direct
|
||||
# calls that bypass FastAPI pass the truthy Query sentinel as the default,
|
||||
# which must not filter (same pattern as `exclude_auto_routers`). Entries
|
||||
# without a `blocked` flag (e.g. A2A agents) are neither active nor paused,
|
||||
# so they drop out of both filtered views.
|
||||
if blocked is True or blocked is False:
|
||||
if (model.get("model_info") or {}).get("blocked") is not blocked:
|
||||
return False
|
||||
return wildcard_only is not True or "*" in str(model.get("model_name") or "")
|
||||
|
||||
|
||||
|
|
@ -15086,12 +15120,19 @@ async def model_info_v2(
|
|||
),
|
||||
sortBy: str | None = fastapi.Query(
|
||||
None,
|
||||
description="Field to sort by. Options: model_name, created_at, updated_at, costs, status",
|
||||
description="Field to sort by. Options: model_name, created_at, updated_at, costs, status, blocked",
|
||||
),
|
||||
sortOrder: str | None = fastapi.Query(
|
||||
"asc",
|
||||
description="Sort order. Options: asc, desc",
|
||||
),
|
||||
blocked: bool | None = fastapi.Query(
|
||||
None,
|
||||
description=(
|
||||
"Filter by routing status: false = active deployments, true = paused (blocked) "
|
||||
"deployments. Omit to return both."
|
||||
),
|
||||
),
|
||||
exclude_auto_routers: bool | None = fastapi.Query(
|
||||
False,
|
||||
description=(
|
||||
|
|
@ -15125,7 +15166,8 @@ async def model_info_v2(
|
|||
search: Case-insensitive partial match on model name or team public name.
|
||||
modelId: Return a single deployment by LiteLLM model id.
|
||||
teamId: Filter to models with direct access or team membership for this team id.
|
||||
sortBy / sortOrder: Sort by model_name, created_at, updated_at, costs, or status.
|
||||
sortBy / sortOrder: Sort by model_name, created_at, updated_at, costs, status, or blocked.
|
||||
blocked: Filter by routing status (false = active, true = paused).
|
||||
access_group: Only return deployments in this model access group.
|
||||
wildcard_only: Only return deployments whose `model_name` contains `*`.
|
||||
|
||||
|
|
@ -15282,7 +15324,8 @@ async def model_info_v2(
|
|||
# `is True` because direct-call tests bypass FastAPI, so the Query default arrives as a
|
||||
# truthy sentinel object rather than False.
|
||||
all_models = [
|
||||
m for m in all_models if _matches_model_info_filters(m, exclude_auto_routers, access_group, wildcard_only)
|
||||
m for m in all_models
|
||||
if _matches_model_info_filters(m, exclude_auto_routers, access_group, wildcard_only, blocked)
|
||||
]
|
||||
|
||||
# Update total count to include agents
|
||||
|
|
|
|||
|
|
@ -802,6 +802,122 @@ def test_v2_model_info_exclude_auto_routers_paginates_over_the_filtered_set(clie
|
|||
assert len(payload["data"]) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /v2/model/info?blocked / ?sortBy=blocked
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def routing_status_router(monkeypatch):
|
||||
"""Router with one paused (blocked) deployment and two active ones."""
|
||||
model_list = [
|
||||
{
|
||||
"model_name": "paused-model",
|
||||
"litellm_params": {"model": "openai/paused-model"},
|
||||
"model_info": {"id": "paused-1", "db_model": True, "blocked": True},
|
||||
},
|
||||
{
|
||||
"model_name": "active-model",
|
||||
"litellm_params": {"model": "openai/active-model"},
|
||||
"model_info": {"id": "active-1", "db_model": True, "blocked": False},
|
||||
},
|
||||
{
|
||||
"model_name": "another-active",
|
||||
"litellm_params": {"model": "openai/another-active"},
|
||||
"model_info": {"id": "active-2", "db_model": True, "blocked": False},
|
||||
},
|
||||
]
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
router = MagicMock()
|
||||
router.model_list = model_list
|
||||
monkeypatch.setattr(proxy_server, "llm_router", router)
|
||||
monkeypatch.setattr(proxy_server, "llm_model_list", model_list)
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
|
||||
monkeypatch.setattr(proxy_server, "user_model", None)
|
||||
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
|
||||
monkeypatch.setattr(
|
||||
proxy_server,
|
||||
"_apply_search_filter_to_models",
|
||||
AsyncMock(side_effect=lambda all_models, **kw: (all_models, len(all_models))),
|
||||
)
|
||||
monkeypatch.setattr(proxy_server, "_enrich_model_info_with_litellm_data", lambda model, **kw: model)
|
||||
|
||||
import litellm.proxy.agent_endpoints.model_list_helpers as mlh
|
||||
|
||||
monkeypatch.setattr(mlh, "append_agents_to_model_info", AsyncMock(side_effect=lambda models, **kw: models))
|
||||
yield router
|
||||
|
||||
|
||||
def test_v2_model_info_blocked_filter_returns_only_paused(client, auth_as, routing_status_router):
|
||||
"""`?blocked=true` keeps just the paused deployments."""
|
||||
with auth_as():
|
||||
response = client.get("/v2/model/info", params={"blocked": "true"})
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert _model_names(payload) == ["paused-model"]
|
||||
assert payload["total_count"] == 1
|
||||
|
||||
|
||||
def test_v2_model_info_blocked_filter_returns_only_active(client, auth_as, routing_status_router):
|
||||
"""`?blocked=false` keeps just the active deployments."""
|
||||
with auth_as():
|
||||
response = client.get("/v2/model/info", params={"blocked": "false"})
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert _model_names(payload) == ["active-model", "another-active"]
|
||||
assert payload["total_count"] == 2
|
||||
|
||||
|
||||
def test_v2_model_info_sort_by_blocked_puts_active_first(client, auth_as, routing_status_router):
|
||||
"""`sortBy=blocked&sortOrder=asc` surfaces the active deployments first."""
|
||||
with auth_as():
|
||||
response = client.get("/v2/model/info", params={"sortBy": "blocked", "sortOrder": "asc"})
|
||||
assert response.status_code == 200
|
||||
assert _model_names(response.json()) == ["active-model", "another-active", "paused-model"]
|
||||
|
||||
|
||||
def test_v2_model_info_search_matches_litellm_model_name(client, auth_as, monkeypatch):
|
||||
"""`search` also hits the underlying LiteLLM model name (e.g. a provider
|
||||
prefix), not just the public model name."""
|
||||
model_list = [
|
||||
{
|
||||
"model_name": "paused-model",
|
||||
"litellm_params": {"model": "openai/paused-model"},
|
||||
"model_info": {"id": "paused-1", "db_model": True, "blocked": True},
|
||||
},
|
||||
{
|
||||
"model_name": "active-model",
|
||||
"litellm_params": {"model": "openai/active-model"},
|
||||
"model_info": {"id": "active-1", "db_model": True, "blocked": False},
|
||||
},
|
||||
]
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
router = MagicMock()
|
||||
router.model_list = model_list
|
||||
monkeypatch.setattr(proxy_server, "llm_router", router)
|
||||
monkeypatch.setattr(proxy_server, "llm_model_list", model_list)
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
|
||||
monkeypatch.setattr(proxy_server, "user_model", None)
|
||||
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
|
||||
|
||||
async def fake_fetch_db_models_for_search(**kwargs):
|
||||
return [], 0
|
||||
|
||||
monkeypatch.setattr(proxy_server, "_fetch_db_models_for_search", fake_fetch_db_models_for_search)
|
||||
monkeypatch.setattr(proxy_server, "_enrich_model_info_with_litellm_data", lambda model, **kw: model)
|
||||
|
||||
import litellm.proxy.agent_endpoints.model_list_helpers as mlh
|
||||
|
||||
monkeypatch.setattr(mlh, "append_agents_to_model_info", AsyncMock(side_effect=lambda models, **kw: models))
|
||||
|
||||
with auth_as():
|
||||
response = client.get("/v2/model/info", params={"search": "openai/paused"})
|
||||
assert response.status_code == 200
|
||||
assert _model_names(response.json()) == ["paused-model"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_info_v2_query_sentinel_does_not_filter(monkeypatch, mixed_auto_router_router):
|
||||
"""Called directly (not through FastAPI) the default arrives as a truthy Query object.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export const useModelsInfo = (
|
|||
modelName?: string,
|
||||
accessGroup?: string,
|
||||
wildcardOnly: boolean = false,
|
||||
blocked?: boolean,
|
||||
) => {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
return useQuery<PaginatedModelInfoResponse>({
|
||||
|
|
@ -62,6 +63,8 @@ export const useModelsInfo = (
|
|||
...(excludeAutoRouters && { excludeAutoRouters: "true" }),
|
||||
...(accessGroup && { accessGroup }),
|
||||
...(wildcardOnly && { wildcardOnly: "true" }),
|
||||
// `blocked !== undefined` (not truthiness): false is a meaningful filter value.
|
||||
...(blocked !== undefined && { blocked }),
|
||||
},
|
||||
}),
|
||||
queryFn: async () =>
|
||||
|
|
@ -80,6 +83,7 @@ export const useModelsInfo = (
|
|||
modelName,
|
||||
accessGroup,
|
||||
wildcardOnly,
|
||||
blocked,
|
||||
),
|
||||
enabled: Boolean(accessToken && userId && userRole),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
isModelTableSortColumnId,
|
||||
MODEL_NAME_COLUMN_ID,
|
||||
MODEL_TABLE_SORT_COLUMN_IDS,
|
||||
ROUTING_STATUS_COLUMN_ID,
|
||||
toServerSortField,
|
||||
} from "./ModelsTableColumns";
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ const TABLE_STATE = {
|
|||
view_mode: parseAsStringLiteral(MODEL_VIEW_MODES).withDefault("current_team"),
|
||||
filter_team: parseAsString.withDefault(PERSONAL_TEAM_VALUE),
|
||||
access_group: parseAsString.withDefault(""),
|
||||
status: parseAsString.withDefault(""),
|
||||
sort_by: parseAsStringLiteral(MODEL_TABLE_SORT_COLUMN_IDS),
|
||||
sort_order: parseAsStringLiteral(["asc", "desc"] as const).withDefault("asc"),
|
||||
page: boundedInteger(1, MAX_PAGE, 1),
|
||||
|
|
@ -88,6 +90,10 @@ const AllModelsTab = ({
|
|||
const modelViewMode = tableState.view_mode;
|
||||
const selectedTeamValue = tableState.filter_team;
|
||||
const selectedModelAccessGroupFilter = tableState.access_group || null;
|
||||
const routingStatusFilter =
|
||||
tableState.status === "active" || tableState.status === "paused" ? tableState.status : null;
|
||||
const blockedForQuery: boolean | undefined =
|
||||
routingStatusFilter === null ? undefined : routingStatusFilter === "active";
|
||||
const pagination = useMemo<PaginationState>(
|
||||
() => ({ pageIndex: tableState.page - 1, pageSize: tableState.page_size }),
|
||||
[tableState.page, tableState.page_size],
|
||||
|
|
@ -142,6 +148,7 @@ const AllModelsTab = ({
|
|||
modelNameForQuery,
|
||||
accessGroupForQuery,
|
||||
wildcardOnlyForQuery,
|
||||
blockedForQuery,
|
||||
);
|
||||
const isLoading = isLoadingModelsInfo || isLoadingModelCostMap;
|
||||
|
||||
|
|
@ -169,8 +176,9 @@ const AllModelsTab = ({
|
|||
? { id: MODEL_NAME_COLUMN_ID, value: selectedModelGroup }
|
||||
: null,
|
||||
selectedModelAccessGroupFilter ? { id: ACCESS_GROUPS_COLUMN_ID, value: selectedModelAccessGroupFilter } : null,
|
||||
routingStatusFilter ? { id: ROUTING_STATUS_COLUMN_ID, value: routingStatusFilter } : null,
|
||||
].filter((entry) => entry !== null),
|
||||
[selectedModelGroup, selectedModelAccessGroupFilter],
|
||||
[selectedModelGroup, selectedModelAccessGroupFilter, routingStatusFilter],
|
||||
);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
|
|
@ -184,8 +192,13 @@ const AllModelsTab = ({
|
|||
const next = functionalUpdate(updater, columnFilters);
|
||||
const modelGroup = next.find((entry) => entry.id === MODEL_NAME_COLUMN_ID)?.value;
|
||||
const accessGroup = next.find((entry) => entry.id === ACCESS_GROUPS_COLUMN_ID)?.value;
|
||||
const status = next.find((entry) => entry.id === ROUTING_STATUS_COLUMN_ID)?.value;
|
||||
setSelectedModelGroup(typeof modelGroup === "string" ? modelGroup : ALL_MODEL_GROUPS_VALUE);
|
||||
void setTableState({ access_group: typeof accessGroup === "string" ? accessGroup : null, page: null });
|
||||
void setTableState({
|
||||
access_group: typeof accessGroup === "string" ? accessGroup : null,
|
||||
status: typeof status === "string" ? status : null,
|
||||
page: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSortingChange: OnChangeFn<SortingState> = (updater) => {
|
||||
|
|
|
|||
|
|
@ -76,13 +76,14 @@ const row = (modelId: string): HTMLElement => {
|
|||
};
|
||||
|
||||
describe("AllModelsTable", () => {
|
||||
it("renders the nine design columns and hides Source behind the Columns menu", async () => {
|
||||
it("renders the ten design columns and hides Source behind the Columns menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AllModelsTable {...baseProps} />);
|
||||
|
||||
for (const header of [
|
||||
"Model ID",
|
||||
"Model Information",
|
||||
"Status",
|
||||
"Credentials",
|
||||
"Created By",
|
||||
"Updated At",
|
||||
|
|
@ -95,17 +96,30 @@ describe("AllModelsTable", () => {
|
|||
}
|
||||
|
||||
expect(screen.queryByRole("columnheader", { name: /^source$/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("columnheader", { name: /^status$/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("DB Model")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /columns/i }));
|
||||
expect(screen.queryByRole("menuitemcheckbox", { name: /status/i })).not.toBeInTheDocument();
|
||||
await user.click(await screen.findByRole("menuitemcheckbox", { name: /^source$/i }));
|
||||
|
||||
expect(await screen.findByRole("columnheader", { name: /^source$/i })).toBeInTheDocument();
|
||||
expect(await screen.findByText("DB Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reports the routing status picked in the Filters drawer", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onColumnFiltersChange = vi.fn();
|
||||
render(<AllModelsTable {...baseProps} onColumnFiltersChange={onColumnFiltersChange} />);
|
||||
|
||||
await user.click(screen.getByTestId("datatable-filters-trigger"));
|
||||
await user.click(screen.getByRole("combobox", { name: /filter by status/i }));
|
||||
await user.click(await screen.findByRole("option", { name: "Paused" }));
|
||||
await user.click(screen.getByTestId("filter-drawer-apply"));
|
||||
|
||||
const updater = onColumnFiltersChange.mock.calls.at(-1)?.[0];
|
||||
const next = typeof updater === "function" ? updater([]) : updater;
|
||||
expect(next).toEqual([{ id: "model_info_blocked", value: "paused" }]);
|
||||
});
|
||||
|
||||
it("opens the model detail from the model ID cell", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onModelIdClick = vi.fn();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
ACCESS_GROUPS_COLUMN_ID,
|
||||
getModelsTableColumns,
|
||||
MODEL_NAME_COLUMN_ID,
|
||||
ROUTING_STATUS_COLUMN_ID,
|
||||
STATUS_COLUMN_ID,
|
||||
} from "./ModelsTableColumns";
|
||||
|
||||
|
|
@ -36,8 +37,15 @@ const ALL_PROXY_MODELS_LABEL = "All Proxy Models";
|
|||
const FILTER_LABELS: Record<string, string> = {
|
||||
[MODEL_NAME_COLUMN_ID]: "Public Model Name",
|
||||
[ACCESS_GROUPS_COLUMN_ID]: "Model Access Group",
|
||||
[ROUTING_STATUS_COLUMN_ID]: "Status",
|
||||
};
|
||||
|
||||
const ROUTING_STATUS_FILTER_VALUES = {
|
||||
all: "All Statuses",
|
||||
active: "Active",
|
||||
paused: "Paused",
|
||||
} as const;
|
||||
|
||||
const VIEW_MODE_LABELS: Record<ModelViewMode, string> = {
|
||||
current_team: "Current Team Models",
|
||||
all: ALL_PROXY_MODELS_LABEL,
|
||||
|
|
@ -167,6 +175,9 @@ export function AllModelsTable({
|
|||
if (columnId === MODEL_NAME_COLUMN_ID && raw === WILDCARD_MODEL_GROUP_VALUE) {
|
||||
return "Wildcard Models (*)";
|
||||
}
|
||||
if (columnId === ROUTING_STATUS_COLUMN_ID) {
|
||||
return ROUTING_STATUS_FILTER_VALUES[raw as keyof typeof ROUTING_STATUS_FILTER_VALUES] ?? raw;
|
||||
}
|
||||
return raw;
|
||||
};
|
||||
|
||||
|
|
@ -288,6 +299,29 @@ export function AllModelsTable({
|
|||
emptyText="No models found"
|
||||
/>
|
||||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Status">
|
||||
<Select
|
||||
value={(get(ROUTING_STATUS_COLUMN_ID) as string) ?? "all"}
|
||||
onValueChange={(value) =>
|
||||
set(ROUTING_STATUS_COLUMN_ID, value === "all" ? undefined : (value as "active" | "paused"))
|
||||
}
|
||||
>
|
||||
<SelectTrigger aria-label="Filter by status" className="w-full">
|
||||
{ROUTING_STATUS_FILTER_VALUES[
|
||||
(get(ROUTING_STATUS_COLUMN_ID) as keyof typeof ROUTING_STATUS_FILTER_VALUES) ?? "all"
|
||||
] ?? ROUTING_STATUS_FILTER_VALUES.all}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(ROUTING_STATUS_FILTER_VALUES) as Array<keyof typeof ROUTING_STATUS_FILTER_VALUES>).map(
|
||||
(value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{ROUTING_STATUS_FILTER_VALUES[value]}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Model Access Group">
|
||||
<SearchSelect
|
||||
options={accessGroupOptions}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const COSTS_COLUMN_ID = "input_cost";
|
|||
export const TEAM_ID_COLUMN_ID = "model_info_team_id";
|
||||
export const ACCESS_GROUPS_COLUMN_ID = "model_info_access_groups";
|
||||
export const STATUS_COLUMN_ID = "model_info_db_model";
|
||||
export const ROUTING_STATUS_COLUMN_ID = "model_info_blocked";
|
||||
|
||||
export const MODEL_TABLE_SORT_COLUMN_IDS = [
|
||||
MODEL_NAME_COLUMN_ID,
|
||||
|
|
@ -30,6 +31,7 @@ export const MODEL_TABLE_SORT_COLUMN_IDS = [
|
|||
UPDATED_AT_COLUMN_ID,
|
||||
COSTS_COLUMN_ID,
|
||||
STATUS_COLUMN_ID,
|
||||
ROUTING_STATUS_COLUMN_ID,
|
||||
] as const;
|
||||
|
||||
export type ModelTableSortColumnId = (typeof MODEL_TABLE_SORT_COLUMN_IDS)[number];
|
||||
|
|
@ -42,6 +44,7 @@ const COLUMN_ID_TO_SERVER_SORT_FIELD: Record<string, string> = {
|
|||
[STATUS_COLUMN_ID]: "status",
|
||||
[CREATED_BY_COLUMN_ID]: "created_at",
|
||||
[UPDATED_AT_COLUMN_ID]: "updated_at",
|
||||
[ROUTING_STATUS_COLUMN_ID]: "blocked",
|
||||
};
|
||||
|
||||
export const toServerSortField = (columnId: string): string => COLUMN_ID_TO_SERVER_SORT_FIELD[columnId] ?? columnId;
|
||||
|
|
@ -259,6 +262,14 @@ function AccessGroupsCell({ accessGroups }: { accessGroups: string[] | null }) {
|
|||
);
|
||||
}
|
||||
|
||||
function RoutingStatusCell({ blocked }: { blocked: boolean | null | undefined }) {
|
||||
return blocked === true ? (
|
||||
<StatusBadge tone="neutral" label="Paused" />
|
||||
) : (
|
||||
<StatusBadge tone="success" label="Active" />
|
||||
);
|
||||
}
|
||||
|
||||
interface ModelRowActionsProps {
|
||||
model: ModelData;
|
||||
userRole: string;
|
||||
|
|
@ -444,6 +455,16 @@ export const getModelsTableColumns = ({
|
|||
minSize: 90,
|
||||
cell: ({ row }) => <CostsCell model={row.original} />,
|
||||
},
|
||||
{
|
||||
id: ROUTING_STATUS_COLUMN_ID,
|
||||
accessorFn: (row) => row.model_info?.blocked === true,
|
||||
meta: { title: "Status", skeleton: "badge" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Status" />,
|
||||
enableSorting: true,
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
cell: ({ row }) => <RoutingStatusCell blocked={row.original.model_info?.blocked} />,
|
||||
},
|
||||
{
|
||||
id: TEAM_ID_COLUMN_ID,
|
||||
accessorFn: (row) => row.model_info.team_id ?? "",
|
||||
|
|
|
|||
|
|
@ -1669,6 +1669,7 @@ export const modelInfoCall = async (
|
|||
modelName?: string,
|
||||
accessGroup?: string,
|
||||
wildcardOnly?: boolean,
|
||||
blocked?: boolean,
|
||||
) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
|
|
@ -1706,6 +1707,9 @@ export const modelInfoCall = async (
|
|||
if (wildcardOnly) {
|
||||
params.append("wildcard_only", "true");
|
||||
}
|
||||
if (blocked !== undefined) {
|
||||
params.append("blocked", blocked ? "true" : "false");
|
||||
}
|
||||
if (params.toString()) {
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue