mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(ui): lock mode chips for tag-based guardrails in CustomCodeModal
- chips still display the flattened modes but disable editing when the stored mode is tag-based, with a hint, so a chip edit can't silently discard tag routing - drop two redundant helper comments in guardrail_info_helpers
This commit is contained in:
parent
5d4f14651f
commit
b2d831745d
3 changed files with 43 additions and 3 deletions
|
|
@ -14,6 +14,8 @@ const mockCreate = vi.mocked(createGuardrailCall);
|
|||
const mockUpdate = vi.mocked(updateGuardrailCall);
|
||||
const mockTest = vi.mocked(testCustomCodeGuardrail);
|
||||
|
||||
const TAG_BASED_MODE_HINT = "Tag-based mode is managed in config and can't be edited here";
|
||||
|
||||
describe("CustomCodeModal", () => {
|
||||
const onClose = vi.fn();
|
||||
const onSuccess = vi.fn();
|
||||
|
|
@ -94,6 +96,39 @@ describe("CustomCodeModal", () => {
|
|||
expect(payload.litellm_params).not.toHaveProperty("mode");
|
||||
});
|
||||
|
||||
it("should lock the mode chips when editing a tag-based mode", async () => {
|
||||
renderModal({
|
||||
editData: {
|
||||
guardrail_id: "g-1",
|
||||
guardrail_name: "tagged-guardrail",
|
||||
litellm_params: {
|
||||
mode: { tags: { "team:internal": "post_call" }, default: ["pre_call"] },
|
||||
default_on: false,
|
||||
custom_code: "def apply_guardrail(): pass",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByLabelText("pre_call (Request)")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("post_call (Response)")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("combobox")[0]).toBeDisabled();
|
||||
expect(screen.getByText(TAG_BASED_MODE_HINT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should leave the mode chips editable when editing a plain mode", async () => {
|
||||
renderModal({
|
||||
editData: {
|
||||
guardrail_id: "g-1",
|
||||
guardrail_name: "plain-guardrail",
|
||||
litellm_params: { mode: ["pre_call"], default_on: false, custom_code: "def apply_guardrail(): pass" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByLabelText("pre_call (Request)")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("combobox")[0]).toBeEnabled();
|
||||
expect(screen.queryByText(TAG_BASED_MODE_HINT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should keep save disabled until a guardrail name is entered", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { Switch } from "@/components/ui/switch";
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import type { GuardrailLitellmParams, GuardrailMode } from "@/components/guardrails/types";
|
||||
import { guardrailModeList } from "../guardrail_info_helpers";
|
||||
import { guardrailModeList, isTagBasedMode } from "../guardrail_info_helpers";
|
||||
|
||||
// Code templates
|
||||
const CODE_TEMPLATES = {
|
||||
|
|
@ -190,6 +190,7 @@ interface CustomCodeModalProps {
|
|||
const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onSuccess, accessToken, editData }) => {
|
||||
const anchor = useComboboxAnchor();
|
||||
const isEditMode = !!editData;
|
||||
const tagBasedMode = isEditMode && isTagBasedMode(editData?.litellm_params?.mode);
|
||||
const [guardrailName, setGuardrailName] = useState("");
|
||||
const [mode, setMode] = useState<string[]>(["pre_call"]);
|
||||
const [defaultOn, setDefaultOn] = useState(false);
|
||||
|
|
@ -519,6 +520,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
items={MODE_OPTIONS}
|
||||
value={selectedModeOptions}
|
||||
onValueChange={(options: ModeOption[]) => setMode(options.map((option) => option.value))}
|
||||
disabled={tagBasedMode}
|
||||
multiple
|
||||
>
|
||||
<ComboboxChips render={<div ref={anchor} />} className="w-full">
|
||||
|
|
@ -540,6 +542,11 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
{tagBasedMode && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Tag-based mode is managed in config and can't be edited here
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[180px]">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">Template</label>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ export const populateGuardrailProviderMap = (providerParamsResponse: Record<stri
|
|||
});
|
||||
};
|
||||
|
||||
// Normalizes a form "mode" value (string, string[], or empty) into a string array
|
||||
export const toModeArray = (raw: unknown): string[] => {
|
||||
if (Array.isArray(raw)) return raw.filter((m): m is string => typeof m === "string");
|
||||
if (typeof raw === "string") return [raw];
|
||||
|
|
@ -113,7 +112,6 @@ export const toModeArray = (raw: unknown): string[] => {
|
|||
export const isTagBasedMode = (raw: unknown): raw is { tags?: Record<string, unknown>; default?: unknown } =>
|
||||
raw !== null && typeof raw === "object" && !Array.isArray(raw);
|
||||
|
||||
// Every mode a guardrail can run in, with a tag-based mode's per-tag and default modes flattened and deduped
|
||||
export const guardrailModeList = (raw: unknown): string[] => {
|
||||
if (!isTagBasedMode(raw)) return toModeArray(raw);
|
||||
const tagged: string[] = raw.tags && typeof raw.tags === "object" ? Object.values(raw.tags).flatMap(toModeArray) : [];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue