From 0e766a19a217059c04684178a93e187e0e460f88 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Thu, 30 Jul 2026 17:11:51 -0700 Subject: [PATCH] fix(ui): keep config-owned OTEL callbacks visible in the logging table The Active Logging Callbacks table filtered every callback whose name is an OTEL trace-destination backend (arize, langfuse_otel, weave_otel, generic) via LOGGING_BACKEND_IDS, intending to drop rows that duplicate a destination. But config-owned global callbacks (configured proxy-wide through /config/update) live in a separate namespace from the credential-backed destination rows, so nothing was ever a duplicate; the filter just removed the config rows, and an admin who had set up a global Arize/Langfuse-OTEL/Weave/generic callback lost all visibility and management of it in the dashboard even though it kept receiving traffic. Show config callbacks and destination rows together. Their name spaces don't overlap (config callbacks keyed by backend name, destinations by credential name), so no duplicate can arise. The other two LOGGING_BACKEND_IDS uses (the add-callback dropdown routing and the availableCallbacks catalog) are unchanged. --- .../src/components/settings.test.tsx | 36 ++++++++++++++++--- .../src/components/settings.tsx | 2 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index 6c255deac7b..11f3651b5d5 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -137,10 +137,9 @@ describe("Settings", () => { }); it("should display edit modal with fields when edit is clicked", async () => { - // Use a config callback that is NOT a logging-destination backend (arize, - // langfuse_otel, etc. are filtered from the table via LOGGING_BACKEND_IDS and - // edited through the destination flow instead). Datadog is a plain config - // callback, so it still renders a row with the legacy Test/Edit/Delete actions. + // Datadog is a plain config callback that renders a row with the legacy + // Test/Edit/Delete actions. Config-owned OTEL callbacks (arize, langfuse_otel, + // etc.) also render as their own rows now; see the regression test below. const mockCallback = { name: "datadog", variables: { @@ -204,6 +203,35 @@ describe("Settings", () => { }); }); + it("should render a config-owned OTEL callback (langfuse_otel) as its own row", async () => { + // Regression: a proxy-wide langfuse_otel/arize/weave/generic callback configured + // via /config/update must stay visible and manageable in the table. It was being + // filtered out by backend id, removing config-owned rows (not just duplicates). + mockGetCallbacksCall.mockResolvedValue({ + callbacks: [{ name: "langfuse_otel", variables: { LANGFUSE_PUBLIC_KEY: "pk", LANGFUSE_SECRET_KEY: "sk" } }], + available_callbacks: { + langfuse_otel: { + litellm_callback_name: "langfuse_otel", + litellm_callback_params: ["LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY"], + ui_callback_name: "Langfuse OTEL", + }, + }, + alerts: [], + }); + mockGetCallbackConfigsCall.mockResolvedValue([ + { id: "langfuse_otel", displayName: "Langfuse OTEL", dynamic_params: {} }, + ]); + + const { getByText } = renderSettings(defaultProps); + + await waitFor(() => { + expect(getByText("Active Logging Callbacks")).toBeInTheDocument(); + }); + await waitFor(() => { + expect(getByText("Langfuse OTEL")).toBeInTheDocument(); + }); + }); + it("should hold the callbacks table in loading state until the fetch settles", async () => { let resolveCallbacks: (value: { callbacks: never[]; diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index bcad61c56fd..8b0d8c2d165 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -670,7 +670,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, !LOGGING_BACKEND_IDS.has(c.name)), ...destinationRows]} + callbacks={[...callbacks, ...destinationRows]} availableCallbacks={allCallbacks} isLoading={isLoadingCallbacks} readOnly={!isProxyAdmin}