mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
Merge f0a0341e6f into 9071ca503e
This commit is contained in:
commit
47dedc01c4
8 changed files with 167 additions and 41 deletions
|
|
@ -1,6 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getModelDisplayName } from "./fetch_available_models_team_key";
|
||||
import {
|
||||
collapseModelSentinelSelection,
|
||||
getModelDisplayName,
|
||||
hasModelSentinel,
|
||||
} from "./fetch_available_models_team_key";
|
||||
|
||||
describe("getModelDisplayName", () => {
|
||||
it("should return display label for all proxy models", () => {
|
||||
|
|
@ -11,3 +15,31 @@ describe("getModelDisplayName", () => {
|
|||
expect(getModelDisplayName("openai/*")).toBe("All openai models");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasModelSentinel", () => {
|
||||
it.each(["all-proxy-models", "all-team-models", "no-default-models"])(
|
||||
"treats %s as exclusive, so the concrete models alongside it are unpickable",
|
||||
(sentinel) => {
|
||||
expect(hasModelSentinel([sentinel])).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("leaves a plain selection pickable", () => {
|
||||
expect(hasModelSentinel(["gpt-4o", "claude-sonnet-4-5"])).toBe(false);
|
||||
expect(hasModelSentinel([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collapseModelSentinelSelection", () => {
|
||||
it.each(["all-team-models", "all-proxy-models", "no-default-models"])(
|
||||
"drops the concrete models already picked when %s is chosen",
|
||||
(sentinel) => {
|
||||
expect(collapseModelSentinelSelection(["gpt-4o", sentinel])).toEqual([sentinel]);
|
||||
},
|
||||
);
|
||||
|
||||
it("leaves a selection of real models alone", () => {
|
||||
expect(collapseModelSentinelSelection(["gpt-4o", "claude-sonnet-4-5"])).toEqual(["gpt-4o", "claude-sonnet-4-5"]);
|
||||
expect(collapseModelSentinelSelection([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,8 +38,15 @@ export const fetchAvailableModelsForTeamOrKey = async (
|
|||
export const excludeProxyWideSentinel = (models: string[]): string[] =>
|
||||
models.filter((model) => model !== "all-proxy-models");
|
||||
|
||||
export const hasAllModelsSentinel = (models: string[]): boolean =>
|
||||
models.includes("all-proxy-models") || models.includes("all-team-models");
|
||||
const MODEL_SENTINELS = ["all-team-models", "all-proxy-models", "no-default-models"] as const;
|
||||
|
||||
export const hasModelSentinel = (models: string[]): boolean =>
|
||||
MODEL_SENTINELS.some((sentinel) => models.includes(sentinel));
|
||||
|
||||
export const collapseModelSentinelSelection = (models: string[]): string[] => {
|
||||
const sentinel = MODEL_SENTINELS.find((candidate) => models.includes(candidate));
|
||||
return sentinel === undefined ? models : [sentinel];
|
||||
};
|
||||
|
||||
export const getModelDisplayName = (model: string) => {
|
||||
if (model === "all-proxy-models") {
|
||||
|
|
|
|||
|
|
@ -776,6 +776,44 @@ describe("CreateKey", () => {
|
|||
expect(await screen.findByRole("option", { name: "All Team Models" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("option", { name: "All Proxy Models" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("offers No Default Models whether or not a team is selected", async () => {
|
||||
await openModal();
|
||||
await userEvent.click(await screen.findByLabelText("Models"));
|
||||
|
||||
expect(await screen.findByRole("option", { name: "No Default Models" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends the no-default-models sentinel rather than an empty list", async () => {
|
||||
await openModal();
|
||||
await nameTheKey();
|
||||
|
||||
await userEvent.click(await screen.findByLabelText("Models"));
|
||||
await userEvent.click(await screen.findByRole("option", { name: "No Default Models" }));
|
||||
await userEvent.keyboard("{Escape}");
|
||||
|
||||
await submit();
|
||||
|
||||
expect((await createdPayload()).models).toStrictEqual(["no-default-models"]);
|
||||
});
|
||||
|
||||
it("drops a model already picked when No Default Models is chosen after it", async () => {
|
||||
state.teams = [{ team_id: "team-1", team_alias: "Team One", models: ["team-model-1"] }];
|
||||
await openModal({ teams: state.teams as unknown as Team[] });
|
||||
await nameTheKey();
|
||||
|
||||
await userEvent.click(await screen.findByLabelText("Team"));
|
||||
await userEvent.click(await screen.findByRole("option", { name: /Team One/ }));
|
||||
|
||||
await userEvent.click(await screen.findByLabelText("Models"));
|
||||
await userEvent.click(await screen.findByRole("option", { name: "team-model-1" }));
|
||||
await userEvent.click(await screen.findByRole("option", { name: "No Default Models" }));
|
||||
await userEvent.keyboard("{Escape}");
|
||||
|
||||
await submit();
|
||||
|
||||
expect((await createdPayload()).models).toStrictEqual(["no-default-models"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organization dropdown", () => {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ import { TagRateLimitEditor, TagRateLimitEntry } from "../key_team_helpers/TagRa
|
|||
import {
|
||||
excludeProxyWideSentinel,
|
||||
getModelDisplayName,
|
||||
hasAllModelsSentinel,
|
||||
collapseModelSentinelSelection,
|
||||
hasModelSentinel,
|
||||
} from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { Team } from "../key_team_helpers/key_list";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
|
|
@ -626,18 +627,25 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
setSelectedProjectId(projectId);
|
||||
};
|
||||
|
||||
const sentinelOptions: MultiSelectOption[] =
|
||||
selectedProjectId === null
|
||||
? [
|
||||
selectedCreateKeyTeam
|
||||
? { value: "all-team-models", label: "All Team Models" }
|
||||
: { value: "all-proxy-models", label: "All Proxy Models" },
|
||||
{ value: "no-default-models", label: "No Default Models" },
|
||||
]
|
||||
: [];
|
||||
const sentinelValues = new Set(sentinelOptions.map((option) => option.value));
|
||||
const modelOptions: MultiSelectOption[] = [
|
||||
...(selectedProjectId === null && selectedCreateKeyTeam
|
||||
? [{ value: "all-team-models", label: "All Team Models" }]
|
||||
: []),
|
||||
...(selectedProjectId === null && !selectedCreateKeyTeam
|
||||
? [{ value: "all-proxy-models", label: "All Proxy Models" }]
|
||||
: []),
|
||||
...modelsToPick.map((model) => ({
|
||||
value: model,
|
||||
label: getModelDisplayName(model),
|
||||
disabled: hasAllModelsSentinel(selectedModels),
|
||||
})),
|
||||
...sentinelOptions,
|
||||
...modelsToPick
|
||||
.filter((model) => !sentinelValues.has(model))
|
||||
.map((model) => ({
|
||||
value: model,
|
||||
label: getModelDisplayName(model),
|
||||
disabled: hasModelSentinel(selectedModels),
|
||||
})),
|
||||
];
|
||||
|
||||
const changeKeyType = (write: FieldWrite) => (value: string) => {
|
||||
|
|
@ -908,14 +916,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
value={(control.value as string[] | undefined) ?? []}
|
||||
placeholder="Select models"
|
||||
disabled={keyType === "management" || keyType === "read_only"}
|
||||
onValueChange={(values) => {
|
||||
control.onChange(values);
|
||||
if (values.includes("all-team-models")) {
|
||||
form.setValue("models", ["all-team-models"]);
|
||||
} else if (values.includes("all-proxy-models")) {
|
||||
form.setValue("models", ["all-proxy-models"]);
|
||||
}
|
||||
}}
|
||||
onValueChange={(values) => control.onChange(collapseModelSentinelSelection(values))}
|
||||
/>
|
||||
)}
|
||||
</MountedFormField>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { modelSentinelOptions } from "./keyEditFieldNormalizers";
|
||||
|
||||
describe("modelSentinelOptions", () => {
|
||||
it("offers No Default Models on a key with no team, so an access-group-only key is selectable", () => {
|
||||
expect(modelSentinelOptions(null, false)).toEqual([
|
||||
{ value: "all-proxy-models", label: "All Proxy Models" },
|
||||
{ value: "no-default-models", label: "No Default Models" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("offers No Default Models on a team key once the team has loaded", () => {
|
||||
expect(modelSentinelOptions("team-1", true)).toEqual([
|
||||
{ value: "all-team-models", label: "All Team Models" },
|
||||
{ value: "no-default-models", label: "No Default Models" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("offers nothing while a team key is still loading its team", () => {
|
||||
expect(modelSentinelOptions("team-1", false)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -30,8 +30,9 @@ export const modelSentinelOptions = (
|
|||
keyTeamId: string | null | undefined,
|
||||
teamLoaded: boolean,
|
||||
): { value: string; label: string }[] => {
|
||||
if (keyTeamId == null) return [{ value: "all-proxy-models", label: "All Proxy Models" }];
|
||||
return teamLoaded ? [{ value: "all-team-models", label: "All Team Models" }] : [];
|
||||
const noDefaultModels = { value: "no-default-models", label: "No Default Models" };
|
||||
if (keyTeamId == null) return [{ value: "all-proxy-models", label: "All Proxy Models" }, noDefaultModels];
|
||||
return teamLoaded ? [{ value: "all-team-models", label: "All Team Models" }, noDefaultModels] : [];
|
||||
};
|
||||
|
||||
export const currentValuePlaceholder = (
|
||||
|
|
|
|||
|
|
@ -326,6 +326,30 @@ describe("KeyEditView", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("lists No Default Models once when the available models already carry the sentinel", async () => {
|
||||
vi.mocked(modelAvailableCall).mockResolvedValue({
|
||||
data: [{ id: "gpt-4" }, { id: "no-default-models" }],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onCancel={() => {}}
|
||||
onSubmit={async () => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={"user-1"}
|
||||
userRole={"Admin"}
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByLabelText("Models"));
|
||||
|
||||
expect(await screen.findByRole("option", { name: "gpt-4" })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("option", { name: "No Default Models" })).toHaveLength(1);
|
||||
expect(screen.queryByRole("option", { name: "no-default-models" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render tags", async () => {
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
|
|
|
|||
|
|
@ -49,7 +49,11 @@ import {
|
|||
tagLimitsToRows,
|
||||
tagRowsToLimits,
|
||||
} from "../key_team_helpers/TagRateLimitEditor";
|
||||
import { excludeProxyWideSentinel, hasAllModelsSentinel } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import {
|
||||
collapseModelSentinelSelection,
|
||||
excludeProxyWideSentinel,
|
||||
hasModelSentinel,
|
||||
} from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
|
||||
|
|
@ -325,13 +329,17 @@ export function KeyEditView({
|
|||
form.setValue("disabled_callbacks", internalValues);
|
||||
};
|
||||
|
||||
const sentinelOptions = modelSentinelOptions(keyData.team_id, team != null);
|
||||
const sentinelValues = new Set(sentinelOptions.map((option) => option.value));
|
||||
const modelOptions = [
|
||||
...modelSentinelOptions(keyData.team_id, team != null),
|
||||
...availableModels.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
disabled: hasAllModelsSentinel(selectedModels),
|
||||
})),
|
||||
...sentinelOptions,
|
||||
...availableModels
|
||||
.filter((model) => !sentinelValues.has(model))
|
||||
.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
disabled: hasModelSentinel(selectedModels),
|
||||
})),
|
||||
];
|
||||
|
||||
const visibleTeams = selectedOrganizationId
|
||||
|
|
@ -361,15 +369,7 @@ export function KeyEditView({
|
|||
id={id}
|
||||
options={modelOptions}
|
||||
value={isModelsDisabled ? [] : (value as string[] | undefined) ?? []}
|
||||
onValueChange={(next) => {
|
||||
if (next.includes("all-team-models")) {
|
||||
onChange(["all-team-models"]);
|
||||
} else if (next.includes("all-proxy-models")) {
|
||||
onChange(["all-proxy-models"]);
|
||||
} else {
|
||||
onChange(next);
|
||||
}
|
||||
}}
|
||||
onValueChange={(next) => onChange(collapseModelSentinelSelection(next))}
|
||||
disabled={isModelsDisabled}
|
||||
placeholder="Select models"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue