Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
24bd8a8
fix(proxy): surface runtime-registered callbacks in /get/config/callb…
yucheng-berri Aug 30, 2026
d99ab35
fix: filter internal proxy hooks from runtime callbacks, update test
yucheng-berri Aug 30, 2026
939a58f
fix: guard non-list config callbacks in get_config, use monkeypatch i…
yucheng-berri Aug 31, 2026
beb5241
fix: rework runtime callback inventory filtering and dedup
yucheng-berri Aug 31, 2026
dfa0cf3
fix: list dict-shaped callback config values by their keys
yucheng-berri Aug 31, 2026
9aca4f6
fix: mark dotted-path callbacks read-only to prevent duplicate display
yucheng-berri Sep 1, 2026
0da275c
fix: dedupe dotted-path callbacks by instance module instead of marki…
yucheng-berri Sep 1, 2026
7c5660e
test: cover dotted-path dedup across success, failure, and callbacks …
yucheng-berri Sep 1, 2026
f2558b4
fix(proxy): filter runtime callback inventory by object identity and …
yucheng-berri Sep 9, 2026
a75d5b6
test(proxy): keep configured-callback assertions minimal when runtime…
yucheng-berri Sep 9, 2026
12497f7
fix(proxy): hide internal cache string callback from runtime callback…
yucheng-berri Sep 9, 2026
1a8e891
fix(proxy): hide auto-registered vector store hook from callback inve…
yucheng-berri Sep 9, 2026
70c900b
fix(proxy): keep YAML OTel-family callbacks listed next to a configur…
yucheng-berri Sep 10, 2026
8c461c4
test(proxy): cover scalar and null YAML callback keys in callback inv…
yucheng-berri Sep 10, 2026
f3b76f1
refactor(proxy): drop docstrings that restate callback inventory helpers
yucheng-berri Sep 10, 2026
d63e655
fix(proxy): keep runtime-only s3 and sqs callbacks in UI Logging inve…
yucheng-berri Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions litellm/litellm_core_utils/logging_callback_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,14 +441,24 @@ def get_callbacks_by_type(self) -> CallbacksByType:

return result

def get_callback_objects(self) -> tuple[tuple[str, CustomLogger | Callable], ...]:
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
return tuple(
(self._get_callback_string(callback), callback)
for callback in self._get_all_callbacks()
if not isinstance(callback, str)
)

def _get_callback_string(self, callback: CustomLogger | Callable | str) -> str:
from litellm.integrations.opentelemetry import OpenTelemetry
from litellm.litellm_core_utils.custom_logger_registry import (
CustomLoggerRegistry,
)

"""Convert a callback to its string representation"""
if isinstance(callback, str):
return callback
elif isinstance(callback, OpenTelemetry) and callback.callback_name is not None:
return callback.callback_name
elif isinstance(callback, CustomLogger):
# Try to get the string representation from the registry
callback_str: Final = CustomLoggerRegistry.get_callback_str_from_class_type(type(callback))
Expand Down
94 changes: 89 additions & 5 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ def generate_feedback_box():
WEEKLY_SPEND_REPORT_JOB_ID,
)
from litellm.exceptions import RejectedRequestError
from litellm.integrations.custom_guardrail import ModifyResponseException
from litellm.integrations.custom_guardrail import CustomGuardrail, ModifyResponseException
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting
from litellm.litellm_core_utils.agentic_loop_settings import (
Expand Down Expand Up @@ -17550,6 +17550,66 @@ async def delete_callback(
)


def _normalize_callback_alias(callback_name: str) -> str:
callback_aliases: Final = (
("opentelemetry", "otel"),
("s3_v2", "s3"),
("aws_sqs", "sqs"),
("custom_callback_api", "generic_api"),
)
return next(
(canonical_name for alias, canonical_name in callback_aliases if alias == callback_name),
callback_name,
)


def _callback_module_name(callback: CustomLogger | Callable[..., object]) -> str:
if inspect.ismethod(callback):
return callback.__func__.__module__
if inspect.isfunction(callback):
return callback.__module__
return type(callback).__module__


def _is_litellm_internal_callback(callback_name: str, callback: CustomLogger | Callable[..., object]) -> bool:
from litellm.litellm_core_utils.custom_logger_registry import CustomLoggerRegistry

module_owner: Final = _callback_module_name(callback).partition(".")[0]
is_registered_integration: Final = callback_name in CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE
return not is_registered_integration and module_owner in ("litellm", "litellm_enterprise")
Comment thread
cursor[bot] marked this conversation as resolved.


def _is_instance_of_configured_callback(
callback_name: str, callback: CustomLogger | Callable[..., object], configured_classes: tuple[type, ...]
) -> bool:
"""Self-naming OTel-family instances (`arize`, `weave_otel`) match by name, so a configured `logfire` (a bare
`OpenTelemetry`) does not hide YAML-configured siblings of the same class."""
from litellm.litellm_core_utils.custom_logger_registry import CustomLoggerRegistry

class_derived_name: Final = CustomLoggerRegistry.get_callback_str_from_class_type(type(callback))
return isinstance(callback, configured_classes) and callback_name in (class_derived_name, type(callback).__name__)


def _hidden_runtime_callback_names(configured_callback_names: frozenset[str]) -> frozenset[str]:
from litellm.litellm_core_utils.custom_logger_registry import CustomLoggerRegistry

configured_classes: Final = tuple(
CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE[name]
for name in configured_callback_names
if name in CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE
)
Comment thread
cursor[bot] marked this conversation as resolved.
configured_modules: Final = frozenset(name.rsplit(".", 1)[0] for name in configured_callback_names if "." in name)
internal_callback_names: Final = frozenset({"cache", "vector_store_pre_call_hook"})
return internal_callback_names | frozenset(
callback_name
for callback_name, callback in litellm.logging_callback_manager.get_callback_objects()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if isinstance(callback, CustomGuardrail)
or _is_litellm_internal_callback(callback_name, callback)
or _is_instance_of_configured_callback(callback_name, callback, configured_classes)
or _callback_module_name(callback) in configured_modules
)


@router.get(
"/get/config/callbacks",
tags=["config.yaml"],
Expand Down Expand Up @@ -17582,10 +17642,10 @@ async def get_config(
# Normalize string callbacks to lists
def normalize_callback(callback):
if isinstance(callback, str):
return [callback]
elif callback is None:
return []
return callback
return (callback,)
if callback is None:
return ()
return tuple(callback) if isinstance(callback, (list, dict)) else ()

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Dictionary callback deletion fails

With dictionary-shaped settings, normalize_callback marks each key editable. Deleting that row reaches list-only delete_callback and returns an error

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dict-shaped success_callback rows were already listed as editable on the base branch, and the delete route was list-only before this PR. Unchanged here, out of scope.


_success_callbacks = normalize_callback(_success_callbacks)
_failure_callbacks = normalize_callback(_failure_callbacks)
Expand Down Expand Up @@ -17616,6 +17676,30 @@ def normalize_callback(callback):
for _callback in _success_and_failure_callbacks:
_data_to_return.append(process_callback(_callback, "success_and_failure", environment_variables))

configured_callback_names: Final = frozenset(
_normalize_callback_alias(callback)
for callback in (_success_callbacks + _failure_callbacks + _success_and_failure_callbacks)
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
runtime_callbacks_by_type: Final = litellm.logging_callback_manager.get_callbacks_by_type()
hidden_callback_names: Final = _hidden_runtime_callback_names(configured_callback_names)
runtime_callback_rows: Final = tuple(
(_normalize_callback_alias(callback_name), callback_type)
for callback_type, callback_names in (
("success", runtime_callbacks_by_type["success"]),
("failure", runtime_callbacks_by_type["failure"]),
("success_and_failure", runtime_callbacks_by_type["success_and_failure"]),
)
for callback_name in callback_names
if callback_name not in hidden_callback_names
)
runtime_only_rows: Final = sorted(
frozenset(row for row in runtime_callback_rows if row[0] not in configured_callback_names)
)
_data_to_return.extend(
dict(process_callback(callback_name, callback_type, environment_variables), read_only=True)
for callback_name, callback_type in runtime_only_rows
)

_data_to_return = _apply_callback_role_gate(_data_to_return, is_full_admin)

# Check if slack alerting is on
Expand Down
2 changes: 1 addition & 1 deletion tests/proxy_unit_tests/test_proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2920,7 +2920,7 @@ async def test_get_config_callbacks_with_all_types(client_no_auth):
assert result["status"] == "success"
assert "callbacks" in result

callbacks = result["callbacks"]
callbacks = [cb for cb in result["callbacks"] if not cb.get("read_only", False)]

# Verify we have all 5 callbacks (2 success + 1 failure + 2 success_and_failure)
assert len(callbacks) == 5
Expand Down
Loading
Loading