mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(ui): fall back to /models for the Playground model dropdown
The Playground (Chat & Compare) populates its model dropdown from /model_group/info, which returns an empty list for non-admin UI sessions, so internal_user saw "No models available" even though /v1/models and the Models+Endpoints page list everything their key can call. When /model_group/info comes back empty or fails, fall back to /models — the canonical user-facing list — so the dropdown always mirrors what the selected credentials can actually call. Capability fields (reasoning efforts) are still preferred from /model_group/info when it has data.
This commit is contained in:
parent
166694948f
commit
a86cf0f619
2 changed files with 59 additions and 10 deletions
|
|
@ -50,14 +50,46 @@ describe("fetchAvailableModels", () => {
|
|||
{ model_group: "plain", mode: "chat" },
|
||||
{ model_group: "smart", mode: "chat", supports_reasoning: true, supported_reasoning_efforts: ["low", "high"] },
|
||||
]);
|
||||
expect(modelAvailableCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["an error payload in place of the list", { data: { error: "no access" } }],
|
||||
["a missing data key", {}],
|
||||
["no body at all", undefined],
|
||||
])("returns an empty list on %s rather than throwing", async (_label, response) => {
|
||||
modelHubCallMock.mockResolvedValue(response);
|
||||
])("falls back to /models on %s so non-admin sessions still get a dropdown", async (_label, hubResponse) => {
|
||||
modelHubCallMock.mockResolvedValue(hubResponse);
|
||||
modelAvailableCallMock.mockResolvedValue({
|
||||
data: [
|
||||
{ id: "llama", mode: "chat" },
|
||||
{ id: "gpt-4o", mode: "chat" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(await fetchAvailableModels("token")).toEqual([
|
||||
{ model_group: "gpt-4o", mode: "chat" },
|
||||
{ model_group: "llama", mode: "chat" },
|
||||
]);
|
||||
expect(modelAvailableCallMock).toHaveBeenCalledWith("token", "", "");
|
||||
});
|
||||
|
||||
it("falls back to /models when the model hub call fails", async () => {
|
||||
modelHubCallMock.mockRejectedValue(new Error("network down"));
|
||||
modelAvailableCallMock.mockResolvedValue({ data: [{ id: "gpt-4o", mode: "chat" }] });
|
||||
|
||||
expect(await fetchAvailableModels("token")).toEqual([{ model_group: "gpt-4o", mode: "chat" }]);
|
||||
});
|
||||
|
||||
it("deduplicates the fallback list by model group", async () => {
|
||||
modelHubCallMock.mockResolvedValue({ data: [] });
|
||||
modelAvailableCallMock.mockResolvedValue({ data: [{ id: "gpt-4o" }, { id: "gpt-4o" }] });
|
||||
|
||||
expect(await fetchAvailableModels("token")).toEqual([{ model_group: "gpt-4o" }]);
|
||||
});
|
||||
|
||||
it("returns an empty list when both routes come back empty", async () => {
|
||||
modelHubCallMock.mockResolvedValue({ data: [] });
|
||||
modelAvailableCallMock.mockResolvedValue({ data: [] });
|
||||
|
||||
expect(await fetchAvailableModels("token")).toEqual([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ const toModelGroup = (item: AvailableModel): ModelGroup => {
|
|||
};
|
||||
};
|
||||
|
||||
const toModelGroups = (fetchedData: unknown): ModelGroup[] =>
|
||||
(Array.isArray(fetchedData) ? fetchedData : [])
|
||||
.map(toModelGroup)
|
||||
.filter((model: ModelGroup) => model.model_group !== "")
|
||||
.sort((a: ModelGroup, b: ModelGroup) => a.model_group.localeCompare(b.model_group));
|
||||
|
||||
const dedupeByGroup = (models: ModelGroup[]): ModelGroup[] =>
|
||||
Array.from(new Map(models.map((model) => [model.model_group, model])).values());
|
||||
|
||||
export const fetchAvailableModelsForTeam = async (accessToken: string, teamId: string): Promise<ModelGroup[]> => {
|
||||
const response = await modelAvailableCall(accessToken, "", "", false, teamId);
|
||||
const modelNames: string[] = (response?.data ?? []).map((model: { id: string }) => model.id);
|
||||
|
|
@ -39,19 +48,27 @@ export const fetchAvailableModelsForTeam = async (accessToken: string, teamId: s
|
|||
};
|
||||
|
||||
/**
|
||||
* Fetches available models using modelHubCall and formats them for the selection dropdown.
|
||||
* Fetches the models the given credentials can call, for the selection dropdown.
|
||||
*
|
||||
* Primary source is /model_group/info (modelHubCall), which carries capability
|
||||
* fields like supported_reasoning_efforts. That route returns an empty list for
|
||||
* non-admin UI sessions (#38534), so when it comes back empty or fails we fall
|
||||
* back to /models — the canonical list (same route as GET /v1/models) that
|
||||
* works for both UI session tokens and virtual keys.
|
||||
*/
|
||||
export const fetchAvailableModels = async (accessToken: string): Promise<ModelGroup[]> => {
|
||||
let models: ModelGroup[] = [];
|
||||
try {
|
||||
const fetchedModels = await modelHubCall(accessToken);
|
||||
const fetchedData: unknown = fetchedModels?.data;
|
||||
const models: ModelGroup[] = (Array.isArray(fetchedData) ? fetchedData : [])
|
||||
.map(toModelGroup)
|
||||
.filter((model: ModelGroup) => model.model_group !== "")
|
||||
.sort((a: ModelGroup, b: ModelGroup) => a.model_group.localeCompare(b.model_group));
|
||||
return Array.from(new Map(models.map((model) => [model.model_group, model])).values());
|
||||
models = toModelGroups(fetchedModels?.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching model info:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (models.length > 0) {
|
||||
return dedupeByGroup(models);
|
||||
}
|
||||
|
||||
const response = await modelAvailableCall(accessToken, "", "");
|
||||
return dedupeByGroup(toModelGroups(response?.data));
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue