diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260702120000_add_logging_exporters_columns/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260702120000_add_logging_exporters_columns/migration.sql index bed03a36455..e9f559aa83e 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260702120000_add_logging_exporters_columns/migration.sql +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260702120000_add_logging_exporters_columns/migration.sql @@ -4,11 +4,5 @@ ALTER TABLE "LiteLLM_TeamTable" ADD COLUMN IF NOT EXISTS "logging_exporters" TEX -- AlterTable ALTER TABLE "LiteLLM_DeletedTeamTable" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[]; --- AlterTable -ALTER TABLE "LiteLLM_VerificationToken" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[]; - --- AlterTable -ALTER TABLE "LiteLLM_DeletedVerificationToken" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[]; - -- AlterTable ALTER TABLE "LiteLLM_OrganizationTable" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[]; diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index ed4f7d4f0a0..60de5dc5470 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -445,7 +445,6 @@ model LiteLLM_VerificationToken { key_type String? policies String[] @default([]) access_group_ids String[] @default([]) - logging_exporters String[] @default([]) // admin-owned OTEL trace destinations assigned to this key (credential names) model_spend Json @default("{}") model_max_budget Json @default("{}") budget_fallbacks Json @default("{}") @@ -541,7 +540,6 @@ model LiteLLM_DeletedVerificationToken { key_type String? policies String[] @default([]) access_group_ids String[] @default([]) - logging_exporters String[] @default([]) model_spend Json @default("{}") model_max_budget Json @default("{}") budget_fallbacks Json @default("{}") diff --git a/litellm/models/verification_token.py b/litellm/models/verification_token.py index 86e9da613f5..519066b8266 100644 --- a/litellm/models/verification_token.py +++ b/litellm/models/verification_token.py @@ -5,7 +5,6 @@ Canonical definition for ``litellm_verificationtoken``. Re-exported from ``litellm.proxy._types`` for backwards compatibility. """ -from collections.abc import Sequence from datetime import datetime from typing import Dict, List, Optional, Union @@ -55,7 +54,6 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase): object_permission_id: Optional[str] = None object_permission: Optional[LiteLLM_ObjectPermissionTable] = None access_group_ids: Optional[List[str]] = None - logging_exporters: Sequence[str] | None = None rotation_count: Optional[int] = 0 auto_rotate: Optional[bool] = False rotation_interval: Optional[str] = None diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 63d5bd19081..15922c9fbb4 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1084,7 +1084,6 @@ class AllowedVectorStoreIndexItem(LiteLLMPydanticObjectBase): class KeyRequestBase(GenerateRequestBase): key: Optional[str] = None budget_id: Optional[str] = None - logging_exporters: Sequence[str] | None = None tags: Optional[List[str]] = None disable_global_guardrails: Optional[bool] = None throttle_on_budget_exceeded: Optional[bool] = None diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 91c15489f72..08766ebae71 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -625,10 +625,9 @@ async def _effective_org_id(user_api_key_dict: UserAPIKeyAuth) -> str | None: async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_id: str | None) -> frozenset[str]: """The union of admin-assigned exporter names across the request's identity chain. - Each level is read from its own ``logging_exporters`` column: the key via - ``get_key_object`` (the auth object's fields are the team's shadow, so it is - fetched fresh), the team via ``get_team_object``, the org via ``get_org_object`` - on the effective ``org_id`` (token org or team fallback). Internal-user is + Each level is read from its own ``logging_exporters`` column: the team via + ``get_team_object``, the org via ``get_org_object`` on the effective ``org_id`` + (token org or team fallback). Keys inherit from their team and org. Internal-user is intentionally not a routing dimension. The assignment is an admin-owned column; the request never supplies it. Needs a DB connection: in SDK mode there is no identity to resolve against, so this is empty (admin-owned destinations do not @@ -636,7 +635,6 @@ async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_i """ from litellm.proxy import proxy_server from litellm.proxy.auth.auth_checks import ( - get_key_object, get_org_object, get_team_object, ) @@ -659,19 +657,6 @@ async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_i except Exception: # noqa: BLE001 # best-effort identity enrichment; a failed lookup must not block the request return () - key_names = ( - await _level( - get_key_object( - hashed_token=user_api_key_dict.token, - prisma_client=prisma_client, - user_api_key_cache=cache, - parent_otel_span=span, - proxy_logging_obj=proxy_server.proxy_logging_obj, - ) - ) - if user_api_key_dict.token - else () - ) team_names = ( await _level( get_team_object( @@ -698,7 +683,7 @@ async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_i if org_id else () ) - return frozenset((*key_names, *team_names, *org_names)) + return frozenset((*team_names, *org_names)) async def _resolve_logging_exporters( diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 7bd7cf0e174..63140689097 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -18,7 +18,7 @@ import os import re import secrets import traceback -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from datetime import datetime, timedelta, timezone from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, cast @@ -1504,7 +1504,6 @@ async def generate_key_fn( - metadata: Optional[dict] - Metadata for key, store information for key. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" } - guardrails: Optional[List[str]] - List of active guardrails for the key - policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules. - - logging_exporters: Optional[List[str]] - Names of admin-owned logging destinations (credential names) this key exports its traces to. - disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key. - throttle_on_budget_exceeded: Optional[bool] - When the key exceeds its max_budget, throttle its tpm/rpm to the global budget_exceeded_throttle_percentage instead of blocking the key entirely. - permissions: Optional[dict] - key-specific permissions. Currently just used for turning off pii masking (if connected). Example - {"pii": false} @@ -1557,9 +1556,6 @@ async def generate_key_fn( """ try: from litellm.proxy._types import CommonProxyErrors - from litellm.proxy.management_endpoints.logging_exporter_validation import ( - validate_logging_exporter_field, - ) from litellm.proxy.proxy_server import ( prisma_client, user_api_key_cache, @@ -1648,11 +1644,6 @@ async def generate_key_fn( route=KeyManagementRoutes.KEY_GENERATE, ) - # logging_exporters on a key is proxy-admin only. Skip the check when the - # field isn't in the payload to keep /key/generate cheap for the common case. - if data.logging_exporters is not None: - validate_logging_exporter_field(data.logging_exporters, user_api_key_dict) - if team_table is not None: await _check_team_key_limits( team_table=team_table, @@ -1830,15 +1821,6 @@ async def generate_service_account_key_fn( route=KeyManagementRoutes.KEY_GENERATE_SERVICE_ACCOUNT, ) - # Same logging_exporters gate as /key/generate: proxy-admin only. Skip the - # check unless the field is being written. - from litellm.proxy.management_endpoints.logging_exporter_validation import ( - validate_logging_exporter_field, - ) - - if data.logging_exporters is not None: - validate_logging_exporter_field(data.logging_exporters, user_api_key_dict) - data.user_id = None # do not allow user_id to be set for service account keys return await _common_key_generation_helper( @@ -2559,7 +2541,6 @@ async def update_key_fn( # noqa: C901 # single endpoint handling many optional - send_invite_email: Optional[bool] - Send invite email to user_id - guardrails: Optional[List[str]] - List of active guardrails for the key - policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules. - - logging_exporters: Optional[List[str]] - Names of admin-owned logging destinations (credential names) this key exports its traces to. - disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key. - throttle_on_budget_exceeded: Optional[bool] - When the key exceeds its max_budget, throttle its tpm/rpm to the global budget_exceeded_throttle_percentage instead of blocking the key entirely. - prompts: Optional[List[str]] - List of prompts that the key is allowed to use. @@ -2594,9 +2575,6 @@ async def update_key_fn( # noqa: C901 # single endpoint handling many optional }' ``` """ - from litellm.proxy.management_endpoints.logging_exporter_validation import ( - validate_logging_exporter_field, - ) from litellm.proxy.proxy_server import ( llm_router, premium_user, @@ -2623,16 +2601,6 @@ async def update_key_fn( # noqa: C901 # single endpoint handling many optional prisma_client=prisma_client, ) - # logging_exporters is proxy-admin only. The validator no-ops when the - # effective value doesn't change; pass the stored column value so a - # non-admin cannot clear an admin-assigned one. - if data.logging_exporters is not None: - validate_logging_exporter_field( - data.logging_exporters, - user_api_key_dict, - existing_exporters=getattr(existing_key_row, "logging_exporters", None), - ) - await _validate_update_key_data( data=data, existing_key_row=existing_key_row, @@ -3621,7 +3589,6 @@ async def generate_key_helper_fn( rotation_interval: Optional[str] = None, router_settings: Optional[dict] = None, access_group_ids: Optional[list] = None, - logging_exporters: Sequence[str] | None = None, # admin-owned OTEL destinations (credential names) budget_limits: Optional[list] = None, # multiple concurrent budget windows ): from litellm.proxy.proxy_server import premium_user, prisma_client @@ -3759,7 +3726,6 @@ async def generate_key_helper_fn( "object_permission_id": object_permission_id, "router_settings": router_settings_json, "access_group_ids": access_group_ids or [], - "logging_exporters": logging_exporters or [], } # Add rotation fields if auto_rotate is enabled @@ -4826,23 +4792,6 @@ async def regenerate_key_fn( # noqa: C901 # single endpoint handling many opti is_proxy_admin=_regen_is_proxy_admin, ) - # logging_exporters gate on regenerate matches /key/generate and - # /key/update. Without this, a key owner could set logging_exporters on - # /key/{id}/regenerate and route future traces to a destination they - # aren't allowed to assign (Veria F3). The validator no-ops when the - # effective value doesn't change; pass the stored column value so a - # non-admin cannot clear an admin-assigned one. - if data is not None and data.logging_exporters is not None: - from litellm.proxy.management_endpoints.logging_exporter_validation import ( - validate_logging_exporter_field, - ) - - validate_logging_exporter_field( - data.logging_exporters, - user_api_key_dict, - existing_exporters=getattr(_key_in_db, "logging_exporters", None), - ) - verbose_proxy_logger.info( "Key regeneration requested: key_alias=%s", getattr(_key_in_db, "key_alias", None), diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index ed4f7d4f0a0..60de5dc5470 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -445,7 +445,6 @@ model LiteLLM_VerificationToken { key_type String? policies String[] @default([]) access_group_ids String[] @default([]) - logging_exporters String[] @default([]) // admin-owned OTEL trace destinations assigned to this key (credential names) model_spend Json @default("{}") model_max_budget Json @default("{}") budget_fallbacks Json @default("{}") @@ -541,7 +540,6 @@ model LiteLLM_DeletedVerificationToken { key_type String? policies String[] @default([]) access_group_ids String[] @default([]) - logging_exporters String[] @default([]) model_spend Json @default("{}") model_max_budget Json @default("{}") budget_fallbacks Json @default("{}") diff --git a/schema.prisma b/schema.prisma index ed4f7d4f0a0..60de5dc5470 100644 --- a/schema.prisma +++ b/schema.prisma @@ -445,7 +445,6 @@ model LiteLLM_VerificationToken { key_type String? policies String[] @default([]) access_group_ids String[] @default([]) - logging_exporters String[] @default([]) // admin-owned OTEL trace destinations assigned to this key (credential names) model_spend Json @default("{}") model_max_budget Json @default("{}") budget_fallbacks Json @default("{}") @@ -541,7 +540,6 @@ model LiteLLM_DeletedVerificationToken { key_type String? policies String[] @default([]) access_group_ids String[] @default([]) - logging_exporters String[] @default([]) model_spend Json @default("{}") model_max_budget Json @default("{}") budget_fallbacks Json @default("{}") diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 367bda5801c..bd7a8814c28 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -5232,11 +5232,12 @@ async def test_resolve_logging_exporters_team_level(_seeded_logging_credentials, @pytest.mark.asyncio -async def test_resolve_logging_exporters_unions_key_team_org( +async def test_resolve_logging_exporters_unions_team_org_keys_inherit( _seeded_logging_credentials, monkeypatch ): - # key, team, and org are each read from their OWN logging_exporters column. - # All three union, deduped. + # team and org are each read from their OWN logging_exporters column and union, + # deduped. Keys have no assignment column: they inherit from team/org, so a + # key-level value (patched below) must contribute nothing. from litellm.proxy.litellm_pre_call_utils import _resolve_logging_exporters _patch_identity(monkeypatch, key=["arize-prod"], team=["langfuse-eu"], org=["langfuse-eu"]) @@ -5245,9 +5246,8 @@ async def test_resolve_logging_exporters_unions_key_team_org( assert {d["endpoint"] for d in destinations} == { "https://cloud.langfuse.com/api/public/otel", # team + org (deduped) - "https://otlp.arize.com/v1", # key } - assert set(backends) == {"langfuse_otel", "arize"} + assert set(backends) == {"langfuse_otel"} @pytest.mark.asyncio diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 7a376cc1b4f..468e6f10a9f 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -36,8 +36,6 @@ import { fetchTeamModels } from "../organisms/create_key_button"; import NumericalInput from "../shared/numerical_input"; import { Tag } from "../tag_management/types"; import EditLoggingSettings from "../team/EditLoggingSettings"; -import { LoggingExportersFormItem } from "../logging_credentials/LoggingExportersSelect"; -import { loggingExportersOf } from "../logging_credentials/loggingExportersOf"; import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"; interface KeyEditViewProps { @@ -210,7 +208,6 @@ export function KeyEditView({ accessGroups: keyData.object_permission?.agent_access_groups || [], }, logging_settings: extractLoggingSettings(keyData.metadata), - logging_exporters: loggingExportersOf(keyData), disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks) ? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks) : [], @@ -242,8 +239,7 @@ export function KeyEditView({ mcp_tool_permissions: keyData.object_permission?.mcp_tool_permissions || {}, throttle_on_budget_exceeded: keyData.metadata?.throttle_on_budget_exceeded || false, logging_settings: extractLoggingSettings(keyData.metadata), - logging_exporters: loggingExportersOf(keyData), - disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks) + disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks) ? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks) : [], access_group_ids: keyData.access_group_ids || [], @@ -834,7 +830,6 @@ export function KeyEditView({ )} - (keyData); - // Destinations whose credential_info.access targets THIS key (via its team_id, - // its team's organization_id, or global). Rendered alongside the key's own - // metadata.logging_exporters so the Logging Exporters section reflects BOTH - // routing directions, matching the resolver's union at request time. - const scopedExportersForKey = useMemo(() => { - const keyTeamId = (currentKeyData as { team_id?: string | null } | undefined)?.team_id ?? null; - const team = (teamsData ?? []).find((t) => t.team_id === keyTeamId); - const teamOrgId = (team as { organization_id?: string | null } | undefined)?.organization_id ?? null; - return (keyCredentialsData?.credentials ?? []) - .filter((c) => c.credential_info?.credential_type === "logging") - .filter((c) => { - const access = c.credential_info?.access; - if (!access) return false; - if (access.global === true) return true; - if (Array.isArray(access.teams) && keyTeamId && access.teams.includes(keyTeamId)) return true; - return Array.isArray(access.orgs) && teamOrgId != null && access.orgs.includes(teamOrgId); - }) - .map((c) => c.credential_name); - }, [keyCredentialsData?.credentials, currentKeyData, teamsData, keyOrganizationsData]); const [lastRegeneratedAt, setLastRegeneratedAt] = useState(null); const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false); const [policyGuardrails, setPolicyGuardrails] = useState>({}); @@ -711,8 +682,6 @@ export default function KeyInfoView({