;
}) {
@@ -137,12 +141,18 @@ export function IdentityMatchDialog({
setEmail(event.target.value)}
required
/>
+
{error && (
{error}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.integration.test.tsx
index ab6848766bf..3c7965e917f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.integration.test.tsx
@@ -133,6 +133,7 @@ describe("ROICalculatorView", () => {
beforeEach(() => {
vi.mocked(apiClient.get).mockReset();
vi.mocked(apiClient.put).mockReset();
+ vi.mocked(apiClient.post).mockReset();
vi.mocked(apiClient.get).mockImplementation((path: string) => {
if (path === "/roi-calculator/settings") return Promise.resolve(settings);
if (path === "/roi-calculator/report") return Promise.resolve({ report: summary });
@@ -192,7 +193,7 @@ describe("ROICalculatorView", () => {
fireEvent.click(await screen.findByRole("tab", { name: "People" }));
fireEvent.click(await screen.findByRole("button", { name: "alice-work" }));
- fireEvent.change(screen.getByRole("textbox", { name: "Gateway email" }), {
+ fireEvent.change(screen.getByLabelText("Gateway email"), {
target: { value: "alice+work@example.com" },
});
fireEvent.click(screen.getByRole("button", { name: "Save match" }));
@@ -206,8 +207,9 @@ describe("ROICalculatorView", () => {
});
it("presents onboarding settings once when no report exists", async () => {
+ const emptySettings = { ...settings, has_github_token: false, ready: false, repos: [], estimator_model: "" };
vi.mocked(apiClient.get).mockImplementation((path: string) => {
- if (path === "/roi-calculator/settings") return Promise.resolve(settings);
+ if (path === "/roi-calculator/settings") return Promise.resolve(emptySettings);
if (path === "/roi-calculator/report") return Promise.resolve({ report: null });
return Promise.resolve(idleStatus);
});
@@ -243,10 +245,9 @@ describe("ROICalculatorView", () => {
render();
expect(await screen.findByRole("progressbar", { name: "Sync progress" })).toBeInTheDocument();
- fireEvent.click(screen.getByRole("tab", { name: "Settings" }));
expect(await screen.findByText("Spend per estimated engineering hour", {}, { timeout: 5000 })).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Connect GitHub to get started" })).not.toBeInTheDocument();
- expect(screen.getByRole("status")).toHaveTextContent("Up to date · Last synced Sep 30, 2026, 12:00 PM UTC");
+ expect(screen.getByRole("status")).toHaveTextContent("Last synced Sep 30, 2026, 12:00 PM UTC");
expect(screen.getByRole("status")).toHaveTextContent("57 of 57 estimates reused");
});
@@ -328,4 +329,22 @@ describe("ROICalculatorView", () => {
expect(await screen.findByText("Spend per estimated engineering hour", {}, { timeout: 7000 })).toBeInTheDocument();
expect(screen.queryByText("The sync status could not be loaded.")).not.toBeInTheDocument();
});
+ it("saves the edited schedule before running from Settings", async () => {
+ vi.mocked(apiClient.put).mockResolvedValue(settings);
+ vi.mocked(apiClient.post).mockResolvedValue({ ...idleStatus, running: true });
+ render();
+ fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
+ fireEvent.change(screen.getByLabelText("Update interval (hours)"), { target: { value: "6" } });
+ fireEvent.click(screen.getByRole("button", { name: "Save and run analysis" }));
+ await waitFor(() => expect(apiClient.post).toHaveBeenCalledWith("/roi-calculator/sync", { accessToken: "token" }));
+ expect(apiClient.put).toHaveBeenCalledWith(
+ "/roi-calculator/settings",
+ expect.objectContaining({
+ body: expect.objectContaining({ update_interval_minutes: 360, estimator_model: "estimator" }),
+ }),
+ );
+ expect(vi.mocked(apiClient.put).mock.invocationCallOrder[0]).toBeLessThan(
+ vi.mocked(apiClient.post).mock.invocationCallOrder[0],
+ );
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx
index 57d8f4d5c2c..b9bf304447c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx
@@ -49,14 +49,19 @@ export default function ROICalculatorView({
userRole?: string | null;
isViewOnly?: boolean;
}) {
- const readOnly = isViewOnly && isProxyAdminTierRole(userRole ?? "");
+ const [sampleSummary, setSampleSummary] = React.useState(null);
+ const adminReadOnly = isViewOnly && isProxyAdminTierRole(userRole ?? "");
+ const readOnly = adminReadOnly || sampleSummary !== null;
const [view, setView] = React.useState("overview");
const [settings, setSettings] = React.useState(null);
- const [summary, setSummary] = React.useState(null);
+ const [liveSummary, setSummary] = React.useState(null);
+ const summary = sampleSummary ?? liveSummary;
const [status, setStatus] = React.useState(IDLE_STATUS);
const [selectedPull, setSelectedPull] = React.useState(null);
const [matchingPerson, setMatchingPerson] = React.useState(null);
const [error, setError] = React.useState(null);
+ const statusRef = React.useRef(IDLE_STATUS);
+ const settingsLoaded = settings !== null;
const [query, setQuery] = React.useState("");
const loadReport = React.useCallback(async () => {
@@ -78,6 +83,7 @@ export default function ROICalculatorView({
setSettings(nextSettings);
setSummary(reportResponse.report);
setStatus(syncStatus);
+ statusRef.current = syncStatus;
setError(null);
})
.catch((reason: unknown) => {
@@ -89,9 +95,10 @@ export default function ROICalculatorView({
}, [accessToken]);
React.useEffect(() => {
- if (!accessToken || !status.running) return;
+ if (!accessToken || !settingsLoaded) return;
let cancelled = false;
let requestInFlight = false;
+ let reportNeedsRefresh = false;
const interval = window.setInterval(() => {
if (requestInFlight) return;
requestInFlight = true;
@@ -99,19 +106,20 @@ export default function ROICalculatorView({
.get("/roi-calculator/sync", { accessToken })
.then(async (nextStatus) => {
if (cancelled) return;
- setError(null);
- if (!nextStatus.running && nextStatus.phase === "complete") {
- try {
- const report = await loadReport();
- if (cancelled) return;
- setSummary(report);
- setError(null);
- if (view === "settings") setView("overview");
- } catch (reason) {
- if (!cancelled) setError(extractErrorMessage(reason));
- }
+ const previousStatus = statusRef.current;
+ statusRef.current = nextStatus;
+ setStatus(nextStatus);
+ const finished = !nextStatus.running && nextStatus.phase === "complete";
+ const reportChanged = previousStatus.running || nextStatus.finished_at !== previousStatus.finished_at;
+ if (finished && (reportChanged || reportNeedsRefresh)) {
+ reportNeedsRefresh = true;
+ const report = await loadReport();
+ if (cancelled) return;
+ setSummary(report);
+ reportNeedsRefresh = false;
+ setView((current) => (current === "settings" ? "overview" : current));
}
- if (!cancelled) setStatus(nextStatus);
+ if (!cancelled) setError(null);
})
.catch((reason: unknown) => {
if (!cancelled) setError(extractErrorMessage(reason));
@@ -124,13 +132,15 @@ export default function ROICalculatorView({
cancelled = true;
window.clearInterval(interval);
};
- }, [accessToken, loadReport, status.running, view]);
+ }, [accessToken, loadReport, settingsLoaded]);
const startSync = React.useCallback(async () => {
if (!accessToken || readOnly) return;
try {
setError(null);
- setStatus(await apiClient.post("/roi-calculator/sync", { accessToken }));
+ const nextStatus = await apiClient.post("/roi-calculator/sync", { accessToken });
+ statusRef.current = nextStatus;
+ setStatus(nextStatus);
} catch (reason) {
setError(extractErrorMessage(reason));
}
@@ -180,6 +190,27 @@ export default function ROICalculatorView({
);
}
+ const previewSample = async () => {
+ try {
+ const response = await apiClient.get("/roi-calculator/report", {
+ accessToken,
+ query: { mode: "demo" },
+ });
+ setSampleSummary(response.report);
+ setView("overview");
+ } catch (reason) {
+ setError(extractErrorMessage(reason));
+ }
+ };
+ const resetView = (updated: ROISettings) => {
+ setSettings(updated);
+ setSummary(null);
+ setView("overview");
+ setStatus(IDLE_STATUS);
+ statusRef.current = IDLE_STATUS;
+ };
+ const showLiveStatus = !sampleSummary && !status.running;
+ const scheduleLabel = settings.update_interval_minutes ? "Automatic updates enabled" : "Manual updates";
const progress = status.total > 0 ? Math.min(100, (status.done / status.total) * 100) : 0;
const statusIsIdleOrComplete = status.phase === "idle" || status.phase === "complete";
const syncIsUpToDate = !status.running && statusIsIdleOrComplete;
@@ -197,7 +228,7 @@ export default function ROICalculatorView({
: "Compare gateway spend with estimated engineering effort for merged pull requests"}
{syncedAt && (
- Up to date · Last synced {formatSyncedAt(syncedAt)}
+ Last synced {formatSyncedAt(syncedAt)}
{!status.running && status.phase === "complete" && status.reused > 0
? ` · ${status.reused} of ${status.total} estimates reused`
: ""}
@@ -206,27 +237,50 @@ export default function ROICalculatorView({
>
}
/>
- {readOnly && (
+ {!liveSummary && showLiveStatus && (
+
+ )}
+ {sampleSummary && (
+
+ Sample report
+
+ Example data only. No GitHub or model requests were made.
+
+
+
+ )}
+ {liveSummary && showLiveStatus && (
+
+ {status.next_update ? `Next update ${formatSyncedAt(status.next_update)}` : scheduleLabel}
+
+ )}
+ {adminReadOnly && (
Read-only access. Settings, analysis runs, and email matches are unavailable.
)}
-
- setView(value as View)}>
-
- Overview
- People
- Settings
-
-
- {view !== "settings" && !readOnly && (
-
- )}
-
+ {summary && (
+
+ setView(value as View)}>
+
+ Overview
+ People
+ {!sampleSummary && Settings}
+
+
+ {view !== "settings" && !readOnly && (
+
+ )}
+
+ )}
{error && (
@@ -263,6 +317,8 @@ export default function ROICalculatorView({
{status.done} of {status.total} pull requests processed · {status.reused} reused
+ {` · ${status.elapsed_seconds ?? 0}s elapsed`}
+ {status.remaining_seconds != null ? ` · about ${status.remaining_seconds}s remaining` : ""}
{!readOnly && (
@@ -276,11 +332,11 @@ export default function ROICalculatorView({
{view === "settings" || (!summary && !status.running) ? (
(person.email ? [person.email] : [])) ?? []}
onClose={() => setMatchingPerson(null)}
onSave={updateIdentity}
/>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorViews.tsx b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorViews.tsx
index dff2ef6ffca..75df18752de 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorViews.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorViews.tsx
@@ -15,7 +15,7 @@ import {
import type { ChartConfig } from "@/components/ui/chart";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
-import { coverageLabel, effortNote, estimateLabel, formatMoney, formatNumber } from "./roiCalculatorData";
+import { coverageLabel, peopleCsv, effortNote, estimateLabel, formatMoney, formatNumber } from "./roiCalculatorData";
import type { ROIPerson, ROIPull, ROISummary } from "./roiCalculatorData";
const CHART_CONFIG = {
@@ -207,8 +207,21 @@ export function ROIPeopleView({
onMatch: (person: ROIPerson, login: string) => void;
readOnly?: boolean;
}) {
+ const exportCsv = () => {
+ const url = URL.createObjectURL(new Blob([peopleCsv(summary)], { type: "text/csv;charset=utf-8" }));
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "litellm-roi.csv";
+ link.click();
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
+ };
return (
+
+
+
{effortNote(summary.effort_basis)} Spend includes each person’s full gateway usage for this period. This does
not measure hours saved by AI or financial returns.
@@ -247,8 +260,9 @@ export function ROIPeopleView({
) : (
Unassigned gateway spend
)}
- {person.match_methods.some((method) =>
- ["manual", "commit email", "profile email"].includes(method),
+ {person.match_methods.some(
+ (method) =>
+ ["manual", "commit email", "profile email"].includes(method) && person.spend != null,
) ? (
Matched
) : (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROISettingsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROISettingsPanel.tsx
index c37d71d7d7a..977d0dbc760 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROISettingsPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROISettingsPanel.tsx
@@ -8,6 +8,14 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import type { ROIRepository, ROIRepositoriesResponse, ROISettings, ROISettingsUpdate } from "./roiCalculatorData";
@@ -16,6 +24,7 @@ export default function ROISettingsPanel({
initialSettings,
onboarding,
onSaved,
+ onReset,
onStartSync,
readOnly,
syncDisabled,
@@ -24,10 +33,13 @@ export default function ROISettingsPanel({
initialSettings: ROISettings;
onboarding: boolean;
onSaved: (settings: ROISettings) => void;
+ onReset: (settings: ROISettings) => void;
onStartSync: () => Promise;
readOnly: boolean;
syncDisabled: boolean;
}) {
+ const initialStep = initialSettings.has_github_token ? 1 : 0;
+ const [step, setStep] = React.useState(initialSettings.ready ? 2 : initialStep);
const [apiUrl, setApiUrl] = React.useState(initialSettings.github_api_url);
const [token, setToken] = React.useState("");
const [clearToken, setClearToken] = React.useState(false);
@@ -35,6 +47,13 @@ export default function ROISettingsPanel({
const [model, setModel] = React.useState(initialSettings.estimator_model);
const [prompt, setPrompt] = React.useState(initialSettings.estimator_prompt);
const [backfillDays, setBackfillDays] = React.useState(String(initialSettings.backfill_days));
+ const [intervalHours, setIntervalHours] = React.useState(
+ String((initialSettings.update_interval_minutes ?? 1440) / 60),
+ );
+ const [estimatorKey, setEstimatorKey] = React.useState("");
+ const [clearEstimatorKey, setClearEstimatorKey] = React.useState(false);
+ const [repositoryName, setRepositoryName] = React.useState("");
+ const [resetOpen, setResetOpen] = React.useState(false);
const [repositoryQuery, setRepositoryQuery] = React.useState("");
const [repositoryPage, setRepositoryPage] = React.useState(1);
const [availableRepos, setAvailableRepos] = React.useState([]);
@@ -65,15 +84,17 @@ export default function ROISettingsPanel({
}
};
- const saveSettings = async (event: React.FormEvent) => {
- event.preventDefault();
- if (!accessToken || readOnly) return;
+ const saveSettings = async () => {
+ if (!accessToken || readOnly) return false;
const body: ROISettingsUpdate = {
github_api_url: apiUrl,
repos,
estimator_model: model,
estimator_prompt: prompt,
backfill_days: Number(backfillDays),
+ update_interval_minutes: Number(intervalHours) * 60,
+ ...(clearEstimatorKey ? { estimator_key: null } : {}),
+ ...(estimatorKey.trim() ? { estimator_key: estimatorKey.trim() } : {}),
...(clearToken ? { github_token: null } : {}),
...(token.trim() ? { github_token: token.trim() } : {}),
};
@@ -82,12 +103,65 @@ export default function ROISettingsPanel({
const updated: ROISettings = await apiClient.put("/roi-calculator/settings", { accessToken, body });
onSaved(updated);
setToken("");
+ setEstimatorKey("");
+ setClearEstimatorKey(false);
setClearToken(false);
setMessage("Settings saved.");
setError(null);
+ return true;
} catch (reason) {
setError(extractErrorMessage(reason));
setMessage(null);
+ return false;
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const submit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!(await saveSettings())) return;
+ if (onboarding && step === 0) {
+ try {
+ const result = await apiClient.get("/roi-calculator/repositories", { accessToken });
+ setAvailableRepos(result.repositories);
+ setHasMoreRepos(result.has_more);
+ setRepositoryPage(1);
+ setStep(1);
+ } catch (reason) {
+ setError(extractErrorMessage(reason));
+ }
+ } else if (onboarding && step === 1) setStep(2);
+ else if (onboarding) await onStartSync();
+ };
+
+ const saveAndRun = async () => {
+ if (await saveSettings()) await onStartSync();
+ };
+
+ const testConnections = async () => {
+ if (!(await saveSettings())) return;
+ setBusy(true);
+ try {
+ await apiClient.post("/roi-calculator/connections/test", { accessToken });
+ setMessage("Gateway model and selected repositories are available.");
+ } catch (reason) {
+ setError(extractErrorMessage(reason));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const resetSetup = async () => {
+ setBusy(true);
+ try {
+ const updated = await apiClient.post("/roi-calculator/setup/reset", { accessToken });
+ setRepos([]);
+ setStep(updated.has_github_token ? 1 : 0);
+ setResetOpen(false);
+ onReset(updated);
+ } catch (reason) {
+ setError(extractErrorMessage(reason));
} finally {
setBusy(false);
}
@@ -97,15 +171,25 @@ export default function ROISettingsPanel({
setRepos((current) => (current.includes(name) ? current.filter((repo) => repo !== name) : [...current, name]));
};
+ const formDisabled = busy || syncDisabled;
+ const runDisabled = formDisabled || !repos.length || !model;
+ const githubUrlChanged = apiUrl !== initialSettings.github_api_url;
+ const missingReplacementToken = initialSettings.has_github_token && githubUrlChanged && !token.trim();
+ const stepReady = [Boolean(token.trim() || initialSettings.has_github_token), repos.length > 0, Boolean(model)][step];
+ const onboardingLabel = step < 2 ? "Continue" : "Start backfill";
+ const submitLabel = onboarding ? onboardingLabel : "Save settings";
+
return (
- {onboarding ? "Connect GitHub to get started" : "ROI Calculator settings"}
+ {onboarding
+ ? ["Connect GitHub to get started", "Choose repositories", "Choose an estimator"][step]
+ : "ROI Calculator settings"}
{onboarding
- ? "Save a GitHub token, choose repositories and a router model, then run the analysis."
+ ? "Your gateway is already connected. Set up GitHub and an estimator to see your first report."
: "Choose GitHub repositories and the router model used for metadata-only estimates."}
@@ -120,168 +204,317 @@ export default function ROISettingsPanel({
{message}
)}
-
+
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.test.ts
index b31c3bae628..6e1c1790468 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.test.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.test.ts
@@ -40,6 +40,8 @@ const summary = {
describe("ROI calculator display helpers", () => {
it("formats spend and estimated hours without losing null values", () => {
expect(formatMoney(1234.5)).toBe("$1,234.50");
+ expect(formatMoney(0.0001)).toBe("<$0.01");
+ expect(formatMoney(0)).toBe("$0.00");
expect(formatMoney(null)).toBe("—");
expect(formatNumber(4.25)).toBe("4.3");
expect(formatNumber(null)).toBe("—");
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.ts b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.ts
index 9f8f8626a5c..e7dd5ebe8a3 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/roiCalculatorData.ts
@@ -23,14 +23,11 @@ const SYNCED_AT_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
timeZoneName: "short",
};
-export const formatMoney = (value: number | null | undefined): string =>
- value == null
- ? "—"
- : new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- maximumFractionDigits: 2,
- }).format(value);
+export const formatMoney = (value: number | null | undefined): string => {
+ if (value == null) return "—";
+ if (value > 0 && value < 0.01) return "<$0.01";
+ return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 2 }).format(value);
+};
export const formatNumber = (value: number | null | undefined): string =>
value == null ? "—" : new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(value);
@@ -63,3 +60,42 @@ export const filterPulls = (pulls: ROIPull[], query: string): ROIPull[] => {
`${pull.title} ${pull.repo} ${pull.number} ${pull.login}`.toLocaleLowerCase().includes(normalized),
);
};
+
+export const peopleCsv = (summary: ROISummary): string => {
+ const escape = (value: unknown): string => {
+ const text = value == null ? "" : String(value);
+ const safe = /^[=+@\-\t\r]/.test(text) ? `'${text}` : text;
+ return `"${safe.replaceAll('"', '""')}"`;
+ };
+ const rows = summary.people.map((person) => [
+ person.email,
+ person.logins.join(";"),
+ person.spend,
+ person.hours,
+ person.prs,
+ person.pending_prs,
+ person.eligible,
+ person.cost_per_hour,
+ summary.start,
+ summary.end,
+ summary.effort_basis ?? "unspecified",
+ ]);
+ return [
+ [
+ "email",
+ "github_logins",
+ "gateway_spend_usd",
+ "estimated_hours",
+ "merged_prs",
+ "pending_estimates",
+ "in_matched_cohort",
+ "cost_per_estimated_hour",
+ "start_utc",
+ "end_utc",
+ "effort_basis",
+ ],
+ ...rows,
+ ]
+ .map((row) => row.map(escape).join(","))
+ .join("\r\n");
+};
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index f7b29ae8648..8de25b7ac07 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -13540,6 +13540,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/roi-calculator/connections/test": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Test Roi Calculator Connections */
+ post: operations["test_roi_calculator_connections_roi_calculator_connections_test_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/roi-calculator/identity-map": {
parameters: {
query?: never;
@@ -13609,6 +13626,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/roi-calculator/setup/reset": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Reset Roi Calculator Setup */
+ post: operations["reset_roi_calculator_setup_roi_calculator_setup_reset_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/roi-calculator/sync": {
parameters: {
query?: never;
@@ -39393,6 +39427,8 @@ export interface components {
estimator_prompt: string;
/** Github Api Url */
github_api_url: string;
+ /** Has Estimator Key */
+ has_estimator_key: boolean;
/** Has Github Token */
has_github_token: boolean;
/** Identity Map */
@@ -39403,11 +39439,15 @@ export interface components {
ready: boolean;
/** Repos */
repos: string[];
+ /** Update Interval Minutes */
+ update_interval_minutes: number;
};
/** ROISettingsUpdate */
ROISettingsUpdate: {
/** Backfill Days */
backfill_days?: number | null;
+ /** Estimator Key */
+ estimator_key?: string | null;
/** Estimator Model */
estimator_model?: string | null;
/** Estimator Prompt */
@@ -39418,6 +39458,8 @@ export interface components {
github_token?: string | null;
/** Repos */
repos?: string[] | null;
+ /** Update Interval Minutes */
+ update_interval_minutes?: number | null;
};
/** ROISummaryResponse */
ROISummaryResponse: {
@@ -39453,23 +39495,36 @@ export interface components {
ROISyncStatus: {
/** Done */
done: number;
+ /**
+ * Elapsed Seconds
+ * @default 0
+ */
+ elapsed_seconds: number;
/** Error */
error: string | null;
/** Estimated */
estimated: number;
+ /** Finished At */
+ finished_at?: string | null;
/** Needs Attention */
needs_attention: number;
+ /** Next Update */
+ next_update?: string | null;
/**
* Phase
* @enum {string}
*/
phase: "idle" | "spend" | "repositories" | "estimates" | "complete" | "cancelled" | "error";
+ /** Remaining Seconds */
+ remaining_seconds?: number | null;
/** Reused */
reused: number;
/** Running */
running: boolean;
/** Stage */
stage: string;
+ /** Started At */
+ started_at?: string | null;
/** Total */
total: number;
};
@@ -64955,6 +65010,26 @@ export interface operations {
};
};
};
+ test_roi_calculator_connections_roi_calculator_connections_test_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ROISettingsResponse"];
+ };
+ };
+ };
+ };
update_roi_calculator_identity_map_roi_calculator_identity_map_put: {
parameters: {
query?: never;
@@ -64990,7 +65065,9 @@ export interface operations {
};
get_roi_calculator_report_roi_calculator_report_get: {
parameters: {
- query?: never;
+ query?: {
+ mode?: "live" | "demo";
+ };
header?: never;
path?: never;
cookie?: never;
@@ -65006,6 +65083,15 @@ export interface operations {
"application/json": components["schemas"]["ROIReportResponse"];
};
};
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
};
};
get_roi_calculator_repositories_roi_calculator_repositories_get: {
@@ -65093,6 +65179,26 @@ export interface operations {
};
};
};
+ reset_roi_calculator_setup_roi_calculator_setup_reset_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ROISettingsResponse"];
+ };
+ };
+ };
+ };
get_roi_calculator_sync_status_roi_calculator_sync_get: {
parameters: {
query?: never;