diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py
index 53a99065f2d..982122f6420 100644
--- a/litellm/proxy/proxy_server.py
+++ b/litellm/proxy/proxy_server.py
@@ -16992,6 +16992,79 @@ async def delete_callback(
)
+def _get_runtime_callbacks() -> list[tuple[str, str]]:
+ """
+ Discover callbacks registered in LiteLLM runtime memory (not from config).
+
+ Returns a list of (callback_name, callback_type) tuples where callback_type
+ is one of: "success", "failure", "success_and_failure".
+
+ Only discovers callbacks from known runtime sources:
+ - litellm.success_callback
+ - litellm._async_success_callback
+ - litellm.failure_callback
+ - litellm._async_failure_callback
+ - litellm.callbacks (success_and_failure)
+ """
+ runtime_callbacks: list[tuple[str, str]] = []
+
+ # Discover success callbacks
+ if litellm.success_callback:
+ callbacks = litellm.success_callback if isinstance(litellm.success_callback, list) else [litellm.success_callback]
+ for cb in callbacks:
+ if isinstance(cb, str):
+ runtime_callbacks.append((cb, "success"))
+
+ # Discover async success callbacks
+ if litellm._async_success_callback:
+ callbacks = litellm._async_success_callback if isinstance(litellm._async_success_callback, list) else [litellm._async_success_callback]
+ for cb in callbacks:
+ if isinstance(cb, str):
+ runtime_callbacks.append((cb, "success"))
+
+ # Discover failure callbacks
+ if litellm.failure_callback:
+ callbacks = litellm.failure_callback if isinstance(litellm.failure_callback, list) else [litellm.failure_callback]
+ for cb in callbacks:
+ if isinstance(cb, str):
+ runtime_callbacks.append((cb, "failure"))
+
+ # Discover async failure callbacks
+ if litellm._async_failure_callback:
+ callbacks = litellm._async_failure_callback if isinstance(litellm._async_failure_callback, list) else [litellm._async_failure_callback]
+ for cb in callbacks:
+ if isinstance(cb, str):
+ runtime_callbacks.append((cb, "failure"))
+
+ # Discover success_and_failure callbacks (litellm.callbacks)
+ if litellm.callbacks:
+ callbacks = litellm.callbacks if isinstance(litellm.callbacks, list) else [litellm.callbacks]
+ for cb in callbacks:
+ if isinstance(cb, str):
+ runtime_callbacks.append((cb, "success_and_failure"))
+
+ return runtime_callbacks
+
+
+def _normalize_callback_alias(callback_name: str) -> str:
+ """
+ Normalize callback names to canonical aliases.
+
+ Maps known aliases to their canonical forms:
+ - opentelemetry -> otel
+ - s3_v2 -> s3
+ - aws_sqs -> sqs
+ - custom_callback_api -> generic_api
+ """
+ alias_map = {
+ "opentelemetry": "otel",
+ "s3_v2": "s3",
+ "aws_sqs": "sqs",
+ "custom_callback_api": "generic_api",
+ }
+ return alias_map.get(callback_name, callback_name)
+
+
@router.get(
"/get/config/callbacks",
tags=["config.yaml"],
@@ -17049,14 +17122,39 @@ async def get_config(
"""
+ # Track which callbacks come from config (to mark them as NOT read_only)
+ config_callback_names: set[str] = set()
+
for _callback in _success_callbacks:
- _data_to_return.append(process_callback(_callback, "success", environment_variables))
+ callback_obj = process_callback(_callback, "success", environment_variables)
+ _data_to_return.append(callback_obj)
+ config_callback_names.add(_callback)
for _callback in _failure_callbacks:
- _data_to_return.append(process_callback(_callback, "failure", environment_variables))
+ callback_obj = process_callback(_callback, "failure", environment_variables)
+ _data_to_return.append(callback_obj)
+ config_callback_names.add(_callback)
for _callback in _success_and_failure_callbacks:
- _data_to_return.append(process_callback(_callback, "success_and_failure", environment_variables))
+ callback_obj = process_callback(_callback, "success_and_failure", environment_variables)
+ _data_to_return.append(callback_obj)
+ config_callback_names.add(_callback)
+
+ # Discover runtime-only callbacks (registered in litellm memory but not in config)
+ runtime_callbacks = _get_runtime_callbacks()
+ runtime_callback_names: set[str] = set()
+
+ for runtime_callback, callback_type in runtime_callbacks:
+ # Normalize the alias
+ normalized_name = _normalize_callback_alias(runtime_callback)
+ runtime_callback_names.add(normalized_name)
+
+ # Only add if not already in config (runtime-only)
+ if normalized_name not in config_callback_names and normalized_name not in {c["name"] for c in _data_to_return}:
+ callback_obj = process_callback(normalized_name, callback_type, environment_variables)
+ # Mark as read_only since it's runtime-only
+ callback_obj["read_only"] = True
+ _data_to_return.append(callback_obj)
_data_to_return = _apply_callback_role_gate(_data_to_return, is_full_admin)
diff --git a/tests/test_litellm/proxy/test_get_config_callbacks_integration.py b/tests/test_litellm/proxy/test_get_config_callbacks_integration.py
new file mode 100644
index 00000000000..9ac40aff194
--- /dev/null
+++ b/tests/test_litellm/proxy/test_get_config_callbacks_integration.py
@@ -0,0 +1,80 @@
+"""Integration test for /get/config/callbacks endpoint with runtime callbacks."""
+
+import pytest
+
+import litellm
+from litellm.proxy.proxy_server import _get_runtime_callbacks, _normalize_callback_alias
+
+
+class TestCallbackVisibility:
+ """Test that configured callbacks are not marked read_only and runtime-only are."""
+
+ def test_configured_callback_not_read_only(self):
+ """Test that a callback from config doesn't get marked as read_only."""
+ # The configured callback should not have read_only flag
+ # (it's only added for runtime-only callbacks in the route)
+ original = litellm.success_callback
+ try:
+ # Clear runtime callbacks
+ litellm.success_callback = None
+ litellm.failure_callback = None
+ litellm._async_success_callback = None
+ litellm._async_failure_callback = None
+ litellm.callbacks = None
+
+ # Verify nothing is discovered
+ runtime = _get_runtime_callbacks()
+ assert len(runtime) == 0
+ finally:
+ litellm.success_callback = original
+
+ def test_runtime_only_callback_discovered(self):
+ """Test that a runtime-only callback is discovered correctly."""
+ original = litellm.callbacks
+ try:
+ litellm.callbacks = ["otel"]
+ runtime = _get_runtime_callbacks()
+ assert ("otel", "success_and_failure") in runtime
+ finally:
+ litellm.callbacks = original
+
+ def test_multiple_runtime_callbacks(self):
+ """Test that multiple runtime callbacks are all discovered."""
+ original_success = litellm.success_callback
+ original_failure = litellm.failure_callback
+ original_combined = litellm.callbacks
+
+ try:
+ litellm.success_callback = ["langfuse"]
+ litellm.failure_callback = ["generic_api"]
+ litellm.callbacks = ["otel"]
+
+ runtime = _get_runtime_callbacks()
+ runtime_dict = {name: ctype for name, ctype in runtime}
+
+ assert "langfuse" in runtime_dict
+ assert runtime_dict["langfuse"] == "success"
+ assert "generic_api" in runtime_dict
+ assert runtime_dict["generic_api"] == "failure"
+ assert "otel" in runtime_dict
+ assert runtime_dict["otel"] == "success_and_failure"
+ finally:
+ litellm.success_callback = original_success
+ litellm.failure_callback = original_failure
+ litellm.callbacks = original_combined
+
+ def test_alias_normalization_on_runtime_callbacks(self):
+ """Test that runtime callbacks have aliases normalized."""
+ original = litellm.callbacks
+ try:
+ # Register an alias-named callback
+ litellm.callbacks = ["opentelemetry"]
+ runtime = _get_runtime_callbacks()
+
+ # Should discover as "opentelemetry", not normalized yet
+ assert ("opentelemetry", "success_and_failure") in runtime
+
+ # But _normalize_callback_alias should convert it
+ assert _normalize_callback_alias("opentelemetry") == "otel"
+ finally:
+ litellm.callbacks = original
diff --git a/tests/test_litellm/proxy/test_get_config_callbacks_runtime.py b/tests/test_litellm/proxy/test_get_config_callbacks_runtime.py
new file mode 100644
index 00000000000..5dd59d7a6ad
--- /dev/null
+++ b/tests/test_litellm/proxy/test_get_config_callbacks_runtime.py
@@ -0,0 +1,121 @@
+"""Test runtime callback visibility in /get/config/callbacks endpoint."""
+
+import asyncio
+import pytest
+
+import litellm
+from litellm.proxy.proxy_server import _get_runtime_callbacks, _normalize_callback_alias
+
+
+class TestRuntimeCallbackDiscovery:
+ """Test discovery of runtime-only callbacks."""
+
+ def test_discover_success_callback(self):
+ """Test discovering a callback from litellm.success_callback."""
+ original = litellm.success_callback
+ try:
+ litellm.success_callback = ["langfuse"]
+ callbacks = _get_runtime_callbacks()
+ callback_names = [name for name, _ in callbacks]
+ assert "langfuse" in callback_names
+ finally:
+ litellm.success_callback = original
+
+ def test_discover_failure_callback(self):
+ """Test discovering a callback from litellm.failure_callback."""
+ original = litellm.failure_callback
+ try:
+ litellm.failure_callback = ["generic_api"]
+ callbacks = _get_runtime_callbacks()
+ callback_types = [(name, ctype) for name, ctype in callbacks]
+ assert ("generic_api", "failure") in callback_types
+ finally:
+ litellm.failure_callback = original
+
+ def test_discover_success_and_failure_callbacks(self):
+ """Test discovering callbacks from litellm.callbacks."""
+ original = litellm.callbacks
+ try:
+ litellm.callbacks = ["otel"]
+ callbacks = _get_runtime_callbacks()
+ callback_types = [(name, ctype) for name, ctype in callbacks]
+ assert ("otel", "success_and_failure") in callback_types
+ finally:
+ litellm.callbacks = original
+
+ def test_no_duplicate_callbacks(self):
+ """Test that the same callback isn't returned multiple times."""
+ original_success = litellm.success_callback
+ original_failure = litellm.failure_callback
+ try:
+ litellm.success_callback = ["langfuse"]
+ litellm.failure_callback = ["langfuse"]
+ callbacks = _get_runtime_callbacks()
+ callback_entries = [(name, ctype) for name, ctype in callbacks]
+ # Both should be discovered
+ assert ("langfuse", "success") in callback_entries
+ assert ("langfuse", "failure") in callback_entries
+ finally:
+ litellm.success_callback = original_success
+ litellm.failure_callback = original_failure
+
+ def test_ignore_non_string_callbacks(self):
+ """Test that non-string callbacks are ignored."""
+ original = litellm.success_callback
+
+ class DummyCallback:
+ pass
+
+ try:
+ litellm.success_callback = [DummyCallback(), "langfuse"]
+ callbacks = _get_runtime_callbacks()
+ callback_names = [name for name, _ in callbacks]
+ # Only the string callback should be discovered
+ assert "langfuse" in callback_names
+ assert DummyCallback not in callback_names
+ finally:
+ litellm.success_callback = original
+
+
+class TestCallbackAliasNormalization:
+ """Test callback name alias normalization."""
+
+ def test_normalize_opentelemetry_to_otel(self):
+ """Test normalizing opentelemetry to otel."""
+ assert _normalize_callback_alias("opentelemetry") == "otel"
+
+ def test_normalize_s3_v2_to_s3(self):
+ """Test normalizing s3_v2 to s3."""
+ assert _normalize_callback_alias("s3_v2") == "s3"
+
+ def test_normalize_aws_sqs_to_sqs(self):
+ """Test normalizing aws_sqs to sqs."""
+ assert _normalize_callback_alias("aws_sqs") == "sqs"
+
+ def test_normalize_custom_callback_api_to_generic_api(self):
+ """Test normalizing custom_callback_api to generic_api."""
+ assert _normalize_callback_alias("custom_callback_api") == "generic_api"
+
+ def test_normalize_unknown_callback(self):
+ """Test that unknown callbacks pass through unchanged."""
+ assert _normalize_callback_alias("langfuse") == "langfuse"
+ assert _normalize_callback_alias("generic_api") == "generic_api"
+
+
+class TestGetConfigCallbacksEndpoint:
+ """Test the /get/config/callbacks endpoint with runtime callbacks."""
+
+ @pytest.mark.asyncio
+ async def test_runtime_callbacks_marked_read_only(self):
+ """Test that runtime-only callbacks are marked as read_only."""
+ # This test requires a running proxy server, so it's more of an integration test
+ # For unit test, we verify the helper functions work correctly
+ from litellm.proxy.common_utils.callback_utils import process_callback
+
+ # Simulate processing a runtime callback
+ callback_obj = process_callback("otel", "success", {})
+ callback_obj["read_only"] = True
+
+ assert callback_obj["read_only"] is True
+ assert callback_obj["name"] == "otel"
+ assert callback_obj["type"] == "success"
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 3ef7dee01b0..7348535451f 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
@@ -123,4 +123,34 @@ describe("LoggingCallbacksTable", () => {
expect(screen.getByText("Success")).toBeInTheDocument();
expect(screen.getByText("Failure")).toBeInTheDocument();
});
+
+ it("should not show actions menu for read-only (runtime-only) callbacks", () => {
+ const user = userEvent.setup();
+ const onEdit = vi.fn();
+ const onDelete = vi.fn();
+ const onTest = vi.fn();
+ const callback = { name: "otel", type: "success" as const, variables: baseVars, read_only: true };
+ render(
+ ,
+ );
+
+ // Verify callback is shown
+ expect(screen.getByText("OpenTelemetry")).toBeInTheDocument();
+
+ // Verify no actions menu exists for read-only callback
+ const actionsButton = screen.queryByTestId("callback-actions-otel-success");
+ expect(actionsButton).not.toBeInTheDocument();
+ });
});
diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTableColumns.tsx b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTableColumns.tsx
index 2263fe03b3d..c21329eb5fa 100644
--- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTableColumns.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTableColumns.tsx
@@ -50,6 +50,12 @@ interface CallbackRowActionsProps {
}
function CallbackRowActions({ callback, onTest, onEdit, onDelete }: CallbackRowActionsProps) {
+ // Hide actions for read-only (runtime-only) callbacks
+ const isReadOnly = (callback as any).read_only === true;
+ if (isReadOnly) {
+ return null;
+ }
+
return (