style: black + prettier formatting and rename shadowed _processor_for import

CI uses Black (not ruff format) and the changed-files frontend lint runs
prettier; apply both. Also alias the providers._processor_for import inside
TenantFanOutSpanProcessor._processor_for so the recursive-function detector
no longer false-positives on the same-name call.
This commit is contained in:
yucheng-berriai 2026-06-23 17:44:24 -07:00
parent 9af9714ed3
commit 8062bdf389
10 changed files with 152 additions and 69 deletions

View file

@ -112,15 +112,17 @@ class OpenTelemetryV2(CustomLogger):
self._tracer_provider: TracerProvider = (
tracer_provider
if tracer_provider is not None
else build_tracer_provider(
self.config, tenant_fan_out_owner=callback_name
)
else build_tracer_provider(self.config, tenant_fan_out_owner=callback_name)
)
self.tracer: Tracer = get_tracer(self._tracer_provider, LITELLM_TRACER_NAME)
self._metrics_recorder = self._init_metrics(meter_provider)
self._metric_filter_error_logged = False
self._emitter = SpanEmitter(self.tracer, self.config, mappers=resolve_mappers(self.config.mapper_names))
self._tenant_tracers = TenantTracerCache(self.config, callback_name, LITELLM_TRACER_NAME)
self._emitter = SpanEmitter(
self.tracer, self.config, mappers=resolve_mappers(self.config.mapper_names)
)
self._tenant_tracers = TenantTracerCache(
self.config, callback_name, LITELLM_TRACER_NAME
)
self._open_llm_calls: "OrderedDict[str, _LLMCallSpan]" = OrderedDict()
self._init_otel_logger_on_litellm_proxy()
@ -144,7 +146,9 @@ class OpenTelemetryV2(CustomLogger):
def _register_in_callback_list(self, callbacks: list) -> None:
already_otel = any(
cb.__class__.__module__.startswith(_OTEL_MODULES) for cb in callbacks if hasattr(cb, "__class__")
cb.__class__.__module__.startswith(_OTEL_MODULES)
for cb in callbacks
if hasattr(cb, "__class__")
)
if not already_otel:
callbacks.append(self)
@ -171,7 +175,9 @@ class OpenTelemetryV2(CustomLogger):
each logger exports only the destinations tagged with its own callback_name,
so each backend's span keeps its own attribute vocabulary.
"""
return tuple(d for d in call.otel_destinations if d.callback_name == self.callback_name)
return tuple(
d for d in call.otel_destinations if d.callback_name == self.callback_name
)
# ====================================================================== #
# LLM-call callbacks — the span is opened at the ``pre_call`` boundary and
@ -220,9 +226,13 @@ class OpenTelemetryV2(CustomLogger):
call.provisional_span_name,
parent_context=parent_context,
start_time_ns=start_time_ns,
tracer=self._tenant_tracers.tracer_for(self.tracer, self._destinations_for_backend(call)),
tracer=self._tenant_tracers.tracer_for(
self.tracer, self._destinations_for_backend(call)
),
)
self._open_llm_calls[call_id] = _LLMCallSpan(span=span, start_time_ns=start_time_ns)
self._open_llm_calls[call_id] = _LLMCallSpan(
span=span, start_time_ns=start_time_ns
)
# Evict the oldest open call if the map is over budget. A call that opens
# but never closes (a stream that only fires stream events) would linger
# otherwise; the evicted span is simply dropped (never exported).
@ -274,7 +284,9 @@ class OpenTelemetryV2(CustomLogger):
no boundary to open it at), deduped on the call id by the emitter.
"""
raw_payload = kwargs.get("standard_logging_object")
if not raw_payload or not is_mcp_tool_call(cast(Mapping[str, object], raw_payload)):
if not raw_payload or not is_mcp_tool_call(
cast(Mapping[str, object], raw_payload)
):
return False
payload = cast("StandardLoggingPayload", raw_payload)
data = MCPToolCallSpanData.from_standard_logging_payload(
@ -321,7 +333,9 @@ class OpenTelemetryV2(CustomLogger):
destinations = self._destinations_for_backend(call)
if payload is None or not destinations:
return None
return self._emit_deferred_llm_call(payload, destinations, to_ns(start_time), to_ns(end_time))
return self._emit_deferred_llm_call(
payload, destinations, to_ns(start_time), to_ns(end_time)
)
end_time_ns = to_ns(end_time)
if payload is None:
@ -329,9 +343,13 @@ class OpenTelemetryV2(CustomLogger):
carrier.span.end(end_time=end_time_ns)
return None
data = LLMCallSpanData.from_standard_logging_payload(payload, capture_content=self.config.capture_span_content)
data = LLMCallSpanData.from_standard_logging_payload(
payload, capture_content=self.config.capture_span_content
)
if carrier.span is not None:
self._emitter.finish_span(SpanRole.LLM_CALL, carrier.span, data, end_time_ns=end_time_ns)
self._emitter.finish_span(
SpanRole.LLM_CALL, carrier.span, data, end_time_ns=end_time_ns
)
return carrier.span
return self._emit_deferred_llm_call(
payload,
@ -355,7 +373,9 @@ class OpenTelemetryV2(CustomLogger):
Both anchor to the request's root span via the worker-copied context and
seed identity Baggage so the span is labeled consistently.
"""
data = LLMCallSpanData.from_standard_logging_payload(payload, capture_content=self.config.capture_span_content)
data = LLMCallSpanData.from_standard_logging_payload(
payload, capture_content=self.config.capture_span_content
)
base_ctx = resolve_request_span_context()
bag = promoted_baggage(
data.identity,
@ -437,7 +457,12 @@ class OpenTelemetryV2(CustomLogger):
# zero-duration root with no context, so skip it. Real background work
# (budget/reset jobs, spend flush) passes start/end times and still emits
# as a root; anything with a parent emits regardless.
if error_override is None and start_time is None and end_time is None and parent_otel_span is None:
if (
error_override is None
and start_time is None
and end_time is None
and parent_otel_span is None
):
return None
if error_override is not None and data.error is None:
data = ServiceSpanData(
@ -583,7 +608,9 @@ def select_global_otel_v2_logger(
"""
if registered is not None:
return registered
existing = next((cb for cb in in_memory_loggers if isinstance(cb, OpenTelemetryV2)), None)
existing = next(
(cb for cb in in_memory_loggers if isinstance(cb, OpenTelemetryV2)), None
)
return existing if existing is not None else OpenTelemetryV2()

View file

@ -27,7 +27,9 @@ _PROPAGATOR = TraceContextTextMapPropagator()
# and is inherited by ``asyncio.create_task`` children — i.e. the async logging
# callbacks that close the span. It is never reset: the contextvar dies with the
# request task, so there is nothing to leak.
_request_root_span: "ContextVar[Span | None]" = ContextVar("litellm_otel_request_root_span", default=None)
_request_root_span: "ContextVar[Span | None]" = ContextVar(
"litellm_otel_request_root_span", default=None
)
# Per-request admin-resolved destinations. Set once at the auth boundary (the
# earliest point a request's identity is known) and read by the global-provider
@ -38,7 +40,9 @@ _request_root_span: "ContextVar[Span | None]" = ContextVar("litellm_otel_request
# ``asyncio.create_task`` children (the success/failure logging callbacks close
# the LLM span in a worker copied from the request context). Request-scoped: the
# contextvar dies with the request task, so nothing leaks across requests.
_request_destinations: "ContextVar[tuple]" = ContextVar("litellm_otel_request_destinations", default=())
_request_destinations: "ContextVar[tuple]" = ContextVar(
"litellm_otel_request_destinations", default=()
)
def set_request_destinations(destinations: tuple) -> None:
@ -68,7 +72,9 @@ def request_root_span() -> "Span | None":
return span if is_recordable_span(span) else None
def set_request_baggage(values: Mapping[str, str], context: Context | None = None) -> Context:
def set_request_baggage(
values: Mapping[str, str], context: Context | None = None
) -> Context:
"""Return a context with ``values`` written into Baggage."""
ctx = context
for key, value in values.items():

View file

@ -37,8 +37,8 @@ if TYPE_CHECKING:
# Bound on cached per-destination processors. One processor per
# ``(endpoint, sorted(headers))`` pair, so the working set is one entry per
# admin-resolved tenant credential -- a real-world deployment with hundreds of
# tenants stays well under this. The LRU shuts down the evicted processor's
# exporter thread so the working set is reclaimed.
# tenants stays well under this. Evicted entries are dropped (not shut down; see
# the eviction site) and reclaimed at process exit.
_MAX_CACHED_PROCESSORS = 256
@ -100,7 +100,9 @@ class TenantFanOutSpanProcessor(SpanProcessor):
try:
processor.shutdown()
except Exception as exc:
verbose_logger.debug("OTel V2 fan-out: processor shutdown failed: %s", exc)
verbose_logger.debug(
"OTel V2 fan-out: processor shutdown failed: %s", exc
)
self._processors.clear()
def force_flush(self, timeout_millis: int = 30000) -> bool:
@ -121,7 +123,7 @@ class TenantFanOutSpanProcessor(SpanProcessor):
return cached
from litellm.integrations.otel.plumbing.providers import (
_exporter_from_spec,
_processor_for,
_processor_for as _build_processor,
default_otlp_kind_for_backend,
)
@ -133,7 +135,7 @@ class TenantFanOutSpanProcessor(SpanProcessor):
owner=None,
)
exporter = _exporter_from_spec(spec)
processor = _processor_for(exporter, use_simple=False)
processor = _build_processor(exporter, use_simple=False)
except Exception as exc:
verbose_logger.debug(
"OTel V2 fan-out: failed to build processor for %s: %s",
@ -177,7 +179,9 @@ def _destination_resource_attrs(destination: "OtelDestination") -> dict[str, str
return {}
def _with_destination_resource(span: ReadableSpan, destination: "OtelDestination") -> ReadableSpan:
def _with_destination_resource(
span: ReadableSpan, destination: "OtelDestination"
) -> ReadableSpan:
"""Return ``span`` with its Resource augmented by the destination's required
attributes. The original span object is left untouched; a shallow wrapper
reuses every other field and only swaps the ``resource`` property."""

View file

@ -53,7 +53,9 @@ def to_otel_span_kind(kind: LiteLLMSpanKind) -> SpanKind:
_EXPORTER_FACTORIES: dict[str, Callable[[ExporterSpec], SpanExporter]] = {}
def register_exporter_factory(kind: str, factory: Callable[[ExporterSpec], SpanExporter]) -> None:
def register_exporter_factory(
kind: str, factory: Callable[[ExporterSpec], SpanExporter]
) -> None:
"""Register a custom exporter ``factory`` for the exporter ``kind``."""
_EXPORTER_FACTORIES[kind.lower()] = factory
@ -70,7 +72,9 @@ class LiteLLMBaggageSpanProcessor(SpanProcessor):
self._allowed_prefixes = tuple(allowed_prefixes)
def _is_allowed(self, key: str) -> bool:
return key in self._allowed_keys or any(key.startswith(prefix) for prefix in self._allowed_prefixes)
return key in self._allowed_keys or any(
key.startswith(prefix) for prefix in self._allowed_prefixes
)
def on_start(self, span: Span, parent_context: Context | None = None) -> None:
for key, value in baggage.get_all(parent_context).items():
@ -164,7 +168,11 @@ def build_span_exporter(config: OpenTelemetryV2Config) -> SpanExporter:
``exporter`` / ``endpoint`` / ``headers`` fields. To configure multiple
exporters, populate ``config.exporters`` directly.
"""
return _exporter_from_spec(ExporterSpec(kind=config.exporter, endpoint=config.endpoint, headers=config.headers))
return _exporter_from_spec(
ExporterSpec(
kind=config.exporter, endpoint=config.endpoint, headers=config.headers
)
)
def _otlp_metrics_endpoint(endpoint: str | None) -> str | None:
@ -313,7 +321,9 @@ def build_tracer_provider(
"""
provider = TracerProvider(resource=build_resource(config))
if baggage_processor is None:
baggage_processor = LiteLLMBaggageSpanProcessor(allowed_keys=config.baggage_promoted_keys)
baggage_processor = LiteLLMBaggageSpanProcessor(
allowed_keys=config.baggage_promoted_keys
)
provider.add_span_processor(baggage_processor)
if tenant_fan_out_owner is not None:
@ -321,7 +331,9 @@ def build_tracer_provider(
TenantFanOutSpanProcessor,
)
provider.add_span_processor(TenantFanOutSpanProcessor(owner_callback_name=tenant_fan_out_owner))
provider.add_span_processor(
TenantFanOutSpanProcessor(owner_callback_name=tenant_fan_out_owner)
)
if exporter is not None:
provider.add_span_processor(_processor_for(exporter, use_simple_processor))
@ -334,7 +346,11 @@ def build_tracer_provider(
provider.add_span_processor(
_processor_for(
exp,
(spec.use_simple_processor if spec.use_simple_processor is not None else use_simple_processor),
(
spec.use_simple_processor
if spec.use_simple_processor is not None
else use_simple_processor
),
)
)
return provider

View file

@ -34,12 +34,18 @@ def _require_proxy_admin(user_api_key_dict: UserAPIKeyAuth) -> None:
def _credential_in_memory(credential_name: str) -> Optional[CredentialItem]:
return next(
(cred for cred in litellm.credential_list if cred.credential_name == credential_name),
(
cred
for cred in litellm.credential_list
if cred.credential_name == credential_name
),
None,
)
async def _credential_for_admin_gate(credential_name: str, prisma_client: object) -> Optional[CredentialItem]:
async def _credential_for_admin_gate(
credential_name: str, prisma_client: object
) -> Optional[CredentialItem]:
"""Authoritative credential lookup for the admin gate on update/delete.
The in-process ``litellm.credential_list`` can be stale: a credential created
@ -70,7 +76,9 @@ class CredentialHelperUtils:
"""Encrypt values in credential.credential_values and add to DB"""
encrypted_credential_values = {}
for key, value in (credential.credential_values or {}).items():
encrypted_credential_values[key] = encrypt_value_helper(value, new_encryption_key)
encrypted_credential_values[key] = encrypt_value_helper(
value, new_encryption_key
)
# Return a new object to avoid mutating the caller's credential, which
# is kept in memory and should remain unencrypted.
@ -119,7 +127,9 @@ async def create_credential(
model = llm_router.get_deployment(credential.model_id)
if model is None:
raise HTTPException(status_code=404, detail="Model not found")
credential_values = llm_router.get_deployment_credentials(credential.model_id)
credential_values = llm_router.get_deployment_credentials(
credential.model_id
)
if credential_values is None:
raise HTTPException(status_code=404, detail="Model not found")
credential.credential_values = credential_values
@ -134,7 +144,9 @@ async def create_credential(
credential_values=credential.credential_values,
credential_info=credential.credential_info,
)
encrypted_credential = CredentialHelperUtils.encrypt_credential_values(processed_credential)
encrypted_credential = CredentialHelperUtils.encrypt_credential_values(
processed_credential
)
credentials_dict = encrypted_credential.model_dump()
credentials_dict_jsonified = jsonify_object(credentials_dict)
await CredentialsRepository(prisma_client).create(
@ -190,7 +202,9 @@ async def get_credentials(
async def get_credential_by_name(
request: Request,
fastapi_response: Response,
credential_name: str = Path(..., description="The credential name, percent-decoded; may contain slashes"),
credential_name: str = Path(
..., description="The credential name, percent-decoded; may contain slashes"
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
@ -268,7 +282,9 @@ async def get_credential_by_model(
async def delete_credential(
request: Request,
fastapi_response: Response,
credential_name: str = Path(..., description="The credential name, percent-decoded; may contain slashes"),
credential_name: str = Path(
..., description="The credential name, percent-decoded; may contain slashes"
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
@ -277,7 +293,9 @@ async def delete_credential(
from litellm.proxy.proxy_server import prisma_client
existing = await _credential_for_admin_gate(credential_name, prisma_client)
if existing is not None and is_admin_gated_credential_info(existing.credential_info):
if existing is not None and is_admin_gated_credential_info(
existing.credential_info
):
_require_proxy_admin(user_api_key_dict)
try:
@ -289,7 +307,11 @@ async def delete_credential(
await CredentialsRepository(prisma_client).delete_by_name(credential_name)
## DELETE FROM LITELLM ##
litellm.credential_list = [cred for cred in litellm.credential_list if cred.credential_name != credential_name]
litellm.credential_list = [
cred
for cred in litellm.credential_list
if cred.credential_name != credential_name
]
return {"success": True, "message": "Credential deleted successfully"}
except Exception as e:
return handle_exception_on_proxy(e)
@ -320,7 +342,9 @@ def update_db_credential(
# update litellm params
if encrypted_credential.credential_values:
# Encrypt any sensitive values
encrypted_params = {k: v for k, v in encrypted_credential.credential_values.items()}
encrypted_params = {
k: v for k, v in encrypted_credential.credential_values.items()
}
merged_credential.credential_values.update(encrypted_params)
@ -345,7 +369,9 @@ async def update_credential(
request: Request,
fastapi_response: Response,
credential: CredentialItem,
credential_name: str = Path(..., description="The credential name, percent-decoded; may contain slashes"),
credential_name: str = Path(
..., description="The credential name, percent-decoded; may contain slashes"
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
@ -355,7 +381,8 @@ async def update_credential(
existing = await _credential_for_admin_gate(credential_name, prisma_client)
if is_admin_gated_credential_info(credential.credential_info) or (
existing is not None and is_admin_gated_credential_info(existing.credential_info)
existing is not None
and is_admin_gated_credential_info(existing.credential_info)
):
_require_proxy_admin(user_api_key_dict)
validate_credential_access(credential.credential_info)
@ -402,7 +429,11 @@ async def update_credential(
)
# Remove old entry if renamed, then use upsert_credentials to handle duplicates
if new_name != credential_name:
litellm.credential_list = [c for c in litellm.credential_list if c.credential_name != credential_name]
litellm.credential_list = [
c
for c in litellm.credential_list
if c.credential_name != credential_name
]
CredentialAccessor.upsert_credentials([updated_in_memory])
return {"success": True, "message": "Credential updated successfully"}

View file

@ -44,7 +44,9 @@ def _connected_db(monkeypatch):
repo.create = AsyncMock()
repo.delete_by_name = AsyncMock()
monkeypatch.setattr(endpoints, "CredentialsRepository", lambda _client: repo)
monkeypatch.setattr(endpoints.CredentialAccessor, "upsert_credentials", lambda creds: None)
monkeypatch.setattr(
endpoints.CredentialAccessor, "upsert_credentials", lambda creds: None
)
return repo
@ -115,7 +117,9 @@ async def test_update_logging_credential_forbidden_for_non_admin(_connected_db):
@pytest.mark.asyncio
async def test_update_existing_logging_credential_forbidden_even_without_logging_patch(_connected_db, monkeypatch):
async def test_update_existing_logging_credential_forbidden_even_without_logging_patch(
_connected_db, monkeypatch
):
"""A non-admin cannot edit a stored logging credential's values, even with a patch
that omits credential_info (the gate consults the in-memory credential too)."""
monkeypatch.setattr(
@ -176,7 +180,9 @@ def test_update_db_credential_preserves_existing_info_on_partial_patch():
@pytest.mark.asyncio
async def test_delete_logging_credential_forbidden_for_non_admin(_connected_db, monkeypatch):
async def test_delete_logging_credential_forbidden_for_non_admin(
_connected_db, monkeypatch
):
monkeypatch.setattr(
litellm,
"credential_list",
@ -200,7 +206,9 @@ async def test_delete_logging_credential_forbidden_for_non_admin(_connected_db,
@pytest.mark.asyncio
async def test_update_db_only_logging_credential_forbidden_for_non_admin(_connected_db, monkeypatch):
async def test_update_db_only_logging_credential_forbidden_for_non_admin(
_connected_db, monkeypatch
):
"""A logging credential that exists ONLY in the DB (not resident in the
in-memory ``credential_list`` -- e.g. created on another scaled instance or
before a restart) must still gate a non-admin update. The gate falls back to
@ -230,7 +238,9 @@ async def test_update_db_only_logging_credential_forbidden_for_non_admin(_connec
@pytest.mark.asyncio
async def test_delete_db_only_logging_credential_forbidden_for_non_admin(_connected_db, monkeypatch):
async def test_delete_db_only_logging_credential_forbidden_for_non_admin(
_connected_db, monkeypatch
):
"""Same DB-only fallback for delete: a non-admin can't delete a logging
credential that is resident only in the DB."""
monkeypatch.setattr(litellm, "credential_list", [])

View file

@ -157,9 +157,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
},
metadata: {
...(values.metadata ? JSON.parse(values.metadata) : {}),
...(values.logging_exporters !== undefined
? { logging_exporters: values.logging_exporters }
: {}),
...(values.logging_exporters !== undefined ? { logging_exporters: values.logging_exporters } : {}),
},
};

View file

@ -39,11 +39,7 @@ import {
setCallbacksCall,
} from "./networking";
import { LoggingCallbacksTable } from "./Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable";
import {
AlertingObject,
CredentialAccess,
ResolvedScope,
} from "./Settings/LoggingAndAlerts/LoggingCallbacks/types";
import { AlertingObject, CredentialAccess, ResolvedScope } from "./Settings/LoggingAndAlerts/LoggingCallbacks/types";
import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
@ -273,8 +269,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
// access for the destination branch of the unified Add modal
const [addAccess, setAddAccess] = useState<CredentialAccess>({});
const addingDestination = selectedCallback != null && LOGGING_BACKEND_IDS.has(selectedCallback);
const addingDestinationFields =
LOGGING_DESTINATION_BACKENDS.find((b) => b.id === selectedCallback)?.fields ?? [];
const addingDestinationFields = LOGGING_DESTINATION_BACKENDS.find((b) => b.id === selectedCallback)?.fields ?? [];
const teamAlias = (id: string): string => {
const t = (teamsData ?? []).find((team) => team.team_id === id);
@ -876,7 +871,9 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
key={f.name}
label={<span className="text-sm font-medium text-gray-700">{f.label}</span>}
name={f.name}
rules={f.optional ? undefined : [{ required: true, message: `Please enter the ${f.label.toLowerCase()}` }]}
rules={
f.optional ? undefined : [{ required: true, message: `Please enter the ${f.label.toLowerCase()}` }]
}
>
{f.type === "password" ? <Input.Password size="large" /> : <Input size="large" />}
</FormItem>

View file

@ -551,9 +551,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
guardrails: (values.guardrails || []).filter((n: string) => !globalGuardrailNames.has(n)),
opted_out_global_guardrails: optedOutGlobalGuardrails,
...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}),
...(values.logging_exporters !== undefined
? { logging_exporters: values.logging_exporters }
: {}),
...(values.logging_exporters !== undefined ? { logging_exporters: values.logging_exporters } : {}),
disable_global_guardrails: killSwitchOnAtSave,
soft_budget_alerting_emails:
typeof values.soft_budget_alerting_emails === "string"

View file

@ -278,9 +278,7 @@ export default function KeyInfoView({
...(Array.isArray(formValues.logging_settings) && formValues.logging_settings.length > 0
? { logging: formValues.logging_settings }
: {}),
...(formValues.logging_exporters !== undefined
? { logging_exporters: formValues.logging_exporters }
: {}),
...(formValues.logging_exporters !== undefined ? { logging_exporters: formValues.logging_exporters } : {}),
...(formValues.disabled_callbacks?.length > 0
? {
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
@ -302,9 +300,7 @@ export default function KeyInfoView({
...(Array.isArray(formValues.logging_settings) && formValues.logging_settings.length > 0
? { logging: formValues.logging_settings }
: {}),
...(formValues.logging_exporters !== undefined
? { logging_exporters: formValues.logging_exporters }
: {}),
...(formValues.logging_exporters !== undefined ? { logging_exporters: formValues.logging_exporters } : {}),
...(formValues.disabled_callbacks?.length > 0
? {
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),