patch bundle fixes

This commit is contained in:
Alejandro Tapia 2026-03-03 15:38:43 -08:00
parent 80d96628b4
commit 04bbb549f2
5 changed files with 106 additions and 43 deletions

View file

@ -2296,14 +2296,12 @@ class UserAPIKeyAuth(
# If values is already an instance (not a dict), return it as-is
if not isinstance(values, dict):
return values
if values.get("api_key") is not None:
values.update(
{"token": cls._safe_hash_litellm_api_key(values.get("api_key"))}
)
if isinstance(values.get("api_key"), str):
values.update(
{"api_key": cls._safe_hash_litellm_api_key(values.get("api_key"))}
)
# api_key can be None (for team_id auth), or non-str from cache/JSON.
# Only hash when it's a str to avoid AttributeError in _safe_hash_litellm_api_key.
api_key = values.get("api_key")
if isinstance(api_key, str):
hashed = cls._safe_hash_litellm_api_key(api_key)
values.update({"token": hashed, "api_key": hashed})
return values
@classmethod

View file

@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Sequence, Union
import litellm
from litellm import get_secret
@ -586,6 +586,20 @@ def process_callback(
return result
def _dedupe_callbacks_preserve_dict_entries(
callbacks: Sequence[Any],
) -> List[Union[str, Dict[str, Any]]]:
"""Deduplicate callbacks by name. Safe for unhashable dicts."""
seen: set = set()
result: List[Union[str, Dict[str, Any]]] = []
for c in callbacks:
name = c if isinstance(c, str) else next(iter(c.keys()))
if name not in seen:
seen.add(name)
result.append(c)
return result
def normalize_callback_names(
callbacks: Iterable[Union[str, object]] | None,
) -> List[Union[str, object]]:

View file

@ -87,6 +87,7 @@ from litellm.proxy._types import (
UserAPIKeyAuth,
)
from litellm.proxy.common_utils.callback_utils import (
_dedupe_callbacks_preserve_dict_entries,
get_callbacks_from_callback_settings,
normalize_callback_names,
process_callback,
@ -2675,26 +2676,29 @@ class ProxyConfig:
DeprecationWarning,
stacklevel=2,
)
merged_success = list(
dict.fromkeys(success_from_settings + litellm_success)
merged_success = _dedupe_callbacks_preserve_dict_entries(
success_from_settings + litellm_success
)
for callback in merged_success:
callback_name = (
callback if isinstance(callback, str) else next(iter(callback.keys()))
)
# user passed custom_callbacks.async_on_succes_logger. They need us to import a function
if "." in callback:
if "." in callback_name:
litellm.logging_callback_manager.add_litellm_success_callback(
get_instance_fn(value=callback)
get_instance_fn(value=callback_name)
)
# these are litellm callbacks - "langfuse", "sentry", "wandb"
else:
resolved = (
litellm.logging_callback_manager._add_custom_callback_generic_api_str(
callback
callback_name
)
)
litellm.logging_callback_manager.add_litellm_success_callback(
resolved
)
if "prometheus" in callback:
if "prometheus" in callback_name:
from litellm.integrations.prometheus import (
PrometheusLogger,
)
@ -2717,20 +2721,23 @@ class ProxyConfig:
DeprecationWarning,
stacklevel=2,
)
merged_failure = list(
dict.fromkeys(failure_from_settings + litellm_failure)
merged_failure = _dedupe_callbacks_preserve_dict_entries(
failure_from_settings + litellm_failure
)
for callback in merged_failure:
callback_name = (
callback if isinstance(callback, str) else next(iter(callback.keys()))
)
# user passed custom_callbacks.async_on_succes_logger. They need us to import a function
if "." in callback:
if "." in callback_name:
litellm.logging_callback_manager.add_litellm_failure_callback(
get_instance_fn(value=callback)
get_instance_fn(value=callback_name)
)
# these are litellm callbacks - "langfuse", "sentry", "wandb"
else:
resolved = (
litellm.logging_callback_manager._add_custom_callback_generic_api_str(
callback
callback_name
)
)
litellm.logging_callback_manager.add_litellm_failure_callback(
@ -3614,11 +3621,11 @@ class ProxyConfig:
failure_callbacks = litellm_settings.get("failure_callback", None)
callbacks = litellm_settings.get("callbacks", None)
merged_success = list(
dict.fromkeys(success_from_settings + (success_callbacks or []))
merged_success = _dedupe_callbacks_preserve_dict_entries(
success_from_settings + (success_callbacks or [])
)
merged_failure = list(
dict.fromkeys(failure_from_settings + (failure_callbacks or []))
merged_failure = _dedupe_callbacks_preserve_dict_entries(
failure_from_settings + (failure_callbacks or [])
)
seen_both = {c if isinstance(c, str) else next(iter(c)) for c in callbacks_from_settings}
merged_callbacks: list = list(callbacks_from_settings)
@ -3629,15 +3636,25 @@ class ProxyConfig:
seen_both.add(name)
for success_callback in merged_success:
_name = (
success_callback
if isinstance(success_callback, str)
else next(iter(success_callback.keys()))
)
self._add_callback_from_db_to_in_memory_litellm_callbacks(
callback=success_callback,
callback=_name,
event_types=["success"],
existing_callbacks=litellm.success_callback,
)
for failure_callback in merged_failure:
_name = (
failure_callback
if isinstance(failure_callback, str)
else next(iter(failure_callback.keys()))
)
self._add_callback_from_db_to_in_memory_litellm_callbacks(
callback=failure_callback,
callback=_name,
event_types=["failure"],
existing_callbacks=litellm.failure_callback,
)
@ -11613,20 +11630,16 @@ async def get_config(): # noqa: PLR0915
return [val]
return list(val) if val else []
_success_callbacks = list(
dict.fromkeys(
_success_from_settings
+ normalize_callback_names(
_to_list(_litellm_settings.get("success_callback"))
)
_success_callbacks = _dedupe_callbacks_preserve_dict_entries(
_success_from_settings
+ normalize_callback_names(
_to_list(_litellm_settings.get("success_callback"))
)
)
_failure_callbacks = list(
dict.fromkeys(
_failure_from_settings
+ normalize_callback_names(
_to_list(_litellm_settings.get("failure_callback"))
)
_failure_callbacks = _dedupe_callbacks_preserve_dict_entries(
_failure_from_settings
+ normalize_callback_names(
_to_list(_litellm_settings.get("failure_callback"))
)
)
_litellm_callbacks = _litellm_settings.get("callbacks", []) or []
@ -11655,29 +11668,40 @@ async def get_config(): # noqa: PLR0915
"""
for _callback in _success_callbacks:
_name = (
_callback if isinstance(_callback, str) else next(iter(_callback.keys()))
)
_data_to_return.append(
process_callback(_callback, "success", environment_variables)
process_callback(_name, "success", environment_variables)
)
for _callback in _failure_callbacks:
_name = (
_callback if isinstance(_callback, str) else next(iter(_callback.keys()))
)
_data_to_return.append(
process_callback(_callback, "failure", environment_variables)
process_callback(_name, "failure", environment_variables)
)
for _callback in _success_and_failure_callbacks:
_callback_params = None
_callback_name: str
if isinstance(_callback, dict) and len(_callback) == 1:
(_callback_name, _callback_params), = _callback.items()
_callback = _callback_name
elif isinstance(_callback, str) and _callback in _callback_settings:
_callback_name = _callback
_raw = _callback_settings.get(_callback) or {}
_callback_params = (
{k: v for k, v in _raw.items() if k not in ("callback_type", "event_types")}
or None
)
else:
_callback_name = (
_callback if isinstance(_callback, str) else next(iter(_callback.keys()))
)
_data_to_return.append(
process_callback(
_callback,
_callback_name,
"success_and_failure",
environment_variables,
callback_params=_callback_params,

View file

@ -6,6 +6,7 @@ sys.path.insert(
) # Adds the parent directory to the system path
from litellm.proxy.common_utils.callback_utils import (
_dedupe_callbacks_preserve_dict_entries,
get_callbacks_from_callback_settings,
get_model_group_from_request_data,
get_remaining_tokens_and_requests_from_request_data,
@ -128,6 +129,27 @@ def test_normalize_callback_names_none_returns_empty_list():
assert normalize_callback_names([]) == []
def test_dedupe_callbacks_preserve_dict_entries_handles_dict_style():
"""dict.fromkeys would crash on unhashable dicts; _dedupe_callbacks_preserve_dict_entries is safe."""
callbacks = [
"langfuse",
{"websearch_interception": {"enabled_providers": ["bedrock"]}},
"langfuse",
"sentry",
]
result = _dedupe_callbacks_preserve_dict_entries(callbacks)
assert result == [
"langfuse",
{"websearch_interception": {"enabled_providers": ["bedrock"]}},
"sentry",
]
def test_dedupe_callbacks_preserve_dict_entries_strings_only():
"""Strings-only input dedupes correctly."""
assert _dedupe_callbacks_preserve_dict_entries(["a", "b", "a"]) == ["a", "b"]
def test_remaining_tokens_requests_returns_empty_when_metadata_empty_or_missing():
"""Empty or missing metadata returns {}."""
assert get_remaining_tokens_and_requests_from_request_data({}) == {}

View file

@ -272,6 +272,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
...normalized,
...paramFields,
callback: selectedEditCallback.name,
callback_type: selectedEditCallback.type ?? "success",
});
}
}, [showEditCallback, selectedEditCallback, editForm]);
@ -340,7 +341,8 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
setIsAddingCallback(true);
}
const payload = buildCallbackPayload(formValues, callbackName, callbacks, isEdit, selectedEditCallback?.type as "success" | "failure" | "success_and_failure" | undefined, callbackSettings);
const callbackType = (isEdit ? formValues.callback_type : undefined) ?? selectedEditCallback?.type as "success" | "failure" | "success_and_failure" | undefined;
const payload = buildCallbackPayload(formValues, callbackName, callbacks, isEdit, callbackType, callbackSettings);
try {
await setCallbacksCall(accessToken, payload);
@ -782,6 +784,9 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
>
{selectedEditCallback && (
<>
<FormItem name="callback_type" hidden>
<input type="hidden" />
</FormItem>
<CallbackSelector
callbackConfigs={callbackConfigs}
selectedCallback={selectedEditCallback.name}