From 985cadfcafaafa6f8b1cb10a7128393ba71154a9 Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 11 Oct 2026 05:28:35 +0000 Subject: [PATCH] fix(ui): send the Decisions cURL under the configured auth header and preview the validated payload Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../components/systemOneUI/SystemOneUI.tsx | 13 ++++-- .../systemOneUI/ValidationFeedback.test.tsx | 40 +++++++++++++++++++ .../systemOneUI/lib/curlCommand.test.ts | 22 +++++++++- .../components/systemOneUI/lib/curlCommand.ts | 5 ++- 4 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/ValidationFeedback.test.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx index 83f585442b2..fad7eb1380d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx @@ -8,7 +8,7 @@ import { SimpleTooltip } from "@/components/ui/tooltip"; import { SearchSelect } from "@/components/shared/SearchSelect"; import { cn } from "@/lib/cva.config"; import { DECISIONS_DOCS_URL } from "@/lib/decisionModels"; -import { getProxyBaseUrl } from "@/components/networking"; +import { getGlobalLitellmHeaderName, getProxyBaseUrl } from "@/components/networking"; import { uiHref } from "@/utils/uiHref"; import { useMutation } from "@tanstack/react-query"; import { BookOpen, Eraser, LoaderCircle, Send } from "lucide-react"; @@ -118,6 +118,10 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = systemOne.reset(); } + function handleReset() { + handlePayloadChange(presetPayload(DECISION_PRESETS[0], endpoint, draftModel ?? decisionModels[0])); + } + function handlePayloadChange(value: string) { if (value !== rawPayload) { clearRequestState(); @@ -260,14 +264,14 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = value={rawPayload} onChange={handlePayloadChange} validation={validated.validation} - onReset={() => handlePayloadChange(presetPayload(DECISION_PRESETS[0], endpoint, draftModel))} + onReset={handleReset} /> ) : ( handlePayloadChange(presetPayload(DECISION_PRESETS[0], endpoint, draftModel))} + onReset={handleReset} /> )} @@ -275,7 +279,8 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/ValidationFeedback.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/ValidationFeedback.test.tsx new file mode 100644 index 00000000000..24f93c58af9 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/ValidationFeedback.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { IssueList, ValidationStatus } from "./ValidationFeedback"; +import type { PayloadValidation } from "./lib/validatePayload"; + +const validation = (issues: PayloadValidation["issues"]): PayloadValidation => ({ + isValid: issues.every((issue) => issue.severity !== "error"), + issues, +}); + +describe("ValidationStatus", () => { + it("counts only errors as issues", () => { + render( + , + ); + expect(screen.getByText("2 issues")).toBeInTheDocument(); + }); + + it("reports a valid payload when only warnings remain", () => { + render(); + expect(screen.getByText("Valid payload")).toBeInTheDocument(); + }); +}); + +describe("IssueList", () => { + it("lists every issue with its path and says ready when there are none", () => { + const { rerender } = render( + , + ); + expect(screen.getByRole("list", { name: "Payload validation issues" })).toHaveTextContent("stateRequired"); + rerender(); + expect(screen.getByRole("status")).toHaveTextContent("Ready to send"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts index bb7fc27451b..7c4a97f01a3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts @@ -7,10 +7,11 @@ describe("buildCurlCommand", () => { baseUrl: "http://localhost:4000/", endpoint: "/v1/systemone", payload: JSON.stringify({ model: "jev-latest", state: "it's broken" }), + authHeaderName: "Authorization", }); expect(command).toContain("curl -X POST 'http://localhost:4000/v1/systemone'"); - expect(command).toContain('-H "Authorization: Bearer $LITELLM_API_KEY"'); + expect(command).toContain("-H 'Authorization: Bearer $LITELLM_API_KEY'"); expect(command).not.toContain("sk-"); expect(command).toContain(' "state": "it\'\\\'\'s broken"'); expect(JSON.parse(curlPayload(command) ?? "")).toEqual({ model: "jev-latest", state: "it's broken" }); @@ -21,12 +22,29 @@ describe("buildCurlCommand", () => { baseUrl: "http://proxy.example/$(touch pwned)'; echo x", endpoint: "/v1/decisions", payload: "{}", + authHeaderName: "Authorization", }); expect(command).toContain(`curl -X POST 'http://proxy.example/$(touch pwned)'\\''; echo x/v1/decisions'`); }); + it("sends the key under the proxy's configured auth header name", () => { + const command = buildCurlCommand({ + baseUrl: "https://proxy.example", + endpoint: "/v1/systemone", + payload: "{}", + authHeaderName: "X-Litellm-Key", + }); + expect(command).toContain("-H 'X-Litellm-Key: Bearer $LITELLM_API_KEY'"); + expect(command).not.toContain("Authorization"); + }); + it("passes a payload that is not JSON through untouched", () => { - const command = buildCurlCommand({ baseUrl: "https://proxy.example", endpoint: "/v1/decisions", payload: "{" }); + const command = buildCurlCommand({ + baseUrl: "https://proxy.example", + endpoint: "/v1/decisions", + payload: "{", + authHeaderName: "Authorization", + }); expect(command).toContain("'https://proxy.example/v1/decisions'"); expect(curlPayload(command)).toBe("{"); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts index 01376232ea4..1153107cade 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts @@ -2,6 +2,7 @@ export interface CurlCommandInput { baseUrl: string; endpoint: "/v1/decisions" | "/v1/systemone"; payload: string; + authHeaderName: string; } export const CURL_KEY_PLACEHOLDER = "$LITELLM_API_KEY"; @@ -16,10 +17,10 @@ const prettyPayload = (payload: string): string => { } }; -export const buildCurlCommand = ({ baseUrl, endpoint, payload }: CurlCommandInput): string => +export const buildCurlCommand = ({ baseUrl, endpoint, payload, authHeaderName }: CurlCommandInput): string => [ `curl -X POST ${shellSingleQuote(`${baseUrl.replace(/\/+$/, "")}${endpoint}`)}`, - ` -H "Authorization: Bearer ${CURL_KEY_PLACEHOLDER}"`, + ` -H ${shellSingleQuote(`${authHeaderName}: Bearer ${CURL_KEY_PLACEHOLDER}`)}`, ` -H "Content-Type: application/json"`, ` -d ${shellSingleQuote(prettyPayload(payload))}`, ].join(" \\\n");