mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
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:
parent
512692dd36
commit
2d896d57f2
10 changed files with 38 additions and 80 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -178,9 +178,7 @@ export default function ModelsAndEndpointsPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{!isViewOnly && (
|
||||
<DecisionModelsBanner onAddModel={canCreate && !modelId ? () => setActiveKey("add") : undefined} />
|
||||
)}
|
||||
{!isViewOnly && <DecisionModelsBanner />}
|
||||
|
||||
{modelId ? (
|
||||
<ModelInfoView
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue