mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(ui): resolve the provider dropdown key to the slug the backend declares (#43707)
* fix(ui): resolve the provider dropdown key to the slug the backend declares
The Add Model form submits the `provider` field from /public/providers/fields
verbatim, but provider_map is keyed by hand on the frontend, so the two sides
disagree for 14 of the 124 providers the backend serves. Some differ only in
case ("MINIMAX" vs "MiniMax", "CURSOR" vs "Cursor"), others have no key at all
("MILVUS", "LANGFUSE"). getProviderModels looked the key up, got undefined,
matched no models, and the model field silently degraded from a dropdown into
a free-text input with no candidates
Both call sites now go through resolveLitellmProviderSlug, which prefers an
exact provider_map key and otherwise lowercases the value. That resolves every
provider in the catalog to the litellm_provider slug the backend declares for
it. Matching provider_map case-insensitively instead would look tempting and be
wrong: "SAGEMAKER" and "SageMaker" are two distinct providers with two distinct
slugs, and a case-insensitive lookup collapses both onto sagemaker_chat
The regression test drives the real provider_create_fields.json rather than a
fixture, so a provider added to the catalog with a mismatched key fails here
instead of reaching users as an empty dropdown
Co-authored-by: AaronHowell <237895480@qq.com>
* chore(ui): drop explanatory comments from provider slug fix
---------
Co-authored-by: AaronHowell <237895480@qq.com>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
This commit is contained in:
parent
985574f3dc
commit
8facf4aca2
4 changed files with 106 additions and 8 deletions
|
|
@ -54,6 +54,32 @@ describe("prepareModelAddRequest", () => {
|
|||
expect(deployment.litellmParamsObj.custom_llm_provider).toBe("petals");
|
||||
});
|
||||
|
||||
it("sends the backend's own slug for a provider whose key is spelled differently in provider_map", async () => {
|
||||
const formValues = {
|
||||
model_mappings: [{ public_name: "Composer", litellm_model: "cursor/composer-1" }],
|
||||
model_name: "cursor/composer-1",
|
||||
custom_llm_provider: "CURSOR",
|
||||
};
|
||||
|
||||
const deployments = await prepareModelAddRequest({ ...formValues }, "token", null);
|
||||
|
||||
expect(deployments![0].litellmParamsObj.custom_llm_provider).toBe("cursor");
|
||||
});
|
||||
|
||||
it("builds the wildcard model from the backend slug, not the raw dropdown key", async () => {
|
||||
const formValues = {
|
||||
model: ["all-wildcard"],
|
||||
custom_llm_provider: "CURSOR",
|
||||
};
|
||||
|
||||
const deployments = await prepareModelAddRequest({ ...formValues }, "token", null);
|
||||
|
||||
expect(deployments).toHaveLength(1);
|
||||
const [deployment] = deployments!;
|
||||
expect(deployment.modelName).toBe("cursor/*");
|
||||
expect(deployment.litellmParamsObj.model).toBe("cursor/*");
|
||||
});
|
||||
|
||||
it("ignores litellm_credential_name inside LiteLLM Params JSON", async () => {
|
||||
const formValues = {
|
||||
model_mappings: [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { toast } from "@/lib/toast";
|
||||
import { Model, modelCreateCall } from "../networking";
|
||||
import { provider_map } from "../provider_info_helpers";
|
||||
import { resolveLitellmProviderSlug } from "../provider_info_helpers";
|
||||
import { ptuPickerToUtcIso } from "../../utils/ptuDatetime";
|
||||
|
||||
export const prepareModelAddRequest = async (formValues: Record<string, any>, accessToken: string, form: any) => {
|
||||
|
|
@ -14,9 +14,7 @@ export const prepareModelAddRequest = async (formValues: Record<string, any>, ac
|
|||
// Handle wildcard case
|
||||
if (formValues["model"] && formValues["model"].includes("all-wildcard")) {
|
||||
const customProviderKey = formValues["custom_llm_provider"] as string;
|
||||
const mappedProvider =
|
||||
provider_map[customProviderKey as keyof typeof provider_map] ?? customProviderKey.toLowerCase();
|
||||
const litellm_custom_provider = mappedProvider;
|
||||
const litellm_custom_provider = resolveLitellmProviderSlug(customProviderKey);
|
||||
const wildcardModel = litellm_custom_provider + "/*";
|
||||
formValues["model_name"] = wildcardModel;
|
||||
modelMappings.push({
|
||||
|
|
@ -101,9 +99,7 @@ export const prepareModelAddRequest = async (formValues: Record<string, any>, ac
|
|||
if (key == "model_name") {
|
||||
litellmParamsObj["model"] = value;
|
||||
} else if (key == "custom_llm_provider") {
|
||||
const providerKey = value as string;
|
||||
const mappingResult = provider_map[providerKey as keyof typeof provider_map] ?? providerKey.toLowerCase();
|
||||
litellmParamsObj["custom_llm_provider"] = mappingResult;
|
||||
litellmParamsObj["custom_llm_provider"] = resolveLitellmProviderSlug(value as string);
|
||||
} else if (key == "model") {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { readFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import {
|
||||
Providers,
|
||||
getPlaceholder,
|
||||
|
|
@ -6,8 +8,17 @@ import {
|
|||
getProviderModels,
|
||||
providerLogoMap,
|
||||
provider_map,
|
||||
resolveLitellmProviderSlug,
|
||||
} from "./provider_info_helpers";
|
||||
|
||||
const PROVIDER_CREATE_FIELDS: { provider: string; litellm_provider: string }[] = JSON.parse(
|
||||
readFileSync(resolve(__dirname, "../../../../litellm/proxy/public_endpoints/provider_create_fields.json"), "utf8"),
|
||||
);
|
||||
|
||||
const BUNDLED_MODEL_MAP: Record<string, { litellm_provider?: string }> = JSON.parse(
|
||||
readFileSync(resolve(__dirname, "../../../../model_prices_and_context_window.json"), "utf8"),
|
||||
);
|
||||
|
||||
describe("provider_info_helpers", () => {
|
||||
it("maps Microsoft 365 Copilot to its chat model provider and placeholder", () => {
|
||||
expect(provider_map.MICROSOFT_365_COPILOT).toBe("microsoft_365_copilot");
|
||||
|
|
@ -364,6 +375,40 @@ describe("provider_info_helpers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("resolveLitellmProviderSlug", () => {
|
||||
it("should resolve every provider the backend serves to the slug that backend declares", () => {
|
||||
const misresolved = PROVIDER_CREATE_FIELDS.filter(
|
||||
(entry) => resolveLitellmProviderSlug(entry.provider) !== entry.litellm_provider,
|
||||
).map(
|
||||
(entry) =>
|
||||
`${entry.provider} -> ${resolveLitellmProviderSlug(entry.provider)} (want ${entry.litellm_provider})`,
|
||||
);
|
||||
expect(misresolved).toEqual([]);
|
||||
});
|
||||
|
||||
it("should resolve providers the backend spells in caps and provider_map spells in camel case", () => {
|
||||
expect(resolveLitellmProviderSlug("MINIMAX")).toBe("minimax");
|
||||
expect(resolveLitellmProviderSlug("CURSOR")).toBe("cursor");
|
||||
expect(resolveLitellmProviderSlug("RUNWAYML")).toBe("runwayml");
|
||||
});
|
||||
|
||||
it("should resolve providers absent from provider_map to their lowercased value", () => {
|
||||
expect(resolveLitellmProviderSlug("MILVUS")).toBe("milvus");
|
||||
expect(resolveLitellmProviderSlug("LANGFUSE")).toBe("langfuse");
|
||||
expect(resolveLitellmProviderSlug("LITELLM_PROXY")).toBe("litellm_proxy");
|
||||
});
|
||||
|
||||
it("should keep SAGEMAKER on the plain slug rather than the chat variant SageMaker maps to", () => {
|
||||
expect(resolveLitellmProviderSlug("SAGEMAKER")).toBe("sagemaker");
|
||||
expect(resolveLitellmProviderSlug("SageMaker")).toBe("sagemaker_chat");
|
||||
});
|
||||
|
||||
it("should prefer an exact provider_map key over the lowercase fallback", () => {
|
||||
expect(resolveLitellmProviderSlug("Vertex_AI")).toBe("vertex_ai");
|
||||
expect(resolveLitellmProviderSlug("Google_AI_Studio")).toBe("gemini");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProviderModels", () => {
|
||||
it("should return empty array when provider is not provided", () => {
|
||||
const modelMap = {};
|
||||
|
|
@ -587,6 +632,34 @@ describe("provider_info_helpers", () => {
|
|||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should populate models for a provider whose backend key is spelled differently from its provider_map key", () => {
|
||||
const modelMap = {
|
||||
"cursor/composer-1": { litellm_provider: "cursor" },
|
||||
"gpt-4": { litellm_provider: "openai" },
|
||||
};
|
||||
const result = getProviderModels("CURSOR" as Providers, modelMap);
|
||||
expect(result).toEqual(["cursor/composer-1"]);
|
||||
});
|
||||
|
||||
it("should populate models for SAGEMAKER, whose key is absent from provider_map", () => {
|
||||
const modelMap = {
|
||||
"sagemaker-base": { litellm_provider: "sagemaker" },
|
||||
"gpt-4": { litellm_provider: "openai" },
|
||||
};
|
||||
expect(getProviderModels("SAGEMAKER" as Providers, modelMap)).toEqual(["sagemaker-base"]);
|
||||
});
|
||||
|
||||
it("should populate MiniMax's bundled models from the real cost map", () => {
|
||||
const minimaxModels = Object.keys(BUNDLED_MODEL_MAP).filter(
|
||||
(key) => BUNDLED_MODEL_MAP[key]?.litellm_provider === "minimax",
|
||||
);
|
||||
expect(minimaxModels.length).toBeGreaterThan(0);
|
||||
|
||||
const result = getProviderModels("MINIMAX" as Providers, BUNDLED_MODEL_MAP);
|
||||
|
||||
expect([...result].sort()).toEqual([...minimaxModels].sort());
|
||||
});
|
||||
|
||||
it("should handle multiple providers correctly", () => {
|
||||
const modelMap = {
|
||||
"gpt-3.5-turbo": { litellm_provider: "openai" },
|
||||
|
|
|
|||
|
|
@ -318,6 +318,9 @@ export const provider_map: Record<string, string> = {
|
|||
|
||||
const standaloneSubproviderSlugs = new Set<string>(["bedrock_mantle"]);
|
||||
|
||||
export const resolveLitellmProviderSlug = (provider: string): string =>
|
||||
provider_map[provider] ?? provider.toLowerCase();
|
||||
|
||||
export const providerLogoMap: Partial<Record<Providers, string>> = {
|
||||
[Providers.A2A_Agent]: a2aAgentLogo.src,
|
||||
[Providers.AI21]: ai21Logo.src,
|
||||
|
|
@ -487,7 +490,7 @@ export const getPlaceholder = (selectedProvider: string): string => {
|
|||
|
||||
export const getProviderModels = (provider: string, modelMap: any): Array<string> => {
|
||||
let providerKey = provider;
|
||||
let custom_llm_provider = provider_map[providerKey];
|
||||
let custom_llm_provider = providerKey ? resolveLitellmProviderSlug(providerKey) : undefined;
|
||||
|
||||
let providerModels: Array<string> = [];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue