+
Charge analysis to
+
+
+ key.key_alias || `${key.token.slice(0, 8)}…`}
+ isItemEqualToValue={(a: Key, b: Key) => a.token === b.token}
+ onInputValueChange={(text, details) => {
+ if (details.reason === "input-change" || details.reason === "input-clear") {
+ setQuery(text);
+ }
+ }}
+ onValueChange={changeKey}
+ >
+
+
+ {loading ? "Loading keys…" : "No matching keys"}
+
+ {(key: Key) => (
+
+ {key.key_alias || `${key.token.slice(0, 8)}…`}
+
+ )}
+
+
+
+
+
+
+
+ Spend appears under this key in API Keys. Its permissions and limits apply.
+
+ {(error || keyPages.error) && (
+
+ {error || keyPages.error?.message}
+
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.integration.test.tsx
index 86499e5adf0..384dd9284db 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.integration.test.tsx
@@ -193,7 +193,14 @@ it("runs saved settings immediately without opening setup", async () => {
engines: [engine],
tracing_enabled: true,
workers: [
- { id: "worker", name: "Worker", revoked: false, scope: engine.scope, last_seen: new Date().toISOString() },
+ {
+ id: "worker",
+ name: "Worker",
+ revoked: false,
+ analysis_key_id: "a".repeat(64),
+ scope: engine.scope,
+ last_seen: new Date().toISOString(),
+ },
],
};
if (path === "/engine/lens/runs") return engine.jobs;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.tsx
index 178a7512821..1ad36c17299 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/EngineView.tsx
@@ -95,7 +95,9 @@ export function EngineView({ accessToken, readOnly = false }: { accessToken: str
const showEmpty = !query.isLoading && !query.error && engines.length === 0;
const engine = engines.find((e) => e.id === selected) ?? engines[0];
const connected =
- query.data?.workers?.some((w) => !w.revoked && query.dataUpdatedAt - Date.parse(w.last_seen) < 120000) ?? false;
+ query.data?.workers?.some(
+ (w) => !w.revoked && w.analysis_key_id && query.dataUpdatedAt - Date.parse(w.last_seen) < 120000,
+ ) ?? false;
const historyQuery = {
queryKey: ["lens-history", engine?.id, historyOffset, accessToken],
enabled: !!engine,
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.integration.test.tsx
index d116283f467..8fdeab444d8 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.integration.test.tsx
@@ -1,12 +1,12 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { describe, expect, it, vi } from "vitest";
-import { renderWithProviders } from "@/../tests/test-utils";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { renderWithProviders, testQueryClient } from "@/../tests/test-utils";
import { apiClient } from "@/components/networking";
import { WorkerSetup } from "./WorkerSetup";
vi.mock("@/components/networking", () => ({
- apiClient: { post: vi.fn() },
+ apiClient: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
proxyBaseUrl: "https://gateway.example/proxy",
}));
@@ -18,10 +18,19 @@ const created = {
last_seen: "1970-01-01T00:00:00Z",
scope: { all_teams: true, api_key_hash: "", team_id: "" },
revoked: false,
+ analysis_key_id: "b".repeat(64),
},
};
describe("Worker setup", () => {
+ beforeEach(() => {
+ testQueryClient.clear();
+ vi.clearAllMocks();
+ vi.mocked(apiClient.get).mockResolvedValue({
+ keys: [{ token: "b".repeat(64), key_alias: "Analysis" }],
+ total_pages: 1,
+ });
+ });
it("generates a complete command using one worker credential and the configured proxy address", async () => {
vi.mocked(apiClient.post).mockResolvedValue(created);
const user = userEvent.setup();
@@ -29,7 +38,14 @@ describe("Worker setup", () => {
expect(screen.getByRole("textbox", { name: "Your LiteLLM deployment URL" })).toHaveValue(
"https://gateway.example/proxy",
);
+ expect(screen.getByRole("button", { name: "Generate setup command" })).toBeDisabled();
+ await user.click(screen.getByRole("combobox", { name: "Charge analysis to" }));
+ await user.click(await screen.findByRole("option", { name: "Analysis" }));
await user.click(screen.getByRole("button", { name: "Generate setup command" }));
+ expect(apiClient.post).toHaveBeenCalledWith("/engine/workers/register", {
+ accessToken: "admin",
+ body: { name: "Lens analyzer", analysis_key_id: "b".repeat(64) },
+ });
expect(screen.getByRole("status")).toHaveTextContent("Waiting for your analyzer to connect");
await user.click(screen.getByRole("button", { name: "Copy Docker command" }));
const command = await navigator.clipboard.readText();
@@ -38,4 +54,28 @@ describe("Worker setup", () => {
expect(command).toContain("--add-host host.docker.internal:host-gateway");
expect(command).toContain("ghcr.io/berriai/litellm-lens-worker@sha256:");
});
+ it("assigns billing to an existing worker without replacing its access token", async () => {
+ const user = userEvent.setup();
+ const changed = vi.fn();
+ vi.mocked(apiClient.put).mockResolvedValue(created.worker);
+ renderWithProviders(
+