diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.test.tsx b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.test.tsx index a65a22edc85..0533b98b762 100644 --- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.test.tsx @@ -55,4 +55,39 @@ describe("LoggingCallbacksTable", () => { ); expect(getByText("custom_callback_x")).toBeInTheDocument(); }); + + // Regression: `/get_callbacks` returns the same `name` twice when a + // callback is registered for both success and failure (e.g. `generic_api` + // → POST to spend-log on both 200 and 4xx/5xx). The UI used to ignore + // the `type` field and render every row as "Success", masking the + // failure registration. Reading `record.type` fixes the badge AND + // composing the rowKey with type avoids React's duplicate-key warning. + it("renders distinct Success and Failure badges for same-name dual registration", () => { + const baseVars = { + SLACK_WEBHOOK_URL: null, + LANGFUSE_PUBLIC_KEY: null, + LANGFUSE_SECRET_KEY: null, + LANGFUSE_HOST: null, + OPENMETER_API_KEY: null, + }; + const { getAllByText, getByText } = render( + , + ); + // Both rows show the same display name, but distinct mode badges. + expect(getAllByText("Custom Callback API")).toHaveLength(2); + expect(getByText("Success")).toBeInTheDocument(); + expect(getByText("Failure")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx index 8f332d0317a..70ec6599ca2 100644 --- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx +++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx @@ -48,7 +48,6 @@ export const LoggingCallbacksTable: React.FC = ({ key: "name", render: (_: string, record: CallbackRow) => { const id = record.name; - console.log("availableCallbacks", availableCallbacks); const displayName = availableCallbacks[id]?.ui_callback_name || id; return
{displayName}
; }, @@ -57,7 +56,10 @@ export const LoggingCallbacksTable: React.FC = ({ title: Mode, key: "mode", render: (_: unknown, record: CallbackRow) => { - const mode = record.mode || "success"; + // Backend sends `type` (success | failure); legacy in-memory rows + // from add-callback flow set `mode`. Read both so newly-added rows + // and server-fetched rows both render correctly. + const mode = record.type || record.mode || "success"; const label = CALLBACK_MODES.find((m) => m.value === mode)?.label || mode; const badgeClass = mode === "success" @@ -109,7 +111,10 @@ export const LoggingCallbacksTable: React.FC = ({ record.name} + // `generic_api` can appear as both a success and a failure + // callback simultaneously — keying by `name` alone produced + // duplicate React keys. Compose with type to keep keys unique. + rowKey={(record) => `${record.name}-${record.type || record.mode || "success"}`} pagination={false} rowClassName={() => "hover:bg-gray-50"} /> diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts index 2fc180e49f3..5d265f95484 100644 --- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts +++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts @@ -1,5 +1,12 @@ export interface AlertingObject { name: string; + // Backend distinguishes success vs failure callback registrations + // (`/get_callbacks` returns `type: "success" | "failure"`). Same callback + // (e.g. `generic_api`) can appear twice — once per event class — and + // those entries fire on disjoint events, not double-fire on one event. + // UI must read this to render the correct badge; missing it caused + // every row to render as "Success". + type?: "success" | "failure" | "success_and_failure"; variables: AlertingVariables; }