fix(ui): address router usage review and test failures

This commit is contained in:
moe-berri 2026-09-24 18:06:06 -07:00
parent 70a87c03aa
commit a55a19190e
13 changed files with 297 additions and 124 deletions

View file

@ -148,6 +148,7 @@ jobs:
test-path: >-
tests/test_litellm/proxy/analytics_endpoints
tests/test_litellm/proxy/management_endpoints
tests/unit/proxy/management_endpoints/test_auto_router_usage.py
tests/test_litellm/proxy/list_api
tests/test_litellm/proxy/memory
tests/test_litellm/proxy/guardrails

View file

@ -60,6 +60,7 @@ ORDER BY spend DESC, model, router_name, tier
"""
_USAGE_ROWS: Final = TypeAdapter(tuple[AutoRouterUsage, ...])
MAX_ROUTING_USAGE_DAYS: Final = 93
@router.get(
@ -80,13 +81,16 @@ async def get_auto_router_usage(
) -> tuple[AutoRouterUsage, ...]:
"""Requests and destination-model spend from retained logs in inclusive UTC days.
Select one model or one router. Internal classifier and shadow-evaluation calls
Select one model or one router over at most 93 inclusive UTC days.
Internal classifier and shadow-evaluation calls
are excluded. Non-admins can only see requests attributed to their own user.
"""
if (destination_model is None) == (router_name is None):
raise HTTPException(status_code=400, detail="Select exactly one model or router")
if end_date < start_date or end_date == date.max:
raise HTTPException(status_code=400, detail="Invalid date range")
if (end_date - start_date).days >= MAX_ROUTING_USAGE_DAYS:
raise HTTPException(status_code=400, detail=f"Select a range of {MAX_ROUTING_USAGE_DAYS} days or fewer")
if router_type is not None and router_name is None:
raise HTTPException(status_code=400, detail="router_type requires router_name")
scoped_user: Final = (

View file

@ -0,0 +1,125 @@
import json
from collections.abc import Mapping
from typing import Final
from uuid import uuid4
import pytest
from httpx import AsyncClient
from litellm.proxy.utils import PrismaClient
from .actors import Actor, World
from .conftest import MASTER_KEY
pytestmark: Final = pytest.mark.asyncio(loop_scope="session")
async def test_actual_model_spend_sources_tiers_and_scope(
proxy_client: AsyncClient, prisma: PrismaClient, world: World
) -> None:
db: Final = prisma.db
caller: Final = world.keys[Actor.INTERNAL_USER]
owner: Final = caller.user_id
key: Final = f"routing-usage-{uuid4()}"
records: Final = (
("fast", 1.0, "a", "SIMPLE", None, owner, "2026-01-01T00:00:00"),
("strong", 5.0, "a", "SIMPLE", None, owner, "2026-01-01T12:00:00"),
("fast", 0.5, "a", None, None, owner, "2026-01-01T12:00:00"),
("fast", 2.0, "b", "COMPLEX", None, owner, "2026-01-01T12:00:00"),
("fast", 3.0, None, None, None, owner, "2026-01-01T12:00:00"),
("fast", 90.0, "a", "SIMPLE", "shadow_eval_router", owner, "2026-01-01T12:00:00"),
("fast", 80.0, None, None, "autorouter_classifier", owner, "2026-01-01T12:00:00"),
("fast", 70.0, "a", "SIMPLE", None, "other-user", "2026-01-01T12:00:00"),
("fast", 60.0, "a", "SIMPLE", None, owner, "2026-01-02T00:00:00"),
)
try:
for index, (model, spend, router, tier, origin, user, at) in enumerate(records):
metadata: Final = json.dumps(
{
"routing_decision": {"router_model_name": router, "router_type": "complexity", "tier": tier}
if router
else None,
"internal_call_origin": origin,
}
)
await db.execute_raw(
'INSERT INTO "LiteLLM_SpendLogs" (request_id, call_type, api_key, model, model_group, spend, '
'"user", "startTime", "endTime", metadata) '
"VALUES ($1, 'completion', $2, $3, 'renamed-alias', $4, $5, $6::timestamp, $6::timestamp, $7::jsonb)",
f"{key}-{index}",
key,
model,
spend,
user,
at,
metadata,
)
query: Final = {"start_date": "2026-01-01", "end_date": "2026-01-01", "api_key": key}
headers: Final = {"Authorization": f"Bearer {caller.cleartext}"}
model_usage: Final = await proxy_client.get(
"/auto_router/usage", params={**query, "destination_model": "fast"}, headers=headers
)
assert model_usage.status_code == 200, model_usage.text
assert model_usage.json() == [
{"model": "fast", "router_name": router, "router_type": router_type, "tier": tier, "requests": 1, "spend": spend}
for router, router_type, tier, spend in (
(None, None, None, 3.0),
("b", "complexity", "COMPLEX", 2.0),
("a", "complexity", "SIMPLE", 1.0),
("a", "complexity", None, 0.5),
)
]
router_usage: Final = await proxy_client.get(
"/auto_router/usage", params={**query, "router_name": "a", "router_type": "complexity"}, headers=headers
)
assert router_usage.status_code == 200, router_usage.text
assert router_usage.json() == [
{"model": model, "router_name": "a", "router_type": "complexity", "tier": tier, "requests": 1, "spend": spend}
for model, tier, spend in (("strong", "SIMPLE", 5.0), ("fast", "SIMPLE", 1.0), ("fast", None, 0.5))
]
empty: Final = await proxy_client.get(
"/auto_router/usage",
params={**query, "destination_model": "fast", "api_key": f"{key}-other"},
headers=headers,
)
assert empty.status_code == 200, empty.text
assert empty.json() == []
finally:
await db.execute_raw('DELETE FROM "LiteLLM_SpendLogs" WHERE api_key = $1', key)
@pytest.mark.parametrize(
"params,status",
(
({}, 400),
({"destination_model": "fast", "router_name": "a"}, 400),
({"destination_model": "fast", "router_type": "complexity"}, 400),
({"destination_model": "fast", "end_date": "2025-12-31"}, 400),
({"destination_model": "fast", "end_date": "9999-12-31"}, 400),
({"destination_model": "fast", "end_date": "2026-04-03"}, 200),
({"destination_model": "fast", "end_date": "2026-04-04"}, 400),
({"destination_model": "fast", "start_date": "0001-01-01"}, 400),
({"destination_model": ""}, 422),
({"destination_model": "fast", "start_date": "not-a-date"}, 422),
),
)
async def test_routing_usage_rejects_invalid_filters_and_bounds_inclusive_days(
proxy_client: AsyncClient, params: Mapping[str, str], status: int
) -> None:
response: Final = await proxy_client.get(
"/auto_router/usage",
params={"start_date": "2026-01-01", "end_date": "2026-01-01", **params},
headers={"Authorization": f"Bearer {MASTER_KEY}"},
)
assert response.status_code == status, response.text
async def test_routing_usage_cannot_query_another_user(proxy_client: AsyncClient, world: World) -> None:
caller: Final = world.keys[Actor.INTERNAL_USER]
response: Final = await proxy_client.get(
"/auto_router/usage",
params={"start_date": "2026-01-01", "end_date": "2026-01-01", "destination_model": "fast", "user_id": "other"},
headers={"Authorization": f"Bearer {caller.cleartext}"},
)
assert response.status_code == 403, response.text

View file

@ -1,90 +0,0 @@
import json
from datetime import date
from typing import Final
from uuid import uuid4
import pytest
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.management_endpoints.auto_router_usage import get_auto_router_usage
pytestmark = pytest.mark.asyncio(loop_scope="session")
async def test_actual_model_spend_sources_tiers_and_scope(db):
key: Final = f"routing-usage-{uuid4()}"
records: Final = (
("fast", 1.0, "a", "SIMPLE", None, "owner", "2026-01-01T00:00:00"),
("strong", 5.0, "a", "SIMPLE", None, "owner", "2026-01-01T12:00:00"),
("fast", 0.5, "a", None, None, "owner", "2026-01-01T12:00:00"),
("fast", 2.0, "b", "COMPLEX", None, "owner", "2026-01-01T12:00:00"),
("fast", 3.0, None, None, None, "owner", "2026-01-01T12:00:00"),
("fast", 90.0, "a", "SIMPLE", "shadow_eval_router", "owner", "2026-01-01T12:00:00"),
("fast", 80.0, None, None, "autorouter_classifier", "owner", "2026-01-01T12:00:00"),
("fast", 70.0, "a", "SIMPLE", None, "other-user", "2026-01-01T12:00:00"),
("fast", 60.0, "a", "SIMPLE", None, "owner", "2026-01-02T00:00:00"),
)
try:
for index, (model, spend, router, tier, origin, user, at) in enumerate(records):
metadata: Final = json.dumps(
{
"routing_decision": {"router_model_name": router, "router_type": "complexity", "tier": tier}
if router
else None,
"internal_call_origin": origin,
}
)
await db.execute_raw(
'INSERT INTO "LiteLLM_SpendLogs" (request_id, call_type, api_key, model, model_group, spend, '
'"user", "startTime", "endTime", metadata) '
"VALUES ($1, 'completion', $2, $3, 'renamed-alias', $4, $5, $6::timestamp, $6::timestamp, $7::jsonb)",
f"{key}-{index}",
key,
model,
spend,
user,
at,
metadata,
)
caller: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, user_id="owner")
model_usage: Final = await get_auto_router_usage(
date(2026, 1, 1),
date(2026, 1, 1),
caller,
db,
destination_model="fast",
api_key=key,
)
assert {(row.router_name, row.tier, row.requests, row.spend) for row in model_usage} == {
(None, None, 1, 3.0),
("b", "COMPLEX", 1, 2.0),
("a", "SIMPLE", 1, 1.0),
("a", None, 1, 0.5),
}
router_usage: Final = await get_auto_router_usage(
date(2026, 1, 1),
date(2026, 1, 1),
caller,
db,
router_name="a",
router_type="complexity",
api_key=key,
)
assert {(row.model, row.tier, row.requests, row.spend) for row in router_usage} == {
("fast", "SIMPLE", 1, 1.0),
("strong", "SIMPLE", 1, 5.0),
("fast", None, 1, 0.5),
}
assert (
await get_auto_router_usage(
date(2026, 1, 1),
date(2026, 1, 1),
caller,
db,
destination_model="fast",
api_key=f"{key}-other",
)
== ()
)
finally:
await db.execute_raw('DELETE FROM "LiteLLM_SpendLogs" WHERE api_key = $1', key)

View file

@ -1,4 +1,5 @@
from datetime import date
from typing import Final
from unittest.mock import AsyncMock
import pytest
@ -91,3 +92,37 @@ async def test_query_requires_one_specific_model_or_router(model: str | None, ro
)
assert error.value.status_code == 400
db.query_raw.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.parametrize("end", [date(2025, 12, 31), date.max, date(2026, 4, 4)])
async def test_invalid_or_overlong_ranges_never_query_spend_logs(end: date) -> None:
db: Final = Database()
with pytest.raises(HTTPException) as error:
await get_auto_router_usage(
date(2026, 1, 1), end, UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), db, destination_model="fast"
)
assert error.value.status_code == 400
db.query_raw.assert_not_called()
@pytest.mark.asyncio
async def test_maximum_range_includes_its_last_day() -> None:
db: Final = Database()
await get_auto_router_usage(
date(2026, 1, 1), date(2026, 4, 3), UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), db,
destination_model="fast",
)
assert db.query_raw.call_args.args[1:3] == ("2026-01-01T00:00:00", "2026-04-04T00:00:00")
@pytest.mark.asyncio
async def test_router_type_cannot_filter_a_destination_model() -> None:
db: Final = Database()
with pytest.raises(HTTPException) as error:
await get_auto_router_usage(
date(2026, 1, 1), date(2026, 1, 1), UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), db,
destination_model="fast", router_type="complexity",
)
assert error.value.status_code == 400
db.query_raw.assert_not_called()

View file

@ -1,6 +1,3 @@
vi.mock("@/components/UsagePage/useRoutingUsage", () => ({
useRoutingUsage: () => ({ data: undefined, isError: false }),
}));
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, within } from "@testing-library/react";
import React from "react";
@ -36,6 +33,10 @@ import type {
} from "./autoRouterBenchmarks";
import { useAutoRouterBenchmarks } from "./useAutoRouterBenchmarks";
vi.mock("@/components/UsagePage/useRoutingUsage", () => ({
useRoutingUsage: () => ({ data: undefined, error: null }),
}));
type HookResult = ReturnType<typeof useAutoRouterBenchmarks>;
const mockAutoRouters = (deployments: AutoRouterDeployment[] = []) => {

View file

@ -277,7 +277,7 @@ const CachingCard: React.FC<{ cache: AutoRouterCacheStats }> = ({ cache }) => {
interface BenchmarksBodyProps {
usage?: readonly RoutingUsage[];
usageUnavailable?: boolean;
usageError?: unknown;
isPending: boolean;
error: unknown;
data: AutoRouterBenchmarksResponse | undefined;
@ -292,7 +292,7 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({
selectedKey,
autoRouters,
usage,
usageUnavailable,
usageError,
}) => {
if (isPending) return <Message>Loading auto-router usage...</Message>;
if (error instanceof ApiError && error.status === 403) {
@ -306,7 +306,7 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({
<>
<HeroCard view={view} />
<TierTurnsChart view={view} autoRouters={autoRouters} usage={usage} usageUnavailable={usageUnavailable} />
<TierTurnsChart view={view} autoRouters={autoRouters} usage={usage} usageError={usageError} />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Metric
@ -414,7 +414,7 @@ export const AutoRouterUsageView: React.FC<AutoRouterBenchmarksTabProps> = ({
data={data}
selectedKey={selectedKey}
usage={usage.data}
usageUnavailable={usage.isError}
usageError={usage.error}
autoRouters={autoRouters ?? []}
/>
</div>

View file

@ -3,6 +3,7 @@ import React from "react";
import { describe, expect, it, vi } from "vitest";
import type { AutoRouterDeployment } from "@/app/(dashboard)/hooks/models/useModels";
import { ApiError } from "@/lib/http/client";
vi.mock("@/components/shared/charts", () => ({
DonutChart: ({ label }: { label: string }) => <div data-testid="donut">{label}</div>,
@ -205,3 +206,57 @@ it("switches the existing donut to actual spend and shows each destination model
expect(screen.getByText("Simple 60% · $6.00")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Spend" })).toHaveAttribute("aria-pressed", "true");
});
it("keeps configured aliases and unused tiers visible separately from recorded destinations", () => {
render(
<TierTurnsChart
view={groupView()}
autoRouters={[deployment({ tiers: { SIMPLE: ["public-fast", "unused"], COMPLEX: "public-strong" } })]}
usage={[
{
model: "provider/fast",
router_name: "claude-auto",
router_type: "complexity",
tier: "SIMPLE",
requests: 3,
spend: 1,
},
]}
/>,
);
expect(screen.getByText("Configured: public-fast, unused")).toBeVisible();
expect(screen.getByText("provider/fast · 3 requests (100%) · $1.00")).toBeVisible();
expect(screen.getByText("Configured: public-strong")).toBeVisible();
expect(screen.getByText("No retained requests for this tier")).toBeVisible();
expect(screen.getByTestId("donut")).toHaveTextContent("3 total requests");
});
it("keeps traffic visible when requests have no spend, including after changing the range in Spend mode", () => {
const usage = [
{ model: "fast", router_name: "claude-auto", router_type: "complexity", tier: "SIMPLE", requests: 3, spend: 1 },
{ model: "strong", router_name: "claude-auto", router_type: "complexity", tier: "COMPLEX", requests: 1, spend: 2 },
];
const { rerender } = render(<TierTurnsChart view={groupView()} autoRouters={[]} usage={usage} />);
fireEvent.click(screen.getByRole("button", { name: "Spend" }));
expect(screen.getByTestId("donut")).toHaveTextContent("$3.00");
rerender(<TierTurnsChart view={groupView()} autoRouters={[]} usage={usage.map((row) => ({ ...row, spend: 0 }))} />);
expect(screen.getByRole("button", { name: "Spend" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Traffic" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText(/No model spend recorded; showing traffic/)).toBeVisible();
expect(screen.getByText("Simple 75% · $0.00")).toBeVisible();
expect(screen.getByText("Complex 25% · $0.00")).toBeVisible();
expect(screen.getByTestId("donut")).toHaveTextContent("4 total requests");
});
it("explains the date limit while retaining the existing tier traffic chart", () => {
render(
<TierTurnsChart
view={groupView()}
autoRouters={[]}
usageError={new ApiError("Select a range of 93 days or fewer", 400, {})}
/>,
);
expect(screen.getByText(/Select a range of 93 days or fewer/)).toBeVisible();
expect(screen.getByTestId("donut")).toHaveTextContent("4 total turns");
expect(screen.queryByRole("button", { name: "Spend" })).not.toBeInTheDocument();
});

View file

@ -14,6 +14,7 @@ import {
import { normalizeTierModels } from "@/components/add_model/complexity_router_tiers";
import { chartColorValue, DEFAULT_COLOR_CYCLE, DonutChart } from "@/components/shared/charts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ApiError } from "@/lib/http/client";
import { viewGroup, type BenchmarkView } from "./autoRouterBenchmarks";
@ -66,27 +67,25 @@ const tierLabelsFor = (
return hydrateTierLabels(config.tier_labels);
};
const tierModelsFor = (
tier: string,
const configuredTiersFor = (
routerName: string,
routerType: string,
autoRouters: readonly AutoRouterDeployment[],
): string[] => {
): Record<string, unknown> => {
const deployment = deploymentFor(routerName, routerType, autoRouters);
if (!deployment) return [];
if (!deployment) return {};
const config = asRecord(deployment.litellm_params?.complexity_router_config);
const tiers = asRecord(config.tiers);
return normalizeTierModels(tiers[tier]);
return asRecord(config.tiers);
};
interface TierTurnsChartProps {
usage?: readonly RoutingUsage[];
usageUnavailable?: boolean;
usageError?: unknown;
view: BenchmarkView;
autoRouters: readonly AutoRouterDeployment[];
}
const TierTurnsChart: React.FC<TierTurnsChartProps> = ({ view, autoRouters, usage, usageUnavailable }) => {
const TierTurnsChart: React.FC<TierTurnsChartProps> = ({ view, autoRouters, usage, usageError }) => {
const [mode, setMode] = useState<"turns" | "spend">("turns");
const group = viewGroup(view);
const entries = Object.entries(group?.tier_turns ?? {}).filter(([, turns]) => turns > 0);
@ -94,23 +93,30 @@ const TierTurnsChart: React.FC<TierTurnsChartProps> = ({ view, autoRouters, usag
const tierLabels = tierLabelsFor(group.router_name, group.router_type, autoRouters);
const measured = usage !== undefined && usage.length > 0;
const spendUnavailable = !measured && (usageUnavailable || usage !== undefined);
const spendUnavailable = !measured && (Boolean(usageError) || usage !== undefined);
const configuredTiers = configuredTiersFor(group.router_name, group.router_type, autoRouters);
const recordedTiers = routingTiers(usage ?? []);
const tiers = [...new Set([...recordedTiers.map((row) => row.tier), ...Object.keys(configuredTiers)])];
const slices = measured
? routingTiers(usage).map((row) => ({
tier: tierDisplayLabel(row.tier ?? "Default / no tier", tierLabels),
turns: row.requests,
spend: row.spend,
models: row.models.map((model) => model.model),
modelUsage: row.models,
}))
? tiers.map((tier) => {
const recorded = recordedTiers.find((row) => row.tier === tier);
return {
tier: tierDisplayLabel(tier ?? "Default / no tier", tierLabels),
turns: recorded?.requests ?? 0,
spend: recorded?.spend ?? 0,
models: tier === null ? [] : normalizeTierModels(configuredTiers[tier]),
modelUsage: recorded?.models ?? [],
};
})
: entries.map(([tier, turns]) => ({
tier: tierDisplayLabel(tier, tierLabels),
turns,
spend: 0,
models: tierModelsFor(tier, group.router_name, group.router_type, autoRouters),
models: normalizeTierModels(configuredTiers[tier]),
modelUsage: [],
}));
const category = measured ? mode : "turns";
const hasSpend = measured && slices.some((slice) => slice.spend > 0);
const category = hasSpend ? mode : "turns";
const total = slices.reduce((sum, slice) => sum + slice[category], 0);
const requests = slices.reduce((sum, slice) => sum + slice.turns, 0);
const colors = slices.map((_, idx) => DEFAULT_COLOR_CYCLE[idx % DEFAULT_COLOR_CYCLE.length]);
@ -127,8 +133,9 @@ const TierTurnsChart: React.FC<TierTurnsChartProps> = ({ view, autoRouters, usag
key={value}
type="button"
aria-pressed={category === value}
disabled={value === "spend" && !hasSpend}
onClick={() => setMode(value)}
className={`rounded-md px-3 py-1 text-sm transition-colors ${category === value ? "bg-card text-foreground shadow-xs" : "text-muted-foreground hover:text-foreground"}`}
className={`rounded-md px-3 py-1 text-sm transition-colors disabled:opacity-50 ${category === value ? "bg-card text-foreground shadow-xs" : "text-muted-foreground hover:text-foreground"}`}
>
{value === "turns" ? "Traffic" : "Spend"}
</button>
@ -140,7 +147,11 @@ const TierTurnsChart: React.FC<TierTurnsChartProps> = ({ view, autoRouters, usag
{measured
? "Requests and model spend from retained logs in this period. Excludes classifier and shadow-evaluation calls."
: "Turns each tier served. Turns the classifier sent to the default model belong to no tier and are not counted here, so this can total less than the router's turns."}
{spendUnavailable && " Model spend is unavailable for this range."}
{spendUnavailable &&
(usageError instanceof ApiError && usageError.status === 400
? ` ${usageError.message}.`
: " Model spend is unavailable for this range.")}
{measured && !hasSpend && " No model spend recorded; showing traffic."}
</p>
</CardHeader>
<CardContent>
@ -171,11 +182,18 @@ const TierTurnsChart: React.FC<TierTurnsChartProps> = ({ view, autoRouters, usag
{slice.tier} {trafficShare(slice[category], total)}
{measured && ` · ${routingSpend(slice.spend)}`}
</p>
{measured && slice.models.length > 0 && (
<p className="text-xs break-words text-muted-foreground">Configured: {slice.models.join(", ")}</p>
)}
{measured && slice.modelUsage.length === 0 && (
<p className="text-xs text-muted-foreground">No retained requests for this tier</p>
)}
{measured
? slice.modelUsage.map((model) => (
<p key={model.model} className="text-xs break-words text-muted-foreground">
{model.model} · {model.requests.toLocaleString()} {model.requests === 1 ? "request" : "requests"} (
{trafficShare(model.requests, requests)}) · {routingSpend(model.spend)}
{model.model} · {model.requests.toLocaleString()}{" "}
{model.requests === 1 ? "request" : "requests"} ({trafficShare(model.requests, requests)}) ·{" "}
{routingSpend(model.spend)}
</p>
))
: slice.models.length > 0 && (

View file

@ -24,7 +24,8 @@ beforeAll(() => {
});
// Mock the networking module
vi.mock("@/components/networking", () => ({
vi.mock("@/components/networking", async (importOriginal) => ({
formatDate: (await importOriginal<typeof import("@/components/networking")>()).formatDate,
userDailyActivityCall: vi.fn(),
userDailyActivityAggregatedCall: vi.fn(),
gatewayDailyActivityCall: vi.fn(),

View file

@ -12,6 +12,22 @@ const rows = [
afterEach(() => vi.restoreAllMocks());
describe("model traffic sources", () => {
it("explains how to recover from a date range rejected by the gateway", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ detail: "Select a range of 93 days or fewer" }), {
status: 400,
headers: { "Content-Type": "application/json" },
}),
);
render(
<QueryClientProvider client={new QueryClient()}>
<ModelRoutingShare model="fast" scope={scope} />
</QueryClientProvider>,
);
expect(await screen.findByText("Select a range of 93 days or fewer")).toBeVisible();
expect(screen.queryByRole("button", { name: /via auto-router/ })).not.toBeInTheDocument();
});
it("loads the selected model and user, then opens direct and router request/spend figures", async () => {
const fetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(rows), {

View file

@ -2,11 +2,17 @@ import { useRoutingUsage } from "../useRoutingUsage";
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { routingSources, routingSpend, trafficShare, type RoutingUsageScope } from "../routingUsage";
import { ApiError } from "@/lib/http/client";
export default function ModelRoutingShare({ model, scope }: { model: string; scope: RoutingUsageScope }) {
const { data, isPending, isError } = useRoutingUsage({ ...scope, destination_model: model });
const { data, isPending, isError, error } = useRoutingUsage({ ...scope, destination_model: model });
if (isPending) return <p className="text-xs text-muted-foreground">Loading traffic sources...</p>;
if (isError) return <p className="text-xs text-muted-foreground">Traffic sources unavailable</p>;
if (isError)
return (
<p className="text-xs text-muted-foreground">
{error instanceof ApiError && error.status === 400 ? error.message : "Traffic sources unavailable"}
</p>
);
const sources = routingSources(data ?? []);
const total = sources.reduce((sum, source) => sum + source.requests, 0);
if (total === 0) return <p className="text-xs text-muted-foreground">No retained requests for traffic sources</p>;

View file

@ -1504,7 +1504,8 @@ export interface paths {
* Get Auto Router Usage
* @description Requests and destination-model spend from retained logs in inclusive UTC days.
*
* Select one model or one router. Internal classifier and shadow-evaluation calls
* Select one model or one router over at most 93 inclusive UTC days.
* Internal classifier and shadow-evaluation calls
* are excluded. Non-admins can only see requests attributed to their own user.
*/
get: operations["get_auto_router_usage_auto_router_usage_get"];