This commit is contained in:
KevinZhou 2026-09-05 12:34:55 +08:00 committed by GitHub
commit 6d959f2d7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 10 deletions

View file

@ -50,6 +50,7 @@ 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("preserves absent, unknown, empty, and explicit effort capability states", async () => {
@ -74,8 +75,48 @@ describe("fetchAvailableModels", () => {
["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("excludes the all-proxy-models permission sentinel from fallback options", async () => {
modelHubCallMock.mockResolvedValue({ data: [] });
modelAvailableCallMock.mockResolvedValue({
data: [{ id: "all-proxy-models" }, { id: "gpt-4o", mode: "chat" }],
});
expect(await fetchAvailableModels("token")).toEqual([{ model_group: "gpt-4o", mode: "chat" }]);
});
it("returns an empty list when both routes come back empty", async () => {
modelHubCallMock.mockResolvedValue({ data: [] });
modelAvailableCallMock.mockResolvedValue({ data: [] });
expect(await fetchAvailableModels("token")).toEqual([]);
});

View file

@ -31,6 +31,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);
@ -41,19 +50,36 @@ 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, "", "");
// Same permission sentinel excludeProxyWideSentinel filters elsewhere: it is
// a marker, not a selectable model.
const selectable = new Set(
excludeProxyWideSentinel(
(Array.isArray(response?.data) ? response.data : []).map(
(model: AvailableModel) => (model.model_group || model.id || model.model_name) ?? "",
),
),
);
return dedupeByGroup(toModelGroups(response?.data).filter((model) => selectable.has(model.model_group)));
};