mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge 42cdabf2ad into f285229b51
This commit is contained in:
commit
f0e65e2e2c
4 changed files with 138 additions and 8 deletions
|
|
@ -54,6 +54,35 @@ 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 () => {
|
||||
// The Provider dropdown submits the backend's `provider` field verbatim, and
|
||||
// "CURSOR" is not a provider_map key. The lowercase fallback has to carry it,
|
||||
// or the deployment is created against a provider litellm cannot resolve.
|
||||
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,20 @@ import {
|
|||
getProviderModels,
|
||||
providerLogoMap,
|
||||
provider_map,
|
||||
resolveLitellmProviderSlug,
|
||||
} from "./provider_info_helpers";
|
||||
|
||||
// The real catalog the Add Model dropdown is populated from, so a provider
|
||||
// added there flows into these expectations instead of going unnoticed.
|
||||
const PROVIDER_CREATE_FIELDS: { provider: string; litellm_provider: string }[] = JSON.parse(
|
||||
readFileSync(resolve(__dirname, "../../../../litellm/proxy/public_endpoints/provider_create_fields.json"), "utf8"),
|
||||
);
|
||||
|
||||
// The bundled cost map a default install serves the Add Model model list from.
|
||||
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", () => {
|
||||
describe("getProviderLogoAndName", () => {
|
||||
it("should return empty logo and dash display name when providerValue is empty", () => {
|
||||
|
|
@ -318,6 +332,47 @@ describe("provider_info_helpers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("resolveLitellmProviderSlug", () => {
|
||||
it("should resolve every provider the backend serves to the slug that backend declares", () => {
|
||||
// The dropdown passes the backend's `provider` field straight through, so
|
||||
// any provider whose key is spelled differently on the two sides (or is
|
||||
// missing from provider_map entirely) silently resolves to the wrong slug
|
||||
// and empties the model dropdown.
|
||||
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", () => {
|
||||
// "SAGEMAKER" and "SageMaker" are two distinct backend providers with two
|
||||
// distinct slugs. Resolving case-insensitively against provider_map would
|
||||
// collapse them and send sagemaker_chat for both.
|
||||
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 = {};
|
||||
|
|
@ -517,6 +572,45 @@ 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", () => {
|
||||
// Selecting "Cursor" in the dropdown passes the backend key "CURSOR",
|
||||
// which is not a provider_map key. Before the slug fallback this resolved
|
||||
// to undefined, matched nothing, and degraded the model field into a
|
||||
// free-text input with no candidates.
|
||||
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", () => {
|
||||
// "SAGEMAKER" (slug "sagemaker") and "SageMaker" (slug "sagemaker_chat")
|
||||
// are two distinct backend providers. Only the latter is a provider_map
|
||||
// key, so before the slug fallback the former matched nothing at all.
|
||||
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", () => {
|
||||
// Selecting MiniMax passes the backend key "MINIMAX", not the provider_map
|
||||
// key "MiniMax", so the model field showed no candidates for any MiniMax
|
||||
// model. Reading the bundled map means this checks what a default install
|
||||
// actually offers rather than a hand-written fixture.
|
||||
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" },
|
||||
|
|
|
|||
|
|
@ -306,6 +306,17 @@ export const provider_map: Record<string, string> = {
|
|||
|
||||
const standaloneSubproviderSlugs = new Set<string>(["bedrock_mantle"]);
|
||||
|
||||
// The Add Model dropdown hands us the `provider` field from
|
||||
// /public/providers/fields, which is not always a provider_map key: the backend
|
||||
// spells eleven of them differently ("MINIMAX" vs "MiniMax", "CURSOR" vs
|
||||
// "Cursor") or has no key at all ("MILVUS", "LANGFUSE"). For every provider the
|
||||
// backend currently serves, the lowercased value is the litellm slug, so use it
|
||||
// when the key misses. Resolving case-insensitively against provider_map
|
||||
// instead would be wrong: "SAGEMAKER" would hit the "SageMaker" key and yield
|
||||
// "sagemaker_chat" rather than the "sagemaker" the backend asked for.
|
||||
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,
|
||||
|
|
@ -467,7 +478,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