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;
}