Fix guardrail_mode.replace crash when backend returns non-string value

The backend type for guardrail_mode is Optional[Union[str, List[str], Dict]]
but the UI typed it as just string, causing a crash when .replace() was
called on null/object/array values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-03-18 12:17:19 -07:00
parent c4aa15b4e2
commit b13a7c6790
3 changed files with 56 additions and 8 deletions

View File

@ -151,6 +151,35 @@ describe("GuardrailViewer", () => {
expect(screen.queryByText(/Raw Bedrock Guardrail Response/)).not.toBeInTheDocument();
});
it("renders without crashing when guardrail_mode is null", () => {
const data = makeGuardrailInformation({ guardrail_mode: null });
renderWithProviders(<GuardrailViewer data={data} />);
expect(screen.getByText("Guardrails & Policy Compliance")).toBeInTheDocument();
// Null mode should display as dash
expect(screen.getByText("—")).toBeInTheDocument();
});
it("renders without crashing when guardrail_mode is an object", () => {
const data = makeGuardrailInformation({
guardrail_mode: { default: "pre_call", tags: {} },
});
renderWithProviders(<GuardrailViewer data={data} />);
expect(screen.getByText("Guardrails & Policy Compliance")).toBeInTheDocument();
expect(screen.getByText("PRE-CALL")).toBeInTheDocument();
});
it("renders without crashing when guardrail_mode is an array", () => {
const data = makeGuardrailInformation({
guardrail_mode: ["pre_call", "post_call"],
});
renderWithProviders(<GuardrailViewer data={data} />);
expect(screen.getByText("Guardrails & Policy Compliance")).toBeInTheDocument();
expect(screen.getByText("PRE-CALL")).toBeInTheDocument();
});
it("integration: renders with real Bedrock details without mocks", async () => {
const user = userEvent.setup();
const data = makeGuardrailInformation({

View File

@ -40,7 +40,7 @@ interface GuardrailInformation {
duration: number;
end_time: number;
start_time: number;
guardrail_mode: string;
guardrail_mode: string | string[] | Record<string, unknown> | null;
guardrail_name: string;
guardrail_status: string;
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse | any;
@ -77,9 +77,25 @@ const PROVIDERS_WITH_CUSTOM_RENDERERS = new Set([
"litellm_content_filter",
]);
const formatMode = (mode: unknown): string => {
if (mode == null || mode === "") return "—";
const s = typeof mode === "string" ? mode : String(mode);
/**
* Extracts a plain string from guardrail_mode, which may be a string,
* an array of strings, an object with a "default" key, or null.
*/
const resolveMode = (mode: GuardrailInformation["guardrail_mode"]): string | null => {
if (mode == null) return null;
if (typeof mode === "string") return mode;
if (Array.isArray(mode)) return mode[0] ?? null;
if (typeof mode === "object" && "default" in mode) {
const def = mode.default;
if (typeof def === "string") return def;
if (Array.isArray(def)) return def[0] ?? null;
}
return null;
};
const formatMode = (mode: GuardrailInformation["guardrail_mode"]): string => {
const s = resolveMode(mode);
if (s == null || s === "") return "—";
return s.replace(/_/g, "-").toUpperCase();
};
@ -302,9 +318,12 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
items.push({ type: "request", label: "Request received", offsetMs: 0 });
// Pre-call guardrails
const preCalls = sorted.filter((e) => e.guardrail_mode === "pre_call");
const postCalls = sorted.filter((e) => e.guardrail_mode === "post_call" || e.guardrail_mode === "logging_only");
const duringCalls = sorted.filter((e) => e.guardrail_mode === "during_call");
const preCalls = sorted.filter((e) => resolveMode(e.guardrail_mode) === "pre_call");
const postCalls = sorted.filter((e) => {
const m = resolveMode(e.guardrail_mode);
return m === "post_call" || m === "logging_only";
});
const duringCalls = sorted.filter((e) => resolveMode(e.guardrail_mode) === "during_call");
for (const e of preCalls) {
const offsetMs = Math.round((e.end_time - baseTime) * 1000);

View File

@ -23,7 +23,7 @@ export interface GuardrailInformation {
duration: number;
end_time: number;
start_time: number;
guardrail_mode: string;
guardrail_mode: string | string[] | Record<string, unknown> | null;
guardrail_name: string;
guardrail_status: string;
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse;