fix(ui): offer No Default Models when picking a virtual key's models

The Models dropdown on key create and edit listed the proxy or team
sentinel and the concrete models, but not no-default-models. An admin
granting access through an access group had to leave Models empty, which
serialises to an empty list and grants every proxy model instead.

Picking any sentinel now clears the models already selected, so a key
saved as no-default-models cannot keep an earlier one and stay authorised
for it. Both forms already collapsed the other two sentinels that way, so
that logic moves to a helper they share, and each form drops a sentinel
the backend also lists as an available model rather than offering it
twice.

Fixes #40212
This commit is contained in:
Animesh Kumar 2026-09-09 23:40:28 +05:30 committed by CaptainAni187
parent 9071ca503e
commit f0a0341e6f
8 changed files with 167 additions and 41 deletions

View file

@ -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([]);
});
});

View file

@ -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") {

View file

@ -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", () => {

View file

@ -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>

View file

@ -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([]);
});
});

View file

@ -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 = (

View file

@ -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

View file

@ -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"
/>