mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy-ui): PR review — key models API clarity and RQ cache keys
- model_checks: simplify empty_models; rename resolved_total_count to resolved_config_entry_count; document pattern vs matched counts - key_management: OpenAPI doc bullets for new response fields - useGetKeyModels: include userId ?? accessToken in query keys - networking: drop no-op try/catch in fetchKeyModelCall; update KeyModelResponse - Tests: mocks/assertions for renamed field Made-with: Cursor
This commit is contained in:
parent
0a8341fd01
commit
32f31889a9
9 changed files with 55 additions and 49 deletions
|
|
@ -534,7 +534,8 @@ def build_key_resolved_model_display_sections(
|
|||
only to avoid repeating the full flat list as "Other models".
|
||||
"""
|
||||
sections: List[KeyResolvedModelDisplaySection] = []
|
||||
empty_models: List[str] = [] if compact else []
|
||||
# Always [] — compact still toggles each section's `models=` via `empty_models if compact else ...` below.
|
||||
empty_models: List[str] = []
|
||||
|
||||
display_set = set(display_models)
|
||||
|
||||
|
|
@ -603,6 +604,9 @@ def prepare_key_models_response_payload(
|
|||
|
||||
Access-group sections list concrete model names: wildcards in router group metadata
|
||||
are expanded against models allowed for this key (resolved ∩ router names).
|
||||
|
||||
``resolved_config_entry_count`` is len(resolved) (pattern/sentinel entries); ``matched_count`` is
|
||||
the number of concrete router names after expansion and search; they differ when wildcards are used.
|
||||
"""
|
||||
base_concrete = _concrete_models_allowed_by_resolved(resolved, all_router_model_names)
|
||||
|
||||
|
|
@ -638,7 +642,7 @@ def prepare_key_models_response_payload(
|
|||
return {
|
||||
"model_display_sections": model_display_sections,
|
||||
"source": source,
|
||||
"resolved_total_count": len(resolved),
|
||||
"resolved_config_entry_count": len(resolved),
|
||||
"matched_count": matched_count,
|
||||
"models_truncated": models_truncated,
|
||||
"all_team_models_without_team": all_team_models_without_team,
|
||||
|
|
|
|||
|
|
@ -2828,6 +2828,8 @@ async def key_resolved_models_fn(
|
|||
- **search**: Filters the resolved list before truncation and sectioning.
|
||||
- **compact**: Metadata-only payload (empty `models` arrays in each section) for fast initial load.
|
||||
- **all_team_models_without_team**: True when the key uses `all-team-models` but has no `team_id` (assign a team in key settings).
|
||||
- **resolved_config_entry_count**: Length of the post-sentinel-resolution pattern list (not the same as concrete `matched_count` when wildcards are present).
|
||||
- **matched_count**: Number of concrete router model names after expansion and optional search (before display truncation).
|
||||
"""
|
||||
from litellm.proxy.proxy_server import llm_router, prisma_client
|
||||
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ def test_prepare_payload_search_and_truncation():
|
|||
compact=False,
|
||||
all_router_model_names=big,
|
||||
)
|
||||
assert out["resolved_total_count"] == len(big)
|
||||
assert out["resolved_config_entry_count"] == len(big)
|
||||
assert out["matched_count"] == len(big)
|
||||
assert out["models_truncated"] is True
|
||||
ung = out["model_display_sections"][0]
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe("KeyModelList", () => {
|
|||
{ title: "grp-a", section_kind: "access_group", models: ["m1"] },
|
||||
],
|
||||
source: "all-proxy-models",
|
||||
resolved_total_count: 2,
|
||||
resolved_config_entry_count: 2,
|
||||
matched_count: 2,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
@ -62,7 +62,7 @@ describe("KeyModelList", () => {
|
|||
{ title: "All team models", section_kind: "all_team_models", models: ["a"] },
|
||||
],
|
||||
source: "all-team-models",
|
||||
resolved_total_count: 1,
|
||||
resolved_config_entry_count: 1,
|
||||
matched_count: 1,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: true,
|
||||
|
|
@ -92,7 +92,7 @@ describe("KeyModelList", () => {
|
|||
{ title: "Other models", section_kind: "ungrouped", models: ["x"] },
|
||||
],
|
||||
source: "no-default-models",
|
||||
resolved_total_count: 1,
|
||||
resolved_config_entry_count: 1,
|
||||
matched_count: 1,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
@ -124,7 +124,7 @@ describe("KeyModelList", () => {
|
|||
{ title: "Other models", section_kind: "ungrouped", models: ["gpt-4"] },
|
||||
],
|
||||
source: "no-default-models",
|
||||
resolved_total_count: 1,
|
||||
resolved_config_entry_count: 1,
|
||||
matched_count: 1,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
@ -152,7 +152,7 @@ describe("KeyModelList", () => {
|
|||
data: {
|
||||
model_display_sections: [],
|
||||
source: "no-default-models",
|
||||
resolved_total_count: 5,
|
||||
resolved_config_entry_count: 5,
|
||||
matched_count: 0,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
@ -184,7 +184,7 @@ describe("KeyModelList", () => {
|
|||
{ title: "grp-b", section_kind: "access_group", models: ["m2", "m1"] },
|
||||
],
|
||||
source: "all-proxy-models",
|
||||
resolved_total_count: 2,
|
||||
resolved_config_entry_count: 2,
|
||||
matched_count: 2,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ describe("fetchKeyModelCall", () => {
|
|||
json: vi.fn().mockResolvedValue({
|
||||
model_display_sections: [],
|
||||
source: "no-default-models",
|
||||
resolved_total_count: 0,
|
||||
resolved_config_entry_count: 0,
|
||||
matched_count: 0,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
|
|||
|
|
@ -3323,7 +3323,8 @@ export interface KeyModelDisplaySection {
|
|||
export interface KeyModelResponse {
|
||||
model_display_sections: KeyModelDisplaySection[];
|
||||
source: string;
|
||||
resolved_total_count: number;
|
||||
/** Number of entries in the post–sentinel-resolution list (`resolved`), not concrete model cardinality. */
|
||||
resolved_config_entry_count: number;
|
||||
matched_count: number;
|
||||
models_truncated: boolean;
|
||||
all_team_models_without_team: boolean;
|
||||
|
|
@ -3339,38 +3340,34 @@ export const fetchKeyModelCall = async (
|
|||
key_id: string,
|
||||
options?: FetchKeyModelCallOptions
|
||||
): Promise<KeyModelResponse> => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.search !== undefined && options.search.trim() !== "") {
|
||||
params.set("search", options.search.trim());
|
||||
}
|
||||
if (options?.compact === true) {
|
||||
params.set("compact", "true");
|
||||
}
|
||||
const qs = params.toString();
|
||||
const base = proxyBaseUrl ? `${proxyBaseUrl}/key/${key_id}/models` : `/key/${key_id}/models`;
|
||||
let url = qs ? `${base}?${qs}` : base;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
const params = new URLSearchParams();
|
||||
if (options?.search !== undefined && options.search.trim() !== "") {
|
||||
params.set("search", options.search.trim());
|
||||
}
|
||||
if (options?.compact === true) {
|
||||
params.set("compact", "true");
|
||||
}
|
||||
const qs = params.toString();
|
||||
const base = proxyBaseUrl ? `${proxyBaseUrl}/key/${key_id}/models` : `/key/${key_id}/models`;
|
||||
const url = qs ? `${base}?${qs}` : base;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const userDailyActivityAggregatedCall = async (accessToken: string, startTime: Date, endTime: Date, userId: string | null = null) => {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ vi.mock("../networking", () => ({
|
|||
},
|
||||
],
|
||||
source: "default",
|
||||
resolved_total_count: 1,
|
||||
resolved_config_entry_count: 1,
|
||||
matched_count: 1,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ vi.mock("@/components/networking", () => ({
|
|||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(() => ({
|
||||
accessToken: "test-token-456",
|
||||
userId: "test-user-id",
|
||||
})),
|
||||
}));
|
||||
|
||||
const emptyKeyModelResponse = {
|
||||
model_display_sections: [],
|
||||
source: "no-default-models",
|
||||
resolved_total_count: 0,
|
||||
resolved_config_entry_count: 0,
|
||||
matched_count: 0,
|
||||
models_truncated: false,
|
||||
all_team_models_without_team: false,
|
||||
|
|
@ -43,13 +44,14 @@ describe("useGetKeyModels", () => {
|
|||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: "test-token-456",
|
||||
userId: "test-user-id",
|
||||
} as any);
|
||||
});
|
||||
|
||||
it("should load default full model list without compact", async () => {
|
||||
vi.mocked(networking.fetchKeyModelCall).mockResolvedValue({
|
||||
...emptyKeyModelResponse,
|
||||
resolved_total_count: 3,
|
||||
resolved_config_entry_count: 3,
|
||||
model_display_sections: [
|
||||
{ title: "Other models", section_kind: "ungrouped", models: ["a", "b", "c"] },
|
||||
],
|
||||
|
|
@ -62,6 +64,6 @@ describe("useGetKeyModels", () => {
|
|||
|
||||
await waitFor(() => expect(result.current.defaultModelsQuery.isSuccess).toBe(true));
|
||||
expect(networking.fetchKeyModelCall).toHaveBeenCalledWith("test-token-456", "test-key-id");
|
||||
expect(result.current.defaultModelsQuery.data?.resolved_total_count).toBe(3);
|
||||
expect(result.current.defaultModelsQuery.data?.resolved_config_entry_count).toBe(3);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import useAuthorized from '@/app/(dashboard)/hooks/useAuthorized';
|
|||
const SEARCH_DEBOUNCE_MS = 450;
|
||||
|
||||
export const useGetKeyModels = (key_id: string) => {
|
||||
const { accessToken } = useAuthorized();
|
||||
const { accessToken, userId } = useAuthorized();
|
||||
const cachePrincipal = userId ?? accessToken ?? '';
|
||||
const [searchInput, setSearchInputState] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useDebouncedState('', {
|
||||
wait: SEARCH_DEBOUNCE_MS,
|
||||
|
|
@ -28,7 +29,7 @@ export const useGetKeyModels = (key_id: string) => {
|
|||
const isSearchDebouncing = searchInput.trim() !== trimmedDebounced;
|
||||
|
||||
const defaultModelsQuery = useQuery({
|
||||
queryKey: ['keyModelsDefault', key_id],
|
||||
queryKey: ['keyModelsDefault', key_id, cachePrincipal],
|
||||
queryFn: () => {
|
||||
if (!accessToken) throw new Error('Access Token required');
|
||||
return fetchKeyModelCall(accessToken, key_id);
|
||||
|
|
@ -37,7 +38,7 @@ export const useGetKeyModels = (key_id: string) => {
|
|||
});
|
||||
|
||||
const searchQuery = useQuery({
|
||||
queryKey: ['keyModelsSearch', key_id, trimmedDebounced],
|
||||
queryKey: ['keyModelsSearch', key_id, trimmedDebounced, cachePrincipal],
|
||||
queryFn: () => {
|
||||
if (!accessToken) throw new Error('Access Token required');
|
||||
return fetchKeyModelCall(accessToken, key_id, { search: trimmedDebounced });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue