feat(ui): link the decision models banner to the supported providers docs and rename the System One playground to Decisions (#45732)

* feat(ui): list the decision providers from the cost map in the decision models banner

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): title-case decision providers the dashboard has no display name for

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): wait for the catalog query to fail before asserting the banner fallback

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(ui): replace Add a decision model with a Supported providers docs link in the decision models banner

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): drop the Add Model search hint from the decision models banner

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): call it the Decisions playground in the decision models banner

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(ui): call the System One playground the Decisions playground across the dashboard

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-09 23:46:14 -07:00 • committed by GitHub
parent 512692dd36
commit 2d896d57f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 38 additions and 80 deletions

View file

@ -22,15 +22,7 @@ vi.mock("./detailNavigation", () => ({
}));
vi.mock("@/components/molecules/DecisionModelsBanner", () => ({
default: ({ onAddModel }: { onAddModel?: () => void }) => (
<section aria-label="Decision models">
{onAddModel && (
<button type="button" onClick={onAddModel}>
Add a decision model
</button>
)}
</section>
),
default: () => <section aria-label="Decision models" />,
}));
vi.mock("@/components/model_info_view", () => ({
default: ({ modelId }: { modelId: string }) => <div data-testid="model-info">model:{modelId}</div>,
@ -107,21 +99,6 @@ describe("ModelsAndEndpointsPage", () => {
expect(screen.queryByTestId("panel-all-models")).not.toBeInTheDocument();
});
it("opens the Add Model tab from the decision models banner", async () => {
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole("button", { name: "Add a decision model" }));
expect(screen.getByTestId("panel-add")).toBeInTheDocument();
expect(screen.queryByTestId("panel-all-models")).not.toBeInTheDocument();
});
it("does not offer the banner's Add a decision model to a session that cannot add models", () => {
mockUseAuthorized.mockReturnValue(NON_ADMIN);
renderPage();
expect(screen.getByRole("region", { name: "Decision models" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Add a decision model" })).not.toBeInTheDocument();
});
it("does not show a view-only session the decision models banner, whose Playground it cannot open", () => {
mockUseAuthorized.mockReturnValue(VIEW_ONLY_ADMIN);
renderPage();

View file

@ -178,9 +178,7 @@ export default function ModelsAndEndpointsPage() {
</div>
</div>
{!isViewOnly && (
<DecisionModelsBanner onAddModel={canCreate && !modelId ? () => setActiveKey("add") : undefined} />
)}
{!isViewOnly && <DecisionModelsBanner />}
{modelId ? (
<ModelInfoView

View file

@ -582,7 +582,7 @@ describe("AddModelPanel decision models", () => {
"href",
"https://docs.litellm.ai/docs/decisions",
);
expect(within(note).getByRole("link", { name: "test it in the System One playground" })).toHaveAttribute(
expect(within(note).getByRole("link", { name: "test it in the Decisions playground" })).toHaveAttribute(
"href",
"/ui/playground?tab=system-one",
);

View file

@ -130,7 +130,7 @@ export default function ResponseView({ response, fallbackModel, latencyMs, error
if (error) {
return (
<Alert variant="destructive">
<AlertTitle>System One request failed</AlertTitle>
<AlertTitle>Decisions request failed</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
);
@ -141,7 +141,7 @@ export default function ResponseView({ response, fallbackModel, latencyMs, error
<Card>
<CardHeader>
<CardTitle>Calibrated probabilities</CardTitle>
<CardDescription>Send a request to see System One answers and probabilities.</CardDescription>
<CardDescription>Send a request to see decision answers and probabilities.</CardDescription>
</CardHeader>
</Card>
);

View file

@ -113,7 +113,7 @@ describe("SystemOneUI integration", () => {
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" }));
expect(screen.getByRole("note", { name: "Decision endpoint notice" })).toHaveTextContent(
"omit model to use the proxy's configured default.",
@ -174,7 +174,7 @@ describe("SystemOneUI integration", () => {
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" }));
expect(await screen.findByRole("link", { name: "Add a decision model" })).toHaveAttribute(
"href",
@ -304,7 +304,7 @@ describe("SystemOneUI integration", () => {
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" }));
const editor = await openJsonEditor();
const draft = JSON.stringify({
model: "my-decider",
@ -327,7 +327,7 @@ describe("SystemOneUI integration", () => {
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" }));
expect(editor).toHaveValue(draft);
expect(screen.queryByText("Selected choice")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Send" }));
@ -341,7 +341,7 @@ describe("SystemOneUI integration", () => {
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" }));
const payload = { state: "An outage", questions: { urgent: { type: "noul", instructions: "Is this urgent?" } } };
fireEvent.change(await openJsonEditor(), {
target: { value: JSON.stringify(payload) },
@ -369,7 +369,7 @@ describe("SystemOneUI integration", () => {
await screen.findByRole("button", { name: "Cancel request" });
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" }));
expect(mockFetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();

View file

@ -142,11 +142,11 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
>
<SelectTrigger className="w-80" aria-label="Decision endpoint">
<SelectValue>
{endpoint === "/v1/systemone" ? "System One · /v1/systemone" : "TypeSafe · /typesafe/v1/systemone"}
{endpoint === "/v1/systemone" ? "Decisions · /v1/systemone" : "TypeSafe · /typesafe/v1/systemone"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="/v1/systemone">System One · /v1/systemone</SelectItem>
<SelectItem value="/v1/systemone">Decisions · /v1/systemone</SelectItem>
<SelectItem value="/typesafe/v1/systemone">TypeSafe · /typesafe/v1/systemone</SelectItem>
</SelectContent>
</Select>
@ -231,7 +231,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
<Alert role="note" aria-label="Decision endpoint notice">
<Info />
<AlertTitle>
{endpoint === "/v1/systemone" ? "Decision models · System One" : "TypeSafe Jev · System One"}
{endpoint === "/v1/systemone" ? "Decision models · /v1/systemone" : "TypeSafe Jev · /typesafe/v1/systemone"}
</AlertTitle>
<AlertDescription>
{endpoint === "/v1/systemone"
@ -249,7 +249,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
</section>
<div className="grid gap-4 xl:min-h-0 xl:flex-1 xl:grid-cols-2">
<section className="flex min-h-96 flex-col xl:min-h-0" aria-label="System One request editor">
<section className="flex min-h-96 flex-col xl:min-h-0" aria-label="Decisions request editor">
<Tabs
value={view}
onValueChange={(value) => (value === "form" || value === "json") && setView(value)}
@ -272,7 +272,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
</TabsContent>
</Tabs>
</section>
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="System One results">
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="Decisions results">
<ResponseView
response={systemOne.data?.response}
fallbackModel={validation.payload?.model}

View file

@ -45,7 +45,7 @@ export default function DecisionModelNote({ catalog, litellmProvider }: Decision
<p>
After you add it,{" "}
<a href={uiHref(SYSTEM_ONE_PLAYGROUND_ROUTE)} className="underline">
test it in the System One playground
test it in the Decisions playground
</a>
</p>
</AlertDescription>

View file

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import DecisionModelsBanner from "./DecisionModelsBanner";
const STORAGE_KEY = "hideDecisionModelsBanner";
@ -10,7 +10,7 @@ describe("DecisionModelsBanner", () => {
localStorage.removeItem("hideCostOptimizationFeedbackBanner");
});
it("links directly to the System One playground", () => {
it("links directly to the Decisions playground", () => {
render(<DecisionModelsBanner />);
const link = screen.getByRole("link", { name: "Try decision models" });
expect(link).toHaveAttribute("href", "/ui/playground?tab=system-one");
@ -24,27 +24,14 @@ describe("DecisionModelsBanner", () => {
);
});
it("offers Add a decision model only when the page can add models, and calls back on click", () => {
const onAddModel = vi.fn();
const { rerender } = render(<DecisionModelsBanner />);
expect(screen.queryByRole("button", { name: "Add a decision model" })).not.toBeInTheDocument();
rerender(<DecisionModelsBanner onAddModel={onAddModel} />);
fireEvent.click(screen.getByRole("button", { name: "Add a decision model" }));
expect(onAddModel).toHaveBeenCalledTimes(1);
});
it("points only users who can add models at Add Model", () => {
const { rerender } = render(<DecisionModelsBanner />);
it("links Supported providers to the provider table in the docs", () => {
render(<DecisionModelsBanner />);
expect(screen.getByRole("link", { name: "Supported providers" })).toHaveAttribute(
"href",
"https://docs.litellm.ai/docs/decisions#supported-providers",
);
expect(screen.getByRole("paragraph")).not.toHaveTextContent("Add Model");
expect(screen.getByRole("paragraph")).toHaveTextContent("Call them at /v1/decisions or /v1/systemone");
rerender(<DecisionModelsBanner onAddModel={vi.fn()} />);
expect(screen.getByRole("paragraph")).toHaveTextContent(
"Search decision in Add Model to find them. Call them at /v1/decisions",
);
});
it("shows the announcement even if the old feedback banner was dismissed", () => {

View file

@ -1,18 +1,14 @@
import React, { useState } from "react";
import { ArrowRight, Sparkles, X } from "lucide-react";
import { DECISIONS_DOCS_URL, SYSTEM_ONE_PLAYGROUND_ROUTE } from "@/lib/decisionModels";
import { DECISIONS_DOCS_URL, DECISIONS_PROVIDERS_DOCS_URL, SYSTEM_ONE_PLAYGROUND_ROUTE } from "@/lib/decisionModels";
import { uiHref } from "@/utils/uiHref";
import { Button } from "@/components/ui/button";
const STORAGE_KEY = "hideDecisionModelsBanner";
interface DecisionModelsBannerProps {
onAddModel?: () => void;
}
const DecisionModelsBanner: React.FC<DecisionModelsBannerProps> = ({ onAddModel }) => {
const DecisionModelsBanner: React.FC = () => {
const [dismissed, setDismissed] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem(STORAGE_KEY) === "true";
@ -32,23 +28,22 @@ const DecisionModelsBanner: React.FC<DecisionModelsBannerProps> = ({ onAddModel
<div className="min-w-0 flex-1">
<h4 className="m-0 text-sm font-semibold text-foreground">Decision models are now supported</h4>
<p className="m-0 mt-0.5 text-xs text-muted-foreground">
{onAddModel && (
<>
Search <code>decision</code> in Add Model to find them.{" "}
</>
)}
Call them at <code>/v1/decisions</code> or <code>/v1/systemone</code>, or try them in the System One
Call them at <code>/v1/decisions</code> or <code>/v1/systemone</code>, or try them in the Decisions
playground.{" "}
<a href={DECISIONS_DOCS_URL} target="_blank" rel="noopener noreferrer" className="underline">
How to call them
</a>
</p>
</div>
{onAddModel && (
<Button type="button" variant="outline" className="shrink-0" onClick={onAddModel}>
Add a decision model
</Button>
)}
<Button
variant="outline"
className="shrink-0"
nativeButton={false}
role="link"
render={<a href={DECISIONS_PROVIDERS_DOCS_URL} target="_blank" rel="noopener noreferrer" />}
>
Supported providers
</Button>
<Button
className="shrink-0"
nativeButton={false}

View file

@ -1,6 +1,7 @@
import { z } from "zod";
export const DECISIONS_DOCS_URL = "https://docs.litellm.ai/docs/decisions";
export const DECISIONS_PROVIDERS_DOCS_URL = `${DECISIONS_DOCS_URL}#supported-providers`;
export const SYSTEM_ONE_PLAYGROUND_ROUTE = "playground?tab=system-one";
const DECISION_ENDPOINTS: ReadonlySet<string> = new Set(["/v1/decisions", "/v1/systemone"]);