diff --git a/litellm/integrations/otel/logger.py b/litellm/integrations/otel/logger.py index 6461abaa9eb..82e4789d86b 100644 --- a/litellm/integrations/otel/logger.py +++ b/litellm/integrations/otel/logger.py @@ -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() diff --git a/litellm/integrations/otel/plumbing/context.py b/litellm/integrations/otel/plumbing/context.py index d261244ad79..15d596058b3 100644 --- a/litellm/integrations/otel/plumbing/context.py +++ b/litellm/integrations/otel/plumbing/context.py @@ -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(): diff --git a/litellm/integrations/otel/plumbing/fan_out.py b/litellm/integrations/otel/plumbing/fan_out.py index eaf12bc0790..a1e6b060be1 100644 --- a/litellm/integrations/otel/plumbing/fan_out.py +++ b/litellm/integrations/otel/plumbing/fan_out.py @@ -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.""" diff --git a/litellm/integrations/otel/plumbing/providers.py b/litellm/integrations/otel/plumbing/providers.py index 39601f9f161..2510f3d3193 100644 --- a/litellm/integrations/otel/plumbing/providers.py +++ b/litellm/integrations/otel/plumbing/providers.py @@ -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 diff --git a/litellm/proxy/credential_endpoints/endpoints.py b/litellm/proxy/credential_endpoints/endpoints.py index 5b359ba2eba..fca16ad982a 100644 --- a/litellm/proxy/credential_endpoints/endpoints.py +++ b/litellm/proxy/credential_endpoints/endpoints.py @@ -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"} diff --git a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py index fab16eba42a..f977be5a343 100644 --- a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py @@ -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", []) diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx index a02acbe07da..99dc0923be5 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -157,9 +157,7 @@ const OrganizationInfoView: React.FC = ({ }, 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 } : {}), }, }; diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 32c4c10db34..6ec3d2b2f08 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -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 = ({ accessToken, userRole, userID, // access for the destination branch of the unified Add modal const [addAccess, setAddAccess] = useState({}); 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 = ({ accessToken, userRole, userID, key={f.name} label={{f.label}} 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" ? : } diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index c805cbf4c17..8c8db3d1441 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -551,9 +551,7 @@ const TeamInfoView: React.FC = ({ 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" diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index c3d080b5ce1..d962433bb69 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -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),