diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py
index 72277cd0be9..53fdbcedaca 100644
--- a/litellm/proxy/litellm_pre_call_utils.py
+++ b/litellm/proxy/litellm_pre_call_utils.py
@@ -650,7 +650,14 @@ async def _resolve_logging_exporters(
if info is None or info.credential_type != "logging":
return False
if info.auto_enable:
- return True
+ # auto_enable is scoped by access: if access has explicit grants,
+ # the request identity must fall within them. Empty access = proxy-wide.
+ from litellm.proxy.management_endpoints.logging_exporter_access import (
+ _has_explicit_access_grants,
+ )
+ if _has_explicit_access_grants(info.access):
+ return access_grants(info.access, team_ids, org_ids)
+ return True # no grants = proxy-wide auto
if credential.credential_name not in names:
return False
return access_grants(info.access, team_ids, org_ids)
diff --git a/litellm/proxy/management_endpoints/logging_exporter_access.py b/litellm/proxy/management_endpoints/logging_exporter_access.py
index f542a10a89e..4514385bb60 100644
--- a/litellm/proxy/management_endpoints/logging_exporter_access.py
+++ b/litellm/proxy/management_endpoints/logging_exporter_access.py
@@ -68,12 +68,40 @@ def access_grants(
return not org_ids.isdisjoint(access.orgs)
+def _has_explicit_access_grants(access: Optional[CredentialAccess]) -> bool:
+ """True when ``access`` contains at least one explicit grant (global, team, or org).
+
+ Used to distinguish "access intentionally left empty" (proxy-wide fallback) from
+ "access scoped to specific teams or orgs".
+ """
+ if access is None:
+ return False
+ return access.global_ or bool(access.teams) or bool(access.orgs)
+
+
def is_destination_visible(
info: CredentialInfo,
team_ids: frozenset[str],
org_ids: frozenset[str],
) -> bool:
"""Whether a caller admin-scoped to ``team_ids`` / ``org_ids`` may see and assign
- this destination: an auto-enabled default, or a grant that reaches their scope.
+ this destination.
+
+ ``auto_enable`` is scoped by ``access``:
+ - If ``access`` has explicit grants (global / teams / orgs), the caller must
+ fall within those grants — even for auto-enabled destinations.
+ - If ``access`` is empty (no grants at all), the destination is treated as
+ proxy-wide and is visible to every admin caller. This preserves backward
+ compatibility for ``auto_enable=True`` destinations created without an
+ ``access`` block.
+
+ A destination with ``auto_enable=False`` follows the same access check; the
+ only difference is that ``auto_enable=True`` without any explicit grants is
+ visible to all admins, while ``auto_enable=False`` without grants is visible
+ to nobody.
"""
- return info.auto_enable or access_grants(info.access, team_ids, org_ids)
+ if _has_explicit_access_grants(info.access):
+ return access_grants(info.access, team_ids, org_ids)
+ # No explicit grants: auto_enable=True → proxy-wide (visible to all admins);
+ # auto_enable=False → invisible (no grants = not reachable by any non-admin).
+ return info.auto_enable
diff --git a/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py b/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py
index bcb357a0753..8ad30a3b9df 100644
--- a/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py
+++ b/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py
@@ -99,12 +99,44 @@ def test_access_grants_not_global_when_false():
assert access_grants(a, frozenset({"t1"}), frozenset({"o1"})) is False
-# --- is_destination_visible: auto_enable OR grant --------------------------
+# --- is_destination_visible: auto_enable scoped by access ------------------
+#
+# auto_enable=True means "selected automatically" but the scope of that
+# automatic selection is controlled by access:
+# - explicit grants (global/teams/orgs) → caller must be within the grant
+# - empty access (no grants at all) → proxy-wide fallback (visible to all)
+# This lets admins create a team-scoped auto-exporter without it leaking to
+# every other team on the proxy.
-def test_visible_auto_enable_ignores_access():
+def test_visible_auto_enable_empty_access_is_proxy_wide():
+ """auto_enable=True with no access grants is proxy-wide: visible to all admins."""
info = CredentialInfo(credential_type="logging", auto_enable=True)
assert is_destination_visible(info, frozenset(), frozenset()) is True
+ assert is_destination_visible(info, frozenset({"any-team"}), frozenset()) is True
+ assert is_destination_visible(info, frozenset(), frozenset({"any-org"})) is True
+
+
+def test_visible_auto_enable_global_access_is_proxy_wide():
+ """auto_enable=True + access.global=True is proxy-wide."""
+ info = CredentialInfo(credential_type="logging", auto_enable=True, access=_access(global_=True))
+ assert is_destination_visible(info, frozenset({"t1"}), frozenset()) is True
+ assert is_destination_visible(info, frozenset(), frozenset()) is True
+
+
+def test_visible_auto_enable_team_scoped():
+ """auto_enable=True + access.teams=[t1] is visible only to t1 admins."""
+ info = CredentialInfo(credential_type="logging", auto_enable=True, access=_access(teams=["t1"]))
+ assert is_destination_visible(info, frozenset({"t1"}), frozenset()) is True
+ assert is_destination_visible(info, frozenset({"t2"}), frozenset()) is False
+ assert is_destination_visible(info, frozenset(), frozenset()) is False
+
+
+def test_visible_auto_enable_org_scoped():
+ """auto_enable=True + access.orgs=[o1] is visible only to o1 admins."""
+ info = CredentialInfo(credential_type="logging", auto_enable=True, access=_access(orgs=["o1"]))
+ assert is_destination_visible(info, frozenset(), frozenset({"o1"})) is True
+ assert is_destination_visible(info, frozenset(), frozenset({"o2"})) is False
def test_visible_delegates_to_access_when_not_auto_enable():
diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx
index 82555ce8929..b817cd2ade8 100644
--- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx
@@ -1,6 +1,6 @@
import { Button } from "@tremor/react";
import type { TableProps } from "antd";
-import { Table, Tag } from "antd";
+import { Table, Tag, Tooltip } from "antd";
import Title from "antd/es/typography/Title";
import React from "react";
import TableIconActionButton from "../../../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
@@ -27,17 +27,23 @@ const isDestination = (record: AlertingObject): boolean => record.credentialName
const SCOPE_BADGES_LIMIT = 4;
-// Renders the union of identities that route to this destination, with each team/org
-// labeled by its alias. Global supersedes everything. Pulls from record.resolvedScope
-// (computed at the page level from BOTH directions: destination-side access AND
-// identity-side metadata.logging_exporters).
+// Renders the explicit access grants for a destination.
+//
+// The Scope column shows only what is statically configured in credential_info.access.
+// It does NOT reflect runtime enablement behavior (auto_enable) — that is the
+// Mode column's job. This keeps the two concepts cleanly separated:
+// - access.global=true → "Global access" (visible/assignable by all)
+// - access.teams=[...] → per-team badges
+// - access.orgs=[...] → per-org badges
+// - empty/absent access → "—" in all cases, including auto_enable=true
const ScopeCell: React.FC<{ record: AlertingObject }> = ({ record }) => {
const scope = record.resolvedScope;
+
if (!scope || (!scope.global && scope.teams.length === 0 && scope.orgs.length === 0)) {
return —;
}
if (scope.global) {
- return Global;
+ return Global access;
}
const items = [
...scope.teams.map((label) => ({ kind: "team" as const, label })),
@@ -97,9 +103,27 @@ export const LoggingCallbacksTable: React.FC = ({
title: Mode,
key: "mode",
render: (_: unknown, record: CallbackRow) => {
- // Destination rows fan out on every span, so the success/failure split
- // does not apply -- only config callbacks carry a mode.
- if (isDestination(record)) return —;
+ // Destination rows show their enablement behaviour: auto_enable=true
+ // means the destination exports automatically (scoped by access grants);
+ // false means it only exports when explicitly named in logging_exporters.
+ if (isDestination(record)) {
+ if (record.autoEnable === true) {
+ const access = record.access;
+ const hasExplicitGrants =
+ access?.global === true ||
+ (Array.isArray(access?.teams) && access.teams.length > 0) ||
+ (Array.isArray(access?.orgs) && access.orgs.length > 0);
+ const tooltipTitle = hasExplicitGrants
+ ? "Exports automatically for all identities within the access scope without requiring explicit assignment."
+ : "No explicit access grants. Treated as proxy-wide automatic export for backward compatibility. Add access.global=true or access.teams/orgs to scope this destination.";
+ return (
+
+ Auto-enabled
+
+ );
+ }
+ return Manual assignment;
+ }
// Backend sends `type` (success | failure); legacy in-memory rows
// from add-callback flow set `mode`. Read both so newly-added rows
// and server-fetched rows both render correctly.
diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts
index 3bf7b09e47c..006b9ebe5c4 100644
--- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts
+++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/types.ts
@@ -14,6 +14,10 @@ export interface AlertingObject {
credentialName?: string;
destinationLabel?: string;
access?: CredentialAccess;
+ // True when credential_info.auto_enable=true: destination exports on every
+ // request without needing explicit key/team/org assignment. Distinct from
+ // access.global (which controls visibility/assignability, not routing).
+ autoEnable?: boolean;
// The union of identities that route to this destination, resolved at render
// time from both directions (destination-side credential_info.access AND
// identity-side metadata.logging_exporters). Display labels only -- ids are
diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx
index f5508ac702b..ebbd622b8ef 100644
--- a/ui/litellm-dashboard/src/components/settings.tsx
+++ b/ui/litellm-dashboard/src/components/settings.tsx
@@ -320,6 +320,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
? `${backendLabel(c.credential_info?.description)} · ${c.credential_info.host}`
: backendLabel(c.credential_info?.description),
access: c.credential_info?.access,
+ autoEnable: c.credential_info?.auto_enable === true,
resolvedScope: resolveScope(c.credential_name, c.credential_info?.access),
}));