import asyncio
import copy
import json
import re
import time
from collections import OrderedDict
from collections.abc import Mapping, MutableMapping
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final

from fastapi import HTTPException, Request
from pydantic import ValidationError as PydanticValidationError
from starlette.datastructures import Headers

import litellm
from litellm._logging import verbose_logger, verbose_proxy_logger
from litellm._service_logger import ServiceLogging
from litellm.constants import (
    CONSUMED_REQUEST_TAGS_METADATA_KEY,
    INTERNAL_CALL_ORIGIN_METADATA_KEY,
    LITELLM_PROXY_MASTER_KEY_ALIAS,
    OTEL_SERVICE_NAME_METADATA_KEYS,
    PRE_CALL_EXECUTED_GUARDRAILS_KEY,
    SESSION_DEPLOYMENT_AFFINITY_TTL_METADATA_KEY,
)
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
    TRUSTED_CALLBACK_VARS_FIELD,
    _request_blocked_callback_params,
    iter_client_callback_metadata_dicts,
)
from litellm.litellm_core_utils.internal_call_metadata import MODEL_ACCESS_GROUP_METADATA_KEY
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
from litellm.litellm_core_utils.url_utils import (
    is_url_destination_allowed_by_host,
    provider_url_destination_candidates,
)
from litellm.proxy._types import (
    AddTeamCallback,
    CommonProxyErrors,
    LitellmDataForBackendLLMCall,
    LitellmUserRoles,
    ProxyErrorTypes,
    ProxyException,
    SpecialHeaders,
    TeamCallbackMetadata,
    UserAPIKeyAuth,
)
from litellm.proxy.common_utils.callback_utils import (
    decrypt_callback_vars,
    get_metadata_variable_name_from_kwargs,
    strip_callback_config,
)
from litellm.proxy.common_utils.http_parsing_utils import _safe_get_request_headers
from litellm.types.integrations.anthropic_cache_control_hook import GATEWAY_INJECTED_CACHE_METADATA_KEY

# Cache special headers as a frozenset for O(1) lookup performance
_SPECIAL_HEADERS_CACHE: Final = frozenset(v.value.lower() for v in SpecialHeaders._member_map_.values())

_REDACTED_HEADER_VALUE: Final = "***REDACTED***"
_CREDENTIAL_HEADER_NAMES: Final = SpecialHeaders.litellm_credential_header_names() | frozenset(
    {"cookie", "proxy-authorization"}
)
_TRANSPORT_ONLY_CREDENTIAL_KEYS: Final = frozenset({"provider_specific_header", "headers", "api_key"})

# Matches any header of the form x-<something>-session-id (case-insensitive).
# Excludes the two explicit litellm headers which are handled with higher priority.
_GENERIC_SESSION_ID_HEADER_RE: Final = re.compile(r"^x-.+-session-id$", re.IGNORECASE)
_EXPLICIT_SESSION_HEADERS: Final = frozenset({"x-litellm-trace-id", "x-litellm-session-id"})
# Codex carries its conversation uuid in unprefixed headers, so the
# x-<vendor>-session-id convention above never matches it. Current builds send
# ``session-id``/``thread-id``; builds before the codex-api split sent
# ``session_id``/``conversation_id``. Ordered session before thread.
_CODEX_SESSION_ID_HEADERS: Final = ("session-id", "session_id", "thread-id", "conversation_id")
# Matches every first-party Codex originator: codex-tui, codex_cli_rs, codex_exec,
# codex_vscode, "Codex ...". A separator is required so an unrelated "codexfoo" client
# does not read as Codex.
_CODEX_CLIENT_PREFIX_RE: Final = re.compile(r"^codex[-_ /]", re.IGNORECASE)
# Session-id values must be non-empty strings of alphanumerics, hyphens, or underscores
# (covers UUIDs and most common session-id formats).
_SESSION_ID_VALUE_RE: Final = re.compile(r"^[a-zA-Z0-9_\-]{8,}$")

_SHA256_HEX_RE: Final = re.compile(r"^[0-9a-f]{64}$")

# W3C Trace Context traceparent header: https://www.w3.org/TR/trace-context/
# e.g. "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
_TRACEPARENT_RE: Final = re.compile(r"^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$", re.IGNORECASE)


def _trace_id_from_traceparent(traceparent: str) -> str | None:
    """Extract the trace-id from a W3C Trace Context traceparent header, e.g.
    "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" -> the 32-hex
    trace-id in the middle. An all-zero trace-id is invalid per spec and is
    rejected, matching how the OpenTelemetry SDK itself treats it."""
    match: Final = _TRACEPARENT_RE.match(traceparent.strip())
    if not match:
        return None
    trace_id: Final = match.group(1).lower()
    return trace_id if trace_id != "0" * 32 else None


def _session_id_from_baggage(baggage: str) -> str | None:
    """Extract a session.id entry from a W3C Baggage header
    (https://www.w3.org/TR/baggage/), e.g. "session.id=abc-123,user.id=42"."""
    for pair in baggage.split(","):
        key, _, value = pair.strip().partition("=")
        if key.strip() == "session.id" and value.strip():
            return value.strip()
    return None


def _stampable_key_hash(user_api_key_dict: UserAPIKeyAuth) -> str | None:
    """Only proxy-validated keys are stamped, proven by the unforgeable
    via_virtual_key marker AND a known non-secret shape: the sha256 hex digest
    UserAPIKeyAuth stores virtual keys in, or the master key's stable alias.
    Custom-auth credentials arrive raw (never forward auth material) and hashed
    JWTs rotate on re-issue (useless as a stable ban id), so both are skipped."""
    api_key: Final = user_api_key_dict.api_key
    if not user_api_key_dict.via_virtual_key or api_key is None:
        return None
    if api_key == LITELLM_PROXY_MASTER_KEY_ALIAS or _SHA256_HEX_RE.fullmatch(api_key):
        return api_key
    return None


_ANTHROPIC_SESSION_ID_VALUE_RE: Final = re.compile(r"^[a-zA-Z0-9_\-]+$")


def _sanitize_for_log(value: Any) -> str:
    """
    Basic log sanitization helper to reduce log-injection risk.

    Removes newline and carriage-return characters so user-controlled
    values cannot forge additional log lines when written to text logs.
    """
    try:
        text = str(value)
    except Exception:
        # Fallback to repr if str() fails for any reason
        text = repr(value)
    # Strip CR/LF characters commonly used for log injection
    return text.replace("\r", "").replace("\n", "")


from litellm.router import Router
from litellm.secret_managers.main import get_secret_bool
from litellm.types.llms.anthropic import ANTHROPIC_API_HEADERS
from litellm.types.services import ServiceTypes
from litellm.types.utils import (
    CustomPricingLiteLLMParams,
    LlmProviders,
    ProviderSpecificHeader,
    StandardLoggingUserAPIKeyMetadata,
    SupportedCacheControls,
)

service_logger_obj: Final = ServiceLogging()  # used for tracking latency on OTEL
# Bounded dedup for stale-alias warnings (FIFO eviction when over cap).
_MAX_STALE_ALIAS_WARNING_KEYS: Final = 10_000
_STALE_TEAM_ALIAS_WARNING_KEYS: Final[OrderedDict[str, None]] = OrderedDict()
# Cache the stale alias bypass flag at module load to avoid hot-path secret lookups
_ENABLE_TEAM_STALE_ALIAS_BYPASS: bool | None = None


if TYPE_CHECKING:
    from litellm.proxy.proxy_server import ProxyConfig as _ProxyConfig
    from litellm.types.proxy.policy_engine import PolicyMatchContext

    ProxyConfig = _ProxyConfig
else:
    ProxyConfig = Any
    PolicyMatchContext = Any


def parse_cache_control(cache_control):
    cache_dict: Final = {}
    directives: Final = cache_control.split(", ")

    for directive in directives:
        if "=" in directive:
            key, value = directive.split("=")
            cache_dict[key] = value
        else:
            cache_dict[directive] = True

    return cache_dict


LITELLM_METADATA_ROUTES: Final = (
    "batches",
    "bedrock",
    "/v1/messages",
    "responses",
    "files",
)

LITELLM_TRACE_CONTROL_METADATA_FIELDS: Final = frozenset(
    {
        "mask_input",
        "mask_output",
        "session_id",
        "trace_id",
        "trace_metadata",
        "trace_name",
        "trace_release",
        "trace_user_id",
        "trace_version",
    }
)

_UNTRUSTED_ROOT_CONTROL_FIELDS: Final = (
    "proxy_server_request",
    "standard_logging_object",
    "secret_fields",
    "mock_response",
    "mock_tool_calls",
    "disable_global_guardrails",
    "disable_global_guardrail",
    "enable_prompt_caching",
    "opted_out_global_guardrails",
    "applied_guardrails",
    "applied_policies",
    "policy_sources",
    "guardrail_scan_ids",
    "routing_decision",
    GATEWAY_INJECTED_CACHE_METADATA_KEY,
    "pillar_response_headers",
    "_guardrail_pipelines",
    "_pipeline_managed_guardrails",
    # Callback-registration fields. ``callbacks``, ``service_callback``,
    # and ``logger_fn`` are read by ``litellm.utils.function_setup`` and
    # appended to process-wide ``litellm.{input,success,failure,_async_*,
    # service}_callback`` lists / ``litellm.user_logger_fn`` — one request
    # poisons the worker for every subsequent caller.
    # ``litellm_disabled_callbacks`` is the inverse primitive: the
    # legitimate path reads it from key/team metadata, the request-body
    # version silently turns off admin-configured audit/observability
    # for the caller's request.
    "callbacks",
    "service_callback",
    "logger_fn",
    "litellm_disabled_callbacks",
    # Agentic-loop control fields. These bound or drive an interceptor's agentic
    # loop (web search, compression, code interpreter) and are server-controlled.
    # A client-supplied value would forge loop depth/cycle state, mark an
    # interception as active (triggering sandbox code execution without the
    # native tool ever being present), force the completed response to be
    # re-wrapped as a synthetic stream the caller never asked for, or raise the
    # loop ceiling to drive many upstream model calls and sandbox executions
    # from a single request.
    "_agentic_loop_depth",
    "_agentic_loop_fingerprints",
    "_code_interpreter_interception_active",
    "_code_interpreter_interception_converted_stream",
    "_code_interpreter_interception_sandbox_key",
    "_code_interpreter_interception_session_scoped",
    "_headroom_interception_converted_stream",
    "max_agentic_loops",
    # Recomputed below from the actual caller-controlled timeout sources (headers and
    # body fields); a client-forged value here would let a request either dodge cooldown
    # protection on a real deployment failure or force a false "not caller-controlled"
    # reading that lets its own bad timeout cool down deployments other tenants rely on.
    "client_side_timeout",
)

_UNTRUSTED_METADATA_CONTROL_FIELDS: Final = (
    "disable_global_guardrails",
    "disable_global_guardrail",
    "opted_out_global_guardrails",
    "pillar_response_headers",
    "_pillar_response_headers_trusted",
    "pillar_flagged",
    "pillar_scanners",
    "pillar_evidence",
    "pillar_evidence_truncated",
    "pillar_session_id_response",
    "applied_guardrails",
    "applied_policies",
    "policy_sources",
    "guardrail_scan_ids",
    "routing_decision",
    GATEWAY_INJECTED_CACHE_METADATA_KEY,
    SESSION_DEPLOYMENT_AFFINITY_TTL_METADATA_KEY,
    CONSUMED_REQUEST_TAGS_METADATA_KEY,
    INTERNAL_CALL_ORIGIN_METADATA_KEY,
    "standard_logging_object",
    "proxy_server_request",
    "secret_fields",
    "_guardrail_pipelines",
    "_pipeline_managed_guardrails",
    "client_disconnected",
    "error_information",
    PRE_CALL_EXECUTED_GUARDRAILS_KEY,
)

UNTRUSTED_REQUEST_HEADER_CONTROL_FIELDS: Final = frozenset(
    {
        "litellm-disable-message-redaction",
    }
)
_CLIENT_MOCK_CONTROL_FIELDS: Final = frozenset({"mock_response", "mock_tool_calls"})
_ALLOW_CLIENT_MOCK_RESPONSE_METADATA_KEY: Final = "allow_client_mock_response"
_ALLOW_CLIENT_MESSAGE_REDACTION_OPT_OUT_METADATA_KEY: Final = "allow_client_message_redaction_opt_out"

# Per-request pricing parameters mutate cost-tracking output and (via
# ``litellm.completion`` → ``register_model``) the process-wide
# ``litellm.model_cost`` map. Both effects belong to deployment configuration,
# not to user-supplied request bodies, so the proxy strips them before they
# reach the call path. Built from the Pydantic model so newly-added pricing
# fields are covered automatically.
_CLIENT_PRICING_CONTROL_FIELDS: Final = frozenset(CustomPricingLiteLLMParams.model_fields.keys())
# ``model_info`` carries the same pricing fields when read by
# ``use_custom_pricing_for_model``; strip from metadata for the same reason.
# ``standard_logging_guardrail_information`` is proxy-written telemetry summed
# into response_cost and spend; a client seeding it forges (even negative)
# guardrail cost.
_CLIENT_PRICING_METADATA_FIELDS: Final = frozenset({"model_info", "standard_logging_guardrail_information"})
# ``attempted_fallbacks`` and ``original_model_group`` are written by the router
# and read by spend logs as fact; a client value has no legitimate meaning and no
# key or team setting keeps it, so the strip is never gated.
_ROUTER_RESERVED_METADATA_FIELDS: Final = frozenset({"attempted_fallbacks", "original_model_group"})
_ALLOW_CLIENT_PRICING_OVERRIDE_METADATA_KEY: Final = "allow_client_pricing_override"

# Request fields whose value, when URL-valued, becomes the outbound destination
# for a provider call. Letting a proxy caller pin the destination is an SSRF
# primitive (HuggingFace/Oobabooga `model`, Gemini files `file_id`); guard
# them centrally so SDK users keep working but proxy users default-deny.
_URL_DESTINATION_REQUEST_FIELDS: Final = ("model", "file_id")


def _reject_url_valued_destinations(data: dict[str, Any]) -> None:
    """Reject URL-valued ``model``/``file_id`` unless admin-allowlisted.

    Some providers (HuggingFace, Oobabooga, Gemini files) accept a URL in the
    identifier field and use it as the outbound destination. On the proxy that
    is an SSRF primitive — a low-privilege caller can point traffic at any
    host the proxy can reach, including internal services. Reject here at the
    proxy boundary so SDK users (who legitimately pass URL-valued identifiers)
    are unaffected, while admins can opt specific hosts back in via
    ``litellm.provider_url_destination_allowed_hosts``.
    """
    for field in _URL_DESTINATION_REQUEST_FIELDS:
        value = data.get(field)
        if isinstance(value, str):
            reject_url_valued_destination(field, value)


def reject_url_valued_destination(field: str, value: str) -> None:
    """Reject a URL-valued destination identifier unless admin-allowlisted.

    Operates on one field/value pair. ``_reject_url_valued_destinations`` applies
    it across ``_URL_DESTINATION_REQUEST_FIELDS`` for a request body.
    """
    allowed_hosts: Final = getattr(litellm, "provider_url_destination_allowed_hosts", []) or []
    for candidate in provider_url_destination_candidates(value):
        if not candidate.lower().startswith(("http://", "https://")):
            continue
        if is_url_destination_allowed_by_host(candidate, allowed_hosts):
            continue
        raise HTTPException(
            status_code=400,
            detail={
                "error": "invalid_request",
                "param": field,
                "message": (
                    f"URL-valued '{field}' is not allowed. Configure custom "
                    "endpoints with api_base instead, or add the destination "
                    "host to `provider_url_destination_allowed_hosts` in "
                    "litellm_settings."
                ),
            },
        )


_METADATA_JSON_TYPE_NAMES: Final[Mapping[type, str]] = MappingProxyType(
    {bool: "a boolean", int: "an integer", float: "a number", str: "a string", list: "an array"}
)


def _invalid_metadata_type_error(field: str, value: object) -> ProxyException:
    received_type: Final = _METADATA_JSON_TYPE_NAMES.get(type(value), f"a {type(value).__name__}")
    return ProxyException(
        message=f"Invalid type for '{field}': expected an object, but got {received_type} instead.",
        type=ProxyErrorTypes.bad_request_error,
        param=field,
        code=400,
    )


def _normalized_metadata_object(field: str, value: object) -> Mapping[str, Any]:
    """Return ``value`` as a metadata object or raise a 400 like OpenAI does.

    A JSON string that parses to an object is accepted because multipart/form-data
    and ``extra_body`` callers can only send metadata as a string. The caller pops
    the raw value from the request body before validating so the failure-logging
    hooks that inspect the body afterwards don't crash on it and mask the 400 as a 500.
    """
    if isinstance(value, dict):
        return value
    if isinstance(value, str) and isinstance((parsed := safe_json_loads(value)), dict):
        return parsed
    raise _invalid_metadata_type_error(field=field, value=value)


def _strip_untrusted_request_header_controls(
    headers: Any,
    *,
    allow_client_message_redaction_opt_out: bool = False,
) -> None:
    if not isinstance(headers, dict):
        return

    for header_name in list(headers.keys()):
        if isinstance(header_name, str) and header_name.lower() in UNTRUSTED_REQUEST_HEADER_CONTROL_FIELDS:
            if allow_client_message_redaction_opt_out:
                continue
            headers.pop(header_name, None)


def _is_false_like(value: Any) -> bool:
    if isinstance(value, bool):
        return value is False
    if isinstance(value, str):
        return value.strip().lower() in {"false", "0", "no", "off"}
    return False


def _key_or_team_metadata_flag_is_true(
    user_api_key_dict: UserAPIKeyAuth,
    metadata_key: str,
) -> bool:
    for admin_metadata in (user_api_key_dict.metadata, user_api_key_dict.team_metadata):
        if isinstance(admin_metadata, dict) and admin_metadata.get(metadata_key) is True:
            return True
    return False


def _key_or_team_allows_client_mock_response(
    user_api_key_dict: UserAPIKeyAuth,
) -> bool:
    return _key_or_team_metadata_flag_is_true(
        user_api_key_dict=user_api_key_dict,
        metadata_key=_ALLOW_CLIENT_MOCK_RESPONSE_METADATA_KEY,
    )


def _key_or_team_allows_client_message_redaction_opt_out(
    user_api_key_dict: UserAPIKeyAuth,
) -> bool:
    return _key_or_team_metadata_flag_is_true(
        user_api_key_dict=user_api_key_dict,
        metadata_key=_ALLOW_CLIENT_MESSAGE_REDACTION_OPT_OUT_METADATA_KEY,
    )


def _key_or_team_allows_client_pricing_override(
    user_api_key_dict: UserAPIKeyAuth,
) -> bool:
    return _key_or_team_metadata_flag_is_true(
        user_api_key_dict=user_api_key_dict,
        metadata_key=_ALLOW_CLIENT_PRICING_OVERRIDE_METADATA_KEY,
    )


def _strip_client_message_redaction_opt_out(data: dict[str, Any]) -> None:
    stripped: Final[list[str]] = []
    if "turn_off_message_logging" in data and _is_false_like(data["turn_off_message_logging"]):
        stripped.append("turn_off_message_logging")
        data.pop("turn_off_message_logging", None)
    for slot_label, metadata in iter_client_callback_metadata_dicts(data):
        if "turn_off_message_logging" in metadata and _is_false_like(metadata["turn_off_message_logging"]):
            stripped.append(f"{slot_label}.turn_off_message_logging")
            metadata.pop("turn_off_message_logging", None)
    if stripped:
        verbose_proxy_logger.debug(
            "Stripped client-supplied message-redaction opt-out fields from request body: %s. "
            "Set `allow_client_message_redaction_opt_out: true` on the key or team metadata "
            "to keep these values.",
            ", ".join(stripped),
        )


def _strip_client_callback_credentials(
    data: dict[str, Any],  # mutable-ok: strips in place on the request body the pre-call pipeline threads through
) -> None:
    """Drop callback credentials and destinations supplied by the caller.

    ``_request_blocked_callback_params`` (Datadog + GCS credentials, sites and agent
    hosts) are already ignored when building ``standard_callback_dynamic_params``.
    Strip them from the body and every client metadata slot as well, so a caller
    cannot pair its own ``dd_site``/``dd_agent_host`` with the team's admin-configured
    ``dd_api_key`` and have the resulting logs shipped to a host it controls.

    ``TRUSTED_CALLBACK_VARS_FIELD`` is proxy-owned; it is cleared here and repopulated
    from team/key callback settings in ``add_litellm_data_to_request``.
    """
    containers: Final = (("body", data), *iter_client_callback_metadata_dicts(data))
    stripped: Final = tuple(
        f"{label}.{field}"
        for label, container in containers
        for field in _request_blocked_callback_params
        if field in container
    )
    for _, container in containers:
        for field in _request_blocked_callback_params:
            container.pop(field, None)
    data.pop(TRUSTED_CALLBACK_VARS_FIELD, None)
    if stripped:
        verbose_proxy_logger.debug(
            "Stripped client-supplied callback credentials from request: %s. "
            "Configure these on the team or key callback settings instead.",
            ", ".join(sorted(stripped)),
        )


def _strip_client_pricing_overrides(data: dict[str, Any]) -> None:
    """Drop pricing overrides from the request body and any metadata variant.

    Skipped only when the calling key/team carries
    ``allow_client_pricing_override: True`` in its metadata. Emits a
    ``debug``-level log line naming the dropped fields so operators can
    trace why a client-supplied pricing override stopped being applied
    (otherwise the strip is invisible from the caller's perspective).
    """
    stripped: Final[list[str]] = []
    for field in _CLIENT_PRICING_CONTROL_FIELDS:
        if field in data:
            stripped.append(field)
            data.pop(field, None)
    for metadata_key in ("metadata", "litellm_metadata"):
        metadata = data.get(metadata_key)
        if not isinstance(metadata, dict):
            continue
        for field in _CLIENT_PRICING_METADATA_FIELDS:
            if field in metadata:
                stripped.append(f"{metadata_key}.{field}")
                metadata.pop(field, None)
    if stripped:
        verbose_proxy_logger.debug(
            "Stripped client-supplied pricing fields from request body: %s. "
            "Set `allow_client_pricing_override: true` on the key or team "
            "metadata to keep these values.",
            ", ".join(stripped),
        )


def _strip_router_reserved_metadata(
    data: dict[str, Any],  # mutable-ok: strips in place on the request body the pre-call pipeline threads through
) -> None:
    """Drop the router-owned fallback stamps from any client-supplied metadata bucket."""
    for metadata_key in ("metadata", "litellm_metadata"):
        if not isinstance(metadata := data.get(metadata_key), dict):
            continue
        for field in _ROUTER_RESERVED_METADATA_FIELDS & metadata.keys():
            metadata.pop(field)
            verbose_proxy_logger.debug(
                "Stripped router-reserved metadata field from request body: %s.%s", metadata_key, field
            )


def _get_metadata_variable_name(request: Request) -> str:
    """
    Helper to return what the "metadata" field should be called in the request data

    For all /thread or /assistant endpoints we need to call this "litellm_metadata"

    For ALL other endpoints we call this "metadata"
    """
    # Inline imports — auth_utils/route_checks participate in a proxy import cycle.
    from litellm.proxy.auth.auth_utils import get_request_route  # noqa: PLC0415

    path: Final = get_request_route(request)
    if "thread" in path or "assistant" in path:
        return "litellm_metadata"

    if any(route in path for route in LITELLM_METADATA_ROUTES):
        return "litellm_metadata"

    return "metadata"


def _promoted_trace_control_fields(
    requester_metadata: Mapping[str, Any],
    litellm_metadata: Mapping[str, Any],
) -> tuple[tuple[str, Any], ...]:
    """Return the caller's trace-control fields that ``litellm_metadata`` does not already set."""
    return tuple(
        (key, value)
        for key, value in requester_metadata.items()
        if key in LITELLM_TRACE_CONTROL_METADATA_FIELDS and key not in litellm_metadata
    )


def _extract_generic_session_id_from_headers(
    normalized: dict[str, str],
) -> str | None:
    """
    Scan a normalised (lower-cased keys) header dict for any header that looks
    like ``x-<vendor>-session-id`` and whose value is a plausible session/trace
    identifier (alphanumeric + hyphens/underscores, at least 8 chars).

    The two explicit LiteLLM headers (``x-litellm-trace-id`` /
    ``x-litellm-session-id``) are excluded here because they are handled with
    higher priority by the caller.

    Example: ``x-claude-code-session-id: e96634a3-fa28-4083-b354-55542e2dca01``
    """
    for key, value in normalized.items():
        if (
            key not in _EXPLICIT_SESSION_HEADERS
            and _GENERIC_SESSION_ID_HEADER_RE.match(key)
            and isinstance(value, str)
            and _SESSION_ID_VALUE_RE.match(value)
        ):
            return value
    return None


def _extract_codex_session_id_from_headers(
    normalized: Mapping[str, str],
) -> str | None:
    """
    Read Codex's conversation uuid off one of ``_CODEX_SESSION_ID_HEADERS``.

    Codex sends no request metadata the Anthropic path could parse and no
    ``x-``-prefixed session header, so without this every turn of a Codex session
    falls through to a freshly generated per-call trace id and lands as its own
    row in the logs instead of grouping.

    Unprefixed names like ``session-id`` are generic enough that another client
    could send one meaning something unrelated, and colliding values across
    callers would merge their traces, so this only applies to callers that
    identify as Codex.
    """
    user_agent: Final = normalized.get("user-agent")
    if not isinstance(user_agent, str) or not is_codex_user_agent(user_agent):
        return None
    return next(
        (
            value
            for value in (normalized.get(header) for header in _CODEX_SESSION_ID_HEADERS)
            if isinstance(value, str) and _SESSION_ID_VALUE_RE.match(value)
        ),
        None,
    )


def get_chain_id_from_headers(headers: dict[str, str] | None) -> str | None:
    """
    Extract chain id for call chaining from request headers.

    Priority order:
    1. ``x-litellm-trace-id`` (explicit, highest priority)
    2. ``x-litellm-session-id`` (explicit)
    3. Any ``x-<vendor>-session-id`` header whose value looks like a session id
       (alphanumeric / UUID, at least 8 chars).  E.g. ``x-claude-code-session-id``.
    4. Codex's unprefixed ``session-id`` / ``thread-id``, for Codex callers only.

    Header keys are matched case-insensitively so this works with raw header
    dicts from any transport.

    Used by MCP (and other paths that have raw_headers but no Request) to set
    litellm_trace_id/litellm_session_id for spend logs and logging consistency.
    """
    if not headers:
        return None
    normalized: Final = {k.lower(): v for k, v in headers.items() if isinstance(k, str)}
    return (
        normalized.get("x-litellm-trace-id")
        or normalized.get("x-litellm-session-id")
        or _extract_generic_session_id_from_headers(normalized)
        or _extract_codex_session_id_from_headers(normalized)
    )


def _get_anthropic_session_id_from_metadata(metadata: object) -> str | None:
    if not isinstance(metadata, dict):
        return None

    user_id: Final = metadata.get("user_id")
    if isinstance(user_id, dict):
        session_id = user_id.get("session_id")
        if isinstance(session_id, str) and _ANTHROPIC_SESSION_ID_VALUE_RE.fullmatch(session_id):
            return session_id
        return None
    if not isinstance(user_id, str):
        return None

    session_marker: Final = "_session_"
    session_marker_index: Final = user_id.rfind(session_marker)
    if session_marker_index == -1:
        return None

    session_id = user_id[session_marker_index + len(session_marker) :]
    if not session_id or not _ANTHROPIC_SESSION_ID_VALUE_RE.fullmatch(session_id):
        return None
    return session_id


def is_claude_code_user_agent(user_agent: str) -> bool:
    """Claude Code identifies itself as ``claude-cli/<version> ...``; the IDE
    extensions and the Agent SDK run through the same CLI and share that prefix."""
    return user_agent.startswith("claude-cli/")


def is_codex_user_agent(user_agent: str) -> bool:
    """Codex builds its user agent as ``<originator>/<version> ...`` and ships
    several first-party originators: ``codex-tui``, ``codex_cli_rs``,
    ``codex_exec`` (exec mode), ``codex_vscode`` (IDE extension) and ``Codex ...``
    (see ``is_first_party_originator`` in codex-rs). They agree only on the
    ``codex`` stem, and the TUI sends a bare ``codex-tui`` with no version at all,
    so match the stem plus a separator rather than any one spelling."""
    return bool(_CODEX_CLIENT_PREFIX_RE.match(user_agent))


def should_auto_drop_params_for_agentic_cli(user_agent: str, data: dict, proxy_config: ProxyConfig) -> bool:
    """drop_params defaults to on for agentic CLIs so their client-specific
    params (e.g. Claude Code's thinking, Codex's service_tier) don't fail
    requests routed to providers that reject them. An explicit drop_params
    from the caller or in the operator's ``litellm_settings`` always wins
    over this default."""
    if not (is_claude_code_user_agent(user_agent) or is_codex_user_agent(user_agent)):
        return False
    if "drop_params" in data:
        return False
    config: Final = getattr(proxy_config, "config", None)
    litellm_settings: Final = config.get("litellm_settings") if isinstance(config, dict) else None
    return not (isinstance(litellm_settings, dict) and "drop_params" in litellm_settings)


def safe_add_api_version_from_query_params(data: dict, request: Request):
    try:
        if hasattr(request, "query_params"):
            query_params: Final = dict(request.query_params)
            if "api-version" in query_params:
                data["api_version"] = query_params["api-version"]
    except KeyError:
        pass
    except Exception as e:
        verbose_logger.exception("error checking api version in query params: %s", str(e))


def convert_key_logging_metadata_to_callback(
    data: AddTeamCallback,
    team_callback_settings_obj: TeamCallbackMetadata | None,
) -> TeamCallbackMetadata:
    if team_callback_settings_obj is None:
        team_callback_settings_obj = TeamCallbackMetadata()
    if data.callback_type == "success":
        if team_callback_settings_obj.success_callback is None:
            team_callback_settings_obj.success_callback = []

        if data.callback_name not in team_callback_settings_obj.success_callback:
            team_callback_settings_obj.success_callback.append(data.callback_name)
    elif data.callback_type == "failure":
        if team_callback_settings_obj.failure_callback is None:
            team_callback_settings_obj.failure_callback = []

        if data.callback_name not in team_callback_settings_obj.failure_callback:
            team_callback_settings_obj.failure_callback.append(data.callback_name)
    elif (
        not data.callback_type or data.callback_type == "success_and_failure"
    ):  # assume 'success_and_failure' = litellm.callbacks
        if team_callback_settings_obj.success_callback is None:
            team_callback_settings_obj.success_callback = []
        if team_callback_settings_obj.failure_callback is None:
            team_callback_settings_obj.failure_callback = []
        if team_callback_settings_obj.callbacks is None:
            team_callback_settings_obj.callbacks = []

        if data.callback_name not in team_callback_settings_obj.success_callback:
            team_callback_settings_obj.success_callback.append(data.callback_name)

        if data.callback_name not in team_callback_settings_obj.failure_callback:
            team_callback_settings_obj.failure_callback.append(data.callback_name)

        if data.callback_name not in team_callback_settings_obj.callbacks:
            team_callback_settings_obj.callbacks.append(data.callback_name)

    for var, value in data.callback_vars.items():
        # New Relic routing reads these from the trusted-vars overlay with no
        # callback-name check, so scope them to the newrelic entry: a team that
        # put newrelic_* under a different callback never asked for New Relic and
        # must not export to it.
        if var.startswith("newrelic_") and data.callback_name != "newrelic":
            continue
        if team_callback_settings_obj.callback_vars is None:
            team_callback_settings_obj.callback_vars = {}
        team_callback_settings_obj.callback_vars[var] = str(value)

    return team_callback_settings_obj


def _get_validated_callback_metadata(item: dict, *, source: str) -> AddTeamCallback | None:
    try:
        return AddTeamCallback(**item)
    except (PydanticValidationError, ValueError) as e:
        verbose_proxy_logger.warning(
            "Ignoring invalid %s callback metadata: %s",
            source,
            _sanitize_for_log(str(e)),
        )
        return None


class KeyAndTeamLoggingSettings:
    """
    Helper class to get the dynamic logging settings for the key and team
    """

    @staticmethod
    def get_key_dynamic_logging_settings(user_api_key_dict: UserAPIKeyAuth):
        if user_api_key_dict.metadata is not None and "logging" in user_api_key_dict.metadata:
            return decrypt_callback_vars(user_api_key_dict.metadata).get("logging")
        return None

    @staticmethod
    def get_team_dynamic_logging_settings(user_api_key_dict: UserAPIKeyAuth):
        if user_api_key_dict.team_metadata is not None and "logging" in user_api_key_dict.team_metadata:
            return decrypt_callback_vars(user_api_key_dict.team_metadata).get("logging")
        return None


def _get_dynamic_logging_metadata(
    user_api_key_dict: UserAPIKeyAuth, proxy_config: ProxyConfig
) -> TeamCallbackMetadata | None:
    callback_settings_obj: TeamCallbackMetadata | None = None
    key_dynamic_logging_settings: Final[dict | None] = KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(
        user_api_key_dict
    )
    team_dynamic_logging_settings: Final[dict | None] = KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(
        user_api_key_dict
    )
    #########################################################################################
    # Key-based callbacks
    #########################################################################################
    if key_dynamic_logging_settings is not None:
        for item in key_dynamic_logging_settings:
            callback = _get_validated_callback_metadata(item=item, source="key-level")
            if callback is None:
                continue
            callback_settings_obj = convert_key_logging_metadata_to_callback(
                data=callback,
                team_callback_settings_obj=callback_settings_obj,
            )
    #########################################################################################
    # Team-based callbacks
    #########################################################################################
    elif team_dynamic_logging_settings is not None:
        for item in team_dynamic_logging_settings:
            callback = _get_validated_callback_metadata(item=item, source="team-level")
            if callback is None:
                continue
            callback_settings_obj = convert_key_logging_metadata_to_callback(
                data=callback,
                team_callback_settings_obj=callback_settings_obj,
            )
    #########################################################################################
    # Deprecated format - maintained for backwards compatibility
    #########################################################################################
    elif user_api_key_dict.team_metadata is not None and "callback_settings" in user_api_key_dict.team_metadata:
        """
        callback_settings = {
            {
            'callback_vars': {'langfuse_public_key': 'pk', 'langfuse_secret_key': 'sk_'},
            'failure_callback': [],
            'success_callback': ['langfuse', 'langfuse']
        }
        }
        """
        team_metadata: Final = decrypt_callback_vars(user_api_key_dict.team_metadata)
        callback_settings: Final = team_metadata.get("callback_settings", None) or {}
        callback_settings_obj = TeamCallbackMetadata(**callback_settings)
        verbose_proxy_logger.debug("Team callback settings activated: %s", callback_settings_obj)
    #########################################################################################
    # Enter here when configured on the config.yaml file.
    #########################################################################################
    elif user_api_key_dict.team_id is not None:
        callback_settings_obj = LiteLLMProxyRequestSetup.add_team_based_callbacks_from_config(
            team_id=user_api_key_dict.team_id, proxy_config=proxy_config
        )
    return callback_settings_obj


def clean_headers(
    headers: Headers,
    litellm_key_header_name: str | None = None,
    forward_llm_provider_auth_headers: bool = False,
    authenticated_with_header: str | None = None,
) -> dict:
    """
    Removes litellm api key from headers

    Args:
        headers: Request headers
        litellm_key_header_name: Custom header name for LiteLLM API key
        forward_llm_provider_auth_headers: Whether to forward provider auth headers
        authenticated_with_header: Which header was used for LiteLLM authentication
            (e.g., "x-litellm-api-key", "authorization", "x-api-key")

    Returns:
        Cleaned headers dict
    """
    from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key

    clean_headers: Final = {}
    litellm_key_lower: Final = litellm_key_header_name.lower() if litellm_key_header_name is not None else None
    for header, value in headers.items():
        header_lower = header.lower()

        if header_lower == "authorization" and is_anthropic_oauth_key(value):
            if authenticated_with_header is None or authenticated_with_header.lower() != "authorization":
                clean_headers[header] = value
            continue
        # Special handling for x-api-key: forward it based on authenticated_with_header
        elif header_lower == "x-api-key":
            if forward_llm_provider_auth_headers and (
                authenticated_with_header is None or authenticated_with_header.lower() != "x-api-key"
            ):
                clean_headers[header] = value
        elif forward_llm_provider_auth_headers and header_lower in _SPECIAL_HEADERS_CACHE:
            if litellm_key_lower and header_lower == litellm_key_lower:
                continue
            if header_lower == "authorization":
                continue
            # Never forward x-litellm-api-key (it's for proxy auth only)
            if header_lower == "x-litellm-api-key":
                continue
            clean_headers[header] = value
        # Check if header should be excluded: either in special headers cache or matches custom litellm key
        elif header_lower not in _SPECIAL_HEADERS_CACHE and (
            litellm_key_lower is None or header_lower != litellm_key_lower
        ):
            clean_headers[header] = value
    return clean_headers


def _is_credential_header(header: str) -> bool:
    """Whether `header` carries a caller credential rather than request context."""
    return header.lower() in _CREDENTIAL_HEADER_NAMES


def redact_credential_headers(headers: Mapping[str, str]) -> Mapping[str, str]:
    """Return a copy of `headers` with credential-bearing values masked.

    `clean_headers` deliberately preserves some credential headers so they can be
    forwarded to the upstream provider; an Anthropic subscription OAuth token in
    `Authorization`, or a client-supplied provider key in `x-api-key`. Those values
    must never reach a logging callback or a spend log, so every observability-facing
    copy of the header dict is built through this helper while the copy that is
    forwarded upstream keeps the real values.

    The returned object is a plain dict; guardrail hooks stamp their own headers onto
    the stored copy and the logging callbacks JSON-serialize it.
    """
    return {
        header: (_REDACTED_HEADER_VALUE if _is_credential_header(header) else value)
        for header, value in headers.items()
    }


class LiteLLMProxyRequestSetup:
    @staticmethod
    def _get_timeout_from_request(headers: dict) -> float | None:
        """
        Workaround for client request from Vercel's AI SDK.

        Allow's user to set a timeout in the request headers.

        Example:

        ```js
        const openaiProvider = createOpenAI({
            baseURL: liteLLM.baseURL,
            apiKey: liteLLM.apiKey,
            compatibility: "compatible",
            headers: {
                "x-litellm-timeout": "90"
            },
        });
        ```
        """
        timeout_header: Final = headers.get("x-litellm-timeout", None)
        if timeout_header is not None:
            return float(timeout_header)
        return None

    @staticmethod
    def _get_stream_timeout_from_request(headers: dict) -> float | None:
        """
        Get the `stream_timeout` from the request headers.
        """
        stream_timeout_header: Final = headers.get("x-litellm-stream-timeout", None)
        if stream_timeout_header is not None:
            return float(stream_timeout_header)
        return None

    @staticmethod
    def _get_keepalive_seconds_from_request(headers: Mapping[str, str]) -> float | None:
        """
        Get `keepalive_seconds` from the request headers, for clients (e.g. the
        Vercel AI SDK) that can set custom headers more easily than extra body
        fields. Subject to the same deployment-level allow_client_keepalive_override
        gate as the request body field: see _resolve_keepalive_seconds.
        """
        keepalive_seconds_header: Final = headers.get("x-litellm-keepalive-seconds", None)
        if keepalive_seconds_header is not None:
            return float(keepalive_seconds_header)
        return None

    @staticmethod
    def _get_num_retries_from_request(headers: dict) -> int | None:
        """
        Workaround for client request from Vercel's AI SDK.
        """
        num_retries_header: Final = headers.get("x-litellm-num-retries", None)
        if num_retries_header is not None:
            return int(num_retries_header)
        return None

    @staticmethod
    def _get_spend_logs_metadata_from_request_headers(headers: dict) -> dict | None:
        """
        Get the `spend_logs_metadata` from the request headers.
        """
        from litellm.litellm_core_utils.safe_json_loads import safe_json_loads

        spend_logs_metadata_header: Final = headers.get("x-litellm-spend-logs-metadata", None)
        if spend_logs_metadata_header is not None:
            return safe_json_loads(spend_logs_metadata_header)
        return None

    @staticmethod
    def _get_forwardable_headers(
        headers: Headers | dict,
    ):
        """
        Get the headers that should be forwarded to the LLM Provider.

        Looks for any `x-` headers and sends them to the LLM Provider.

        [07/09/2025] - Support 'anthropic-beta' header as well.
        """
        forwarded_headers: Final = {}
        for header, value in headers.items():
            if (
                header.lower().startswith("x-")
                and not header.lower().startswith("x-stainless")
                or header.lower().startswith("anthropic-beta")
            ):  # causes openai sdk to fail
                forwarded_headers[header] = value

        return forwarded_headers

    @staticmethod
    def _get_case_insensitive_header(headers: dict, key: str) -> str | None:
        """
        Get a case-insensitive header from the headers dictionary.
        """
        for header, value in headers.items():
            if header.lower() == key.lower():
                return value
        return None

    @staticmethod
    def add_internal_user_from_user_mapping(
        general_settings: dict | None,
        user_api_key_dict: UserAPIKeyAuth,
        headers: dict,
    ) -> UserAPIKeyAuth:
        if general_settings is None:
            return user_api_key_dict
        user_header_mapping: Final = general_settings.get("user_header_mappings")
        if not user_header_mapping:
            return user_api_key_dict
        header_name: Final = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(user_header_mapping)
        if not header_name:
            return user_api_key_dict
        header_value: Final = LiteLLMProxyRequestSetup._get_case_insensitive_header(headers, header_name)
        if header_value:
            user_api_key_dict.user_id = header_value
            return user_api_key_dict
        return user_api_key_dict

    @staticmethod
    def get_user_from_headers(headers: dict, general_settings: dict | None = None) -> str | None:
        """
        Get the user from the specified header if `general_settings.user_header_name` is set.
        """
        if general_settings is None:
            return None

        header_name: Final = general_settings.get("user_header_name")
        if header_name is None or header_name == "":
            return None

        if not isinstance(header_name, str):
            raise TypeError(f"Expected user_header_name to be a str but got {type(header_name)}")

        user: Final = LiteLLMProxyRequestSetup._get_case_insensitive_header(headers, header_name)
        if user is not None:
            verbose_logger.info('found user "%s" in header "%s"', user, header_name)

        return user

    @staticmethod
    def get_openai_org_id_from_headers(headers: dict, general_settings: dict | None = None) -> str | None:
        """
        Get the OpenAI Org ID from the headers.
        """
        if general_settings is not None and general_settings.get("forward_openai_org_id") is not True:
            return None
        for header, value in headers.items():
            if header.lower() == "openai-organization":
                verbose_logger.info("found openai org id: %s, sending to llm", value)
                return value
        return None

    @staticmethod
    def add_headers_to_llm_call(headers: dict, user_api_key_dict: UserAPIKeyAuth) -> dict:
        """
        Add headers to the LLM call

        - Checks request headers for forwardable headers
        - Checks if user information should be added to the headers
        """

        returned_headers: Final = LiteLLMProxyRequestSetup._get_forwardable_headers(headers)

        if litellm.add_user_information_to_llm_headers is True:
            litellm_logging_metadata_headers: Final = LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(
                user_api_key_dict=user_api_key_dict
            )
            for k, v in litellm_logging_metadata_headers.items():
                if v is None:
                    continue
                # httpx requires header values to be str or bytes; coerce numbers/bools
                # to str and JSON-encode dict/list (e.g. user_api_key_spend is float,
                # user_api_key_auth_metadata is dict). See #27458.
                if isinstance(v, (dict, list)):
                    returned_headers[f"x-litellm-{k}"] = json.dumps(v)
                elif isinstance(v, (str, bytes)):
                    returned_headers[f"x-litellm-{k}"] = v
                else:
                    returned_headers[f"x-litellm-{k}"] = str(v)

        return returned_headers

    @staticmethod
    def add_headers_to_llm_call_by_model_group(data: dict, headers: dict, user_api_key_dict: UserAPIKeyAuth) -> dict:
        """
        Add headers to the LLM call by model group
        """
        from litellm.proxy.auth.auth_checks import _check_model_access_helper
        from litellm.proxy.proxy_server import llm_router

        data_model: Final = data.get("model")

        if (
            data_model is not None
            and litellm.model_group_settings is not None
            and litellm.model_group_settings.forward_client_headers_to_llm_api is not None
            and _check_model_access_helper(
                model=data_model,
                llm_router=llm_router,
                models=litellm.model_group_settings.forward_client_headers_to_llm_api,
                team_model_aliases=user_api_key_dict.team_model_aliases,
                team_id=user_api_key_dict.team_id,
            )  # handles aliases, wildcards, etc.
        ):
            _headers: Final = LiteLLMProxyRequestSetup.add_headers_to_llm_call(headers, user_api_key_dict)
            if _headers != {}:
                data["headers"] = _headers
        return data

    @staticmethod
    def get_internal_user_header_from_mapping(user_header_mapping) -> str | None:
        if not user_header_mapping:
            return None
        items: Final = user_header_mapping if isinstance(user_header_mapping, list) else [user_header_mapping]
        for item in items:
            if not isinstance(item, dict):
                continue
            role = item.get("litellm_user_role")
            header_name = item.get("header_name")
            if role is None or not header_name:
                continue
            if str(role).lower() == str(LitellmUserRoles.INTERNAL_USER).lower():
                return header_name
        return None

    @staticmethod
    def add_litellm_data_for_backend_llm_call(
        *,
        headers: dict,
        request_data: Mapping[str, Any],
        user_api_key_dict: UserAPIKeyAuth,
        general_settings: dict[str, Any] | None = None,
    ) -> LitellmDataForBackendLLMCall:
        """
        - Adds user from headers
        - Adds forwardable headers
        - Adds org id
        """
        data: Final = LitellmDataForBackendLLMCall()

        if general_settings and general_settings.get("forward_client_headers_to_llm_api") is True:
            _headers: Final = LiteLLMProxyRequestSetup.add_headers_to_llm_call(headers, user_api_key_dict)
            if _headers != {}:
                data["headers"] = _headers
        _organization: Final = LiteLLMProxyRequestSetup.get_openai_org_id_from_headers(headers, general_settings)
        if _organization is not None:
            data["organization"] = _organization

        header_timeout: Final = LiteLLMProxyRequestSetup._get_timeout_from_request(headers)
        if header_timeout is not None:
            data["timeout"] = header_timeout

        header_stream_timeout: Final = LiteLLMProxyRequestSetup._get_stream_timeout_from_request(headers)
        if header_stream_timeout is not None:
            data["stream_timeout"] = header_stream_timeout

        # Router._get_timeout resolves the effective per-attempt timeout from any of
        # kwargs["timeout"], kwargs["request_timeout"], or kwargs["stream_timeout"], and a
        # caller can supply any of those via the request body as well as the headers above.
        # A deliberately tiny value can force a 408 on every deployment in a fallback chain,
        # so this marker (never trusted verbatim from the client; stripped above) must cover
        # every source cooldown_handlers._trigger_cooldown_for_failed_deployment needs to
        # distinguish from a real deployment health signal.
        if (
            header_timeout is not None
            or header_stream_timeout is not None
            or request_data.get("timeout") is not None
            or request_data.get("request_timeout") is not None
            or request_data.get("stream_timeout") is not None
        ):
            data["client_side_timeout"] = True

        num_retries: Final = LiteLLMProxyRequestSetup._get_num_retries_from_request(headers)
        if num_retries is not None:
            data["num_retries"] = num_retries

        keepalive_seconds: Final = LiteLLMProxyRequestSetup._get_keepalive_seconds_from_request(headers)
        if keepalive_seconds is not None:
            data["keepalive_seconds"] = keepalive_seconds

        return data

    @staticmethod
    def add_litellm_metadata_from_request_headers(
        headers: dict,
        data: dict,
        _metadata_variable_name: str,
    ) -> dict:
        """
        Add litellm metadata from request headers

        Relevant issue: https://github.com/BerriAI/litellm/issues/14008
        """
        from litellm.proxy._types import LitellmMetadataFromRequestHeaders

        metadata_from_headers: Final = LitellmMetadataFromRequestHeaders()
        spend_logs_metadata: Final = LiteLLMProxyRequestSetup._get_spend_logs_metadata_from_request_headers(headers)
        if spend_logs_metadata is not None:
            metadata_from_headers["spend_logs_metadata"] = spend_logs_metadata

        #########################################################################################
        # Finally update the requests metadata with the `metadata_from_headers`
        #########################################################################################

        agent_id_from_header: Final = headers.get("x-litellm-agent-id")
        # Explicit litellm headers take precedence; fall back to any x-*-session-id header.
        chain_id: Final = get_chain_id_from_headers(dict(headers))

        if agent_id_from_header:
            metadata_from_headers["agent_id"] = agent_id_from_header
            verbose_proxy_logger.debug("Extracted agent_id from header: %s", agent_id_from_header)

        if chain_id:
            metadata_from_headers["trace_id"] = chain_id
            metadata_from_headers["session_id"] = chain_id
            data["litellm_session_id"] = chain_id
            data["litellm_trace_id"] = chain_id
            verbose_proxy_logger.debug("Extracted chain_id from header (trace-id/session-id): %s", chain_id)
        else:
            body_metadata: Final = data.get("metadata")
            session_id: Final = _get_anthropic_session_id_from_metadata(body_metadata)
            if session_id:
                metadata_from_headers["session_id"] = session_id
                data["litellm_session_id"] = session_id
                if isinstance(body_metadata, dict) and isinstance(body_metadata.get("user_id"), dict):
                    body_metadata["user_id"] = session_id
                verbose_proxy_logger.debug("Extracted session_id from Anthropic metadata.user_id")

        # Last-resort fallback: the W3C standards for trace/session propagation
        # (https://www.w3.org/TR/trace-context/, https://www.w3.org/TR/baggage/).
        # Lower priority than everything above - only fires when neither the
        # explicit litellm headers nor the Anthropic-metadata path found
        # anything - but lets a caller's existing traceparent/baggage headers
        # (from real OTel instrumentation) correlate with litellm's own logs
        # instead of generating an unrelated trace_id.
        normalized_headers: Final = MappingProxyType({k.lower(): v for k, v in headers.items() if isinstance(k, str)})
        if "litellm_trace_id" not in data:
            traceparent: Final = normalized_headers.get("traceparent")
            if isinstance(traceparent, str):
                trace_id_from_traceparent: Final = _trace_id_from_traceparent(traceparent)
                if trace_id_from_traceparent:
                    metadata_from_headers["trace_id"] = trace_id_from_traceparent
                    data["litellm_trace_id"] = trace_id_from_traceparent  # rebind-ok: data is an out-param
                    verbose_proxy_logger.debug(
                        "Extracted trace_id from W3C traceparent header: %s", trace_id_from_traceparent
                    )
        if "litellm_session_id" not in data:
            baggage: Final = normalized_headers.get("baggage")
            if isinstance(baggage, str):
                session_id_from_baggage: Final = _session_id_from_baggage(baggage)
                if session_id_from_baggage:
                    metadata_from_headers["session_id"] = session_id_from_baggage
                    data["litellm_session_id"] = session_id_from_baggage  # rebind-ok: data is an out-param
                    verbose_proxy_logger.debug("Extracted session_id from W3C baggage header")

        if isinstance(data[_metadata_variable_name], dict):
            data[_metadata_variable_name].update(metadata_from_headers)
        return data

    @staticmethod
    def get_sanitized_user_information_from_key(
        user_api_key_dict: UserAPIKeyAuth,
    ) -> StandardLoggingUserAPIKeyMetadata:
        user_api_key_logged_metadata: Final = StandardLoggingUserAPIKeyMetadata(
            user_api_key_hash=user_api_key_dict.api_key,  # just the hashed token
            user_api_key_alias=user_api_key_dict.key_alias,
            user_api_key_spend=user_api_key_dict.spend,
            user_api_key_max_budget=user_api_key_dict.max_budget,
            user_api_key_user_spend=user_api_key_dict.user_spend,
            user_api_key_user_max_budget=user_api_key_dict.user_max_budget,
            user_api_key_team_spend=user_api_key_dict.team_spend,
            user_api_key_team_max_budget=user_api_key_dict.team_max_budget,
            user_api_key_team_id=user_api_key_dict.team_id,
            user_api_key_project_id=user_api_key_dict.project_id,
            user_api_key_project_alias=user_api_key_dict.project_alias,
            user_api_key_user_id=user_api_key_dict.user_id,
            user_api_key_org_id=user_api_key_dict.org_id,
            user_api_key_org_alias=user_api_key_dict.organization_alias,
            user_api_key_team_alias=user_api_key_dict.team_alias,
            user_api_key_end_user_id=user_api_key_dict.end_user_id,
            user_api_key_user_email=user_api_key_dict.user_email,
            user_api_key_request_route=user_api_key_dict.request_route,
            user_api_key_budget_reset_at=(
                user_api_key_dict.budget_reset_at.isoformat() if user_api_key_dict.budget_reset_at else None
            ),
            user_api_key_auth_metadata=strip_callback_config(user_api_key_dict.metadata),
        )
        return user_api_key_logged_metadata

    @staticmethod
    def add_user_api_key_auth_to_request_metadata(
        data: dict,
        user_api_key_dict: UserAPIKeyAuth,
        _metadata_variable_name: str,
    ) -> dict:
        """
        Adds the `UserAPIKeyAuth` object to the request metadata.
        """
        user_api_key_logged_metadata: Final = LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(
            user_api_key_dict=user_api_key_dict
        )
        data[_metadata_variable_name].update(user_api_key_logged_metadata)
        data[_metadata_variable_name]["user_api_key"] = user_api_key_dict.api_key  # this is just the hashed token

        # Key-owned agent_id for spend attribution; keep existing (e.g. from header) if key has none
        _key_agent_id: Final = getattr(user_api_key_dict, "agent_id", None)
        _existing_agent_id: Final = data[_metadata_variable_name].get("agent_id")
        _resolved_agent_id: Final = _key_agent_id or _existing_agent_id
        data[_metadata_variable_name]["agent_id"] = _resolved_agent_id

        data[_metadata_variable_name]["user_api_end_user_max_budget"] = getattr(
            user_api_key_dict, "end_user_max_budget", None
        )
        if user_api_key_dict.budget_reservation is not None:
            data[_metadata_variable_name]["user_api_key_budget_reservation"] = user_api_key_dict.budget_reservation
        if user_api_key_dict.matched_model_access_groups:
            data[_metadata_variable_name][MODEL_ACCESS_GROUP_METADATA_KEY] = (
                user_api_key_dict.matched_model_access_groups
            )
        # UserAPIKeyAuth object for MCP server access control
        data[_metadata_variable_name]["user_api_key_auth"] = user_api_key_dict.model_copy(
            update={
                "metadata": strip_callback_config(user_api_key_dict.metadata),
                "team_metadata": strip_callback_config(user_api_key_dict.team_metadata),
                "project_metadata": strip_callback_config(user_api_key_dict.project_metadata),
                "organization_metadata": strip_callback_config(user_api_key_dict.organization_metadata),
            }
        )
        return data

    @staticmethod
    def add_management_endpoint_metadata_to_request_metadata(
        data: dict,
        management_endpoint_metadata: dict,
        _metadata_variable_name: str,
    ) -> dict:
        """
        Adds the `UserAPIKeyAuth` metadata to the request metadata.

        ignore any sensitive fields like logging, api_key, etc.
        """
        if _metadata_variable_name not in data:
            return data
        from litellm.proxy._types import (
            LiteLLM_ManagementEndpoint_MetadataFields,
            LiteLLM_ManagementEndpoint_MetadataFields_Premium,
        )

        # ignore any special fields
        added_metadata: Final = {
            k: v
            for k, v in (strip_callback_config(management_endpoint_metadata) or {}).items()
            if k not in (LiteLLM_ManagementEndpoint_MetadataFields_Premium + LiteLLM_ManagementEndpoint_MetadataFields)
        }
        if data[_metadata_variable_name].get("user_api_key_auth_metadata") is None:
            data[_metadata_variable_name]["user_api_key_auth_metadata"] = {}
        data[_metadata_variable_name]["user_api_key_auth_metadata"].update(added_metadata)
        return data

    @staticmethod
    def add_key_level_controls(key_metadata: dict | None, data: dict, _metadata_variable_name: str):
        if key_metadata is None:
            return data
        if "cache" in key_metadata:
            data["cache"] = {}
            if isinstance(key_metadata["cache"], dict):
                for k, v in key_metadata["cache"].items():
                    if k in SupportedCacheControls:
                        data["cache"][k] = v

        ## KEY-LEVEL SPEND LOGS / TAGS
        if "tags" in key_metadata and key_metadata["tags"] is not None:
            data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags(
                request_tags=data[_metadata_variable_name].get("tags"),
                tags_to_add=key_metadata["tags"],
            )
        if "disable_global_guardrails" in key_metadata and isinstance(key_metadata["disable_global_guardrails"], bool):
            data[_metadata_variable_name]["disable_global_guardrails"] = key_metadata["disable_global_guardrails"]
        if "spend_logs_metadata" in key_metadata and isinstance(key_metadata["spend_logs_metadata"], dict):
            if "spend_logs_metadata" in data[_metadata_variable_name] and isinstance(
                data[_metadata_variable_name]["spend_logs_metadata"], dict
            ):
                for key, value in key_metadata["spend_logs_metadata"].items():
                    if (
                        key not in data[_metadata_variable_name]["spend_logs_metadata"]
                    ):  # don't override k-v pair sent by request (user request)
                        data[_metadata_variable_name]["spend_logs_metadata"][key] = value
            else:
                data[_metadata_variable_name]["spend_logs_metadata"] = key_metadata["spend_logs_metadata"]

        ## KEY-LEVEL DISABLE FALLBACKS
        if "disable_fallbacks" in key_metadata and isinstance(key_metadata["disable_fallbacks"], bool):
            data["disable_fallbacks"] = key_metadata["disable_fallbacks"]

        if isinstance(key_metadata.get("enable_prompt_caching"), bool):
            data["enable_prompt_caching"] = key_metadata["enable_prompt_caching"]  # rebind-ok: data is an out-param

        ## KEY-LEVEL METADATA
        data = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata(
            data=data,
            management_endpoint_metadata=key_metadata,
            _metadata_variable_name=_metadata_variable_name,
        )
        return data

    @staticmethod
    def _merge_tags(request_tags: list | None, tags_to_add: list | None) -> list:
        """
        Helper function to merge two lists of tags, ensuring no duplicates.

        Args:
            request_tags (Optional[list]): List of tags from the original request
            tags_to_add (Optional[list]): List of tags to add

        Returns:
            list: Combined list of unique tags
        """
        final_tags: Final = []

        if request_tags and isinstance(request_tags, list):
            final_tags.extend(request_tags)

        if tags_to_add and isinstance(tags_to_add, list):
            for tag in tags_to_add:
                if tag not in final_tags:
                    final_tags.append(tag)

        return final_tags

    @staticmethod
    def add_team_based_callbacks_from_config(
        team_id: str,
        proxy_config: ProxyConfig,
    ) -> TeamCallbackMetadata | None:
        """
        Add team-based callbacks from the config
        """
        team_config: Final = proxy_config.load_team_config(team_id=team_id)
        if not isinstance(team_config, dict) or len(team_config) == 0:
            return None

        callback_vars_dict = {**team_config.get("callback_vars", team_config)}
        callback_vars_dict.pop("team_id", None)
        callback_vars_dict.pop("success_callback", None)
        callback_vars_dict.pop("failure_callback", None)
        callback_vars_dict = {
            key: (litellm.utils.get_secret(value, default_value=value) or value if isinstance(value, str) else value)
            for key, value in callback_vars_dict.items()
        }

        return TeamCallbackMetadata(
            success_callback=team_config.get("success_callback", None),
            failure_callback=team_config.get("failure_callback", None),
            callback_vars=callback_vars_dict,
        )

    @staticmethod
    def add_request_tag_to_metadata(
        llm_router: Router | None,
        headers: dict,
        data: dict,
    ) -> list[str] | None:
        tags = None

        # Check request headers for tags
        if "x-litellm-tags" in headers:
            if isinstance(headers["x-litellm-tags"], str):
                _tags: Final = headers["x-litellm-tags"].split(",")
                tags = [tag.strip() for tag in _tags]
            elif isinstance(headers["x-litellm-tags"], list):
                tags = headers["x-litellm-tags"]
        # Check request body for tags
        if "tags" in data and isinstance(data["tags"], list):
            tags = data["tags"]

        return tags

    @staticmethod
    def pre_seed_litellm_metadata_for_route(
        request_data: dict,
        route: str,
    ) -> None:
        """Pre-seed ``litellm_metadata`` for routes that track tags there.

        Routes in ``LITELLM_METADATA_ROUTES`` (e.g. Bedrock, ``/v1/messages``,
        responses, batches, files) store request-scoped tag metadata in
        ``litellm_metadata`` rather than the provider-facing ``metadata``
        field. ``get_metadata_variable_name_from_kwargs`` picks the target
        based on whether ``litellm_metadata`` is present, so it must be
        seeded BEFORE any tag merge runs; otherwise header tags from
        ``apply_client_tag_policy_pre_auth`` land in ``metadata`` while
        key tags from ``apply_key_tags_pre_auth`` and the read in
        ``_tag_max_budget_check`` resolve to ``litellm_metadata``, leaving
        header tags invisible to per-tag budget enforcement.
        """
        if any(metadata_route in route for metadata_route in LITELLM_METADATA_ROUTES):
            request_data.setdefault("litellm_metadata", {})

    @staticmethod
    def apply_key_tags_pre_auth(
        request_data: dict,
        user_api_key_dict: UserAPIKeyAuth,
    ) -> None:
        """Merge key metadata tags into request_data before _tag_max_budget_check."""
        key_metadata: Final = user_api_key_dict.metadata
        if not key_metadata:
            return

        key_tags: Final = key_metadata.get("tags")
        if not key_tags or not isinstance(key_tags, list):
            return

        _metadata_variable_name: Final = get_metadata_variable_name_from_kwargs(request_data)
        metadata = request_data.get(_metadata_variable_name)
        if isinstance(metadata, str):
            parsed: Final = safe_json_loads(metadata)
            metadata = parsed if isinstance(parsed, dict) else {}
            request_data[_metadata_variable_name] = metadata
        elif not isinstance(metadata, dict):
            metadata = {}
            request_data[_metadata_variable_name] = metadata

        existing_tags: Final = metadata.get("tags")
        metadata["tags"] = LiteLLMProxyRequestSetup._merge_tags(
            request_tags=existing_tags if isinstance(existing_tags, list) else None,
            tags_to_add=key_tags,
        )

    @staticmethod
    def apply_client_tag_policy_pre_auth(
        request: Request,
        request_data: dict,
        user_api_key_dict: UserAPIKeyAuth,
    ) -> None:
        """
        Merge ``x-litellm-tags`` header tags into ``request_data`` BEFORE
        auth budget gates run, so ``_tag_max_budget_check`` (which only
        inspects ``request_data``) sees them. Without this, header-tagged
        requests silently bypass per-tag budget enforcement.

        Why: ``add_litellm_data_to_request`` runs the equivalent merge
        post-auth, after ``_tag_max_budget_check`` has already executed.
        Header-supplied tags merged there are invisible to that check.
        Running the merge here closes that gap; the post-auth merge in
        ``add_litellm_data_to_request`` remains as defense-in-depth.

        How to apply: invoked from the auth chain just before
        ``common_checks``. Mutates ``request_data`` in place; idempotent
        when followed by ``add_litellm_data_to_request``.
        """
        # No allow_client_tags opt-in: caller-supplied tags always flow
        # into metadata.tags (see add_litellm_data_to_request). The pre-auth
        # merge mirrors that so _tag_max_budget_check sees the same tags.
        headers: Final = _safe_get_request_headers(request=request)
        raw_header_tags: Final = headers.get("x-litellm-tags")
        if not raw_header_tags:
            return

        if isinstance(raw_header_tags, str):
            header_tags: list[str] = [t.strip() for t in raw_header_tags.split(",") if t.strip()]
        elif isinstance(raw_header_tags, list):
            header_tags = [t for t in raw_header_tags if isinstance(t, str) and t]
        else:
            return

        if not header_tags:
            return

        # Match the metadata key that get_tags_from_request_body will read
        # from (litellm_metadata vs metadata) so the merged tags are visible
        # to _tag_max_budget_check.
        _metadata_variable_name: Final = get_metadata_variable_name_from_kwargs(request_data)
        metadata = request_data.get(_metadata_variable_name)
        # metadata can arrive as a JSON string (multipart/form-data, extra_body).
        # Parse it so existing tags survive the merge — overwriting the string
        # with {} would let a caller bypass _tag_max_budget_check on an
        # over-budget body tag by also sending a within-budget header tag.
        if isinstance(metadata, str):
            parsed: Final = safe_json_loads(metadata)
            metadata = parsed if isinstance(parsed, dict) else {}
            request_data[_metadata_variable_name] = metadata
        elif not isinstance(metadata, dict):
            metadata = {}
            request_data[_metadata_variable_name] = metadata

        existing_tags: Final = metadata.get("tags")
        metadata["tags"] = LiteLLMProxyRequestSetup._merge_tags(
            request_tags=existing_tags if isinstance(existing_tags, list) else None,
            tags_to_add=header_tags,
        )


def refresh_proxy_server_request_body_snapshot(
    data: MutableMapping[str, object],
) -> None:
    """
    Re-snapshot ``data["proxy_server_request"]["body"]`` from the current state of ``data``.

    ``add_litellm_data_to_request`` takes the initial snapshot before guardrails
    (pre_call_hook) run. A guardrail that masks PII/PCI in place (e.g. Presidio)
    mutates ``data`` afterward, so callers that persist ``proxy_server_request.body``
    for audit/spend-tracking purposes must call this again post-guardrail, or the
    persisted body silently bypasses whatever masking the guardrail applied.

    By the time a caller refreshes post-guardrail, ``litellm.utils.function_setup``
    has already stamped ``data["litellm_logging_obj"]`` with a live (non-serializable)
    ``Logging`` instance, so it must be excluded here the same way ``secret_fields``
    and ``proxy_server_request`` are.
    """
    proxy_server_request = data.get("proxy_server_request")
    if not isinstance(proxy_server_request, dict):
        return
    _body_snapshot_exclude = (
        frozenset({"secret_fields", "proxy_server_request", "litellm_logging_obj"}) | _TRANSPORT_ONLY_CREDENTIAL_KEYS
    )
    proxy_server_request["body"] = {k: v for k, v in data.items() if k not in _body_snapshot_exclude}


async def add_litellm_data_to_request(
    data: dict,
    request: Request,
    user_api_key_dict: UserAPIKeyAuth,
    proxy_config: ProxyConfig,
    general_settings: dict[str, Any] | None = None,
    version: str | None = None,
):
    """
    Adds LiteLLM-specific data to the request.

    Args:
        data (dict): The data dictionary to be modified.
        request (Request): The incoming request.
        user_api_key_dict (UserAPIKeyAuth): The user API key dictionary.
        general_settings (Optional[Dict[str, Any]], optional): General settings. Defaults to None.
        version (Optional[str], optional): Version. Defaults to None.

    Returns:
        dict: The modified data dictionary.

    """

    from litellm.proxy.proxy_server import llm_router, premium_user
    from litellm.types.proxy.litellm_pre_call_utils import RedactedDict, SecretFields

    # Strip internal-only keys from user input before the proxy sets its own.
    # These keys are injected by the proxy itself below — user-supplied values
    # must not be trusted.
    _allow_client_mock_response: Final = _key_or_team_allows_client_mock_response(user_api_key_dict)
    _allow_client_message_redaction_opt_out = _key_or_team_allows_client_message_redaction_opt_out(user_api_key_dict)
    for _internal_key in _UNTRUSTED_ROOT_CONTROL_FIELDS:
        if _allow_client_mock_response and _internal_key in _CLIENT_MOCK_CONTROL_FIELDS:
            continue
        data.pop(_internal_key, None)
    _reject_url_valued_destinations(data)
    _raw_metadata_by_field: Final = {
        _metadata_field: data.pop(_metadata_field)
        for _metadata_field in ("metadata", "litellm_metadata")
        if data.get(_metadata_field) is not None
    }
    for _metadata_field, _raw_metadata in _raw_metadata_by_field.items():
        data[_metadata_field] = _normalized_metadata_object(_metadata_field, _raw_metadata)
    # Strip spoofable auth metadata from user-supplied metadata dict
    _user_metadata = data.get("metadata")
    if isinstance(_user_metadata, dict):
        for _mk in list(_user_metadata.keys()):
            if _mk.startswith("user_api_key_"):
                del _user_metadata[_mk]

    _raw_headers: Final[dict[str, str]] = RedactedDict(_safe_get_request_headers(request))

    forward_llm_auth = False
    if general_settings:
        forward_llm_auth = general_settings.get("forward_llm_provider_auth_headers", False)
    if not forward_llm_auth:
        forward_llm_auth = getattr(litellm, "forward_llm_provider_auth_headers", False)
    # Determine which header was used for authentication
    # This enables forwarding provider keys (e.g., x-api-key) when they weren't used for LiteLLM auth
    authenticated_with_header = None
    if "x-litellm-api-key" in request.headers:
        # If x-litellm-api-key is present, it was used for auth
        authenticated_with_header = "x-litellm-api-key"
    elif "authorization" in request.headers:
        # Authorization header was used for auth
        authenticated_with_header = "authorization"
    else:
        # x-api-key or another header was used for auth
        authenticated_with_header = "x-api-key"

    _headers: Final[dict[str, str]] = clean_headers(
        request.headers,
        litellm_key_header_name=(
            general_settings.get("litellm_key_header_name") if general_settings is not None else None
        ),
        forward_llm_provider_auth_headers=forward_llm_auth,
        authenticated_with_header=authenticated_with_header,
    )
    _strip_untrusted_request_header_controls(
        _headers,
        allow_client_message_redaction_opt_out=_allow_client_message_redaction_opt_out,
    )
    _logging_safe_headers: Final = redact_credential_headers(_headers)
    verbose_proxy_logger.debug("Request Headers: %s", _logging_safe_headers)
    verbose_proxy_logger.debug("Raw Headers: %s", _raw_headers)

    if forward_llm_auth and "x-api-key" in _headers:
        data["api_key"] = _headers["x-api-key"]
        verbose_proxy_logger.debug(
            "Setting client-provided x-api-key as api_key parameter (will override deployment key)"
        )

    ##########################################################
    # Init - Proxy Server Request
    # we do this as soon as entering so we track the original request
    ##########################################################
    # Track arrival time for queue time metric. Prefer the timestamp stamped at
    # the top of user_api_key_auth (request.state.litellm_received_at): by the
    # time this function runs, auth has already completed, so time.time() here
    # would silently exclude the entire auth phase from the queue-time window.
    # Falls back to time.time() for callers that never went through
    # user_api_key_auth. The body snapshot is filled in after the
    # admin-injection strip below so the audit / spend-tracking consumers of
    # proxy_server_request["body"] see the cleaned metadata rather than
    # attacker-forged user_api_key_* fields.
    _litellm_received_at: Final = getattr(request.state, "litellm_received_at", None)
    arrival_time: Final = _litellm_received_at.timestamp() if _litellm_received_at is not None else time.time()
    data["proxy_server_request"] = {
        "url": str(request.url),
        "method": request.method,
        "headers": _logging_safe_headers,
        "body": None,  # filled in post-strip; see below
        "arrival_time": arrival_time,  # Track when request arrived at proxy
    }

    safe_add_api_version_from_query_params(data, request)
    _metadata_variable_name: Final = _get_metadata_variable_name(request)
    if data.get(_metadata_variable_name, None) is None:
        data[_metadata_variable_name] = {}

    data.update(
        LiteLLMProxyRequestSetup.add_litellm_data_for_backend_llm_call(
            headers=_headers,
            request_data=data,
            user_api_key_dict=user_api_key_dict,
            general_settings=general_settings,
        )
    )

    LiteLLMProxyRequestSetup.add_litellm_metadata_from_request_headers(
        headers=_headers,
        data=data,
        _metadata_variable_name=_metadata_variable_name,
    )

    # Expose request headers under the metadata field for guardrails (fixes #17477)
    if _metadata_variable_name in data and isinstance(data[_metadata_variable_name], dict):
        data[_metadata_variable_name]["headers"] = _logging_safe_headers

    # check for forwardable headers
    data = LiteLLMProxyRequestSetup.add_headers_to_llm_call_by_model_group(
        data=data, headers=_headers, user_api_key_dict=user_api_key_dict
    )

    user_api_key_dict = LiteLLMProxyRequestSetup.add_internal_user_from_user_mapping(
        general_settings, user_api_key_dict, _headers
    )

    # Parse user info from headers (fallback to general_settings.user_header_name)
    user: Final = LiteLLMProxyRequestSetup.get_user_from_headers(_headers, general_settings)
    if user is not None:
        if user_api_key_dict.end_user_id is None:
            user_api_key_dict.end_user_id = user
        if "user" not in data:
            data["user"] = user

    if litellm.overwrite_user_with_key_hash is True:
        stampable_hash: Final = _stampable_key_hash(user_api_key_dict)
        if stampable_hash is not None:
            data["user"] = stampable_hash

    data["secret_fields"] = SecretFields(raw_headers=_raw_headers)

    ## Dynamic api version (Azure OpenAI endpoints) ##
    try:
        query_params: Final = request.query_params
        # Convert query parameters to a dictionary (optional)
        query_dict = dict(query_params)
    except KeyError:
        query_dict = {}

    ## check for api version in query params
    dynamic_api_version: Final[str | None] = query_dict.get("api-version")

    if dynamic_api_version is not None:  # only pass, if set
        data["api_version"] = dynamic_api_version

    ## Forward any LLM API Provider specific headers in extra_headers
    add_provider_specific_headers_to_request(data=data, headers=_headers)

    ## Cache Controls
    cache_control_header: Final = _headers.get("Cache-Control", None)
    if cache_control_header:
        cache_dict: Final = parse_cache_control(cache_control_header)
        data["ttl"] = cache_dict.get("s-maxage")

    # requester_metadata is snapshotted AFTER the strip below so
    # downstream consumers (e.g. PANW guardrail reading user_ip /
    # profile_id) don't see attacker-injected admin slots preserved in
    # the deepcopy.

    # Strip internal pipeline state and admin-injection slots from user input.
    # Runs AFTER the string-to-dict parse above so JSON-string metadata (sent
    # via multipart/form-data or extra_body) cannot smuggle admin fields past
    # the isinstance(dict) guard.
    #
    # The proxy populates a family of ``user_api_key_*`` fields below
    # (user_api_key_metadata, user_api_key_user_id, user_api_key_alias,
    # user_api_key_spend, user_api_key_team_metadata, …) into
    # data[_metadata_variable_name]. Because the proxy only writes to ONE of
    # the two metadata dicts, a caller pre-populating any of these keys on
    # the OTHER metadata dict would have their forged values surface in
    # guardrails, spend tracking, audit logs, and identity resolution. Strip
    # by prefix so new ``user_api_key_*`` fields added in the future are
    # covered without per-key maintenance.
    for _meta_key in ("metadata", "litellm_metadata"):
        _user_meta = data.get(_meta_key)
        if isinstance(_user_meta, dict):
            _strip_untrusted_request_header_controls(
                _user_meta.get("headers"),
                allow_client_message_redaction_opt_out=(_allow_client_message_redaction_opt_out),
            )
            for _k in [
                k for k in _user_meta if k.startswith("user_api_key_") or k in _UNTRUSTED_METADATA_CONTROL_FIELDS
            ]:
                _user_meta.pop(_k, None)

    # Strip pricing overrides AFTER the litellm_metadata string-to-dict parse
    # above, for the same reason as the user_api_key_* strip — JSON-string
    # metadata (sent via multipart/form-data or extra_body) wouldn't be a
    # dict yet at the earlier strip point and the isinstance(dict) guard
    # would silently skip the field.
    if not _key_or_team_allows_client_pricing_override(user_api_key_dict):
        _strip_client_pricing_overrides(data)
    _strip_router_reserved_metadata(data)

    # Same reason as the strips above: runs after the metadata string-to-dict parse
    # so JSON-string metadata cannot smuggle callback credentials past the dict guard.
    _strip_client_callback_credentials(data)

    if not _allow_client_message_redaction_opt_out and litellm.turn_off_message_logging is True:
        _strip_client_message_redaction_opt_out(data)

    # Fill in the proxy_server_request body snapshot now that metadata has
    # been parsed. Consumers (standard_logging_payload, lago,
    # spend_tracking_utils, streaming_iterator) read `body` to audit the
    # request; taking the snapshot here ensures they see cleaned metadata.
    #
    # Exclude:
    #   - secret_fields: contains raw_headers with Authorization tokens; must
    #     never be persisted in spend logs or any other audit trail.
    #   - proxy_server_request: already a key on `data` at this point (set
    #     earlier in this function); including it would make the snapshot
    #     self-reference — body.proxy_server_request.body would be the same
    #     dict as body, producing an infinite traversal loop for any consumer
    #     that walks the structure.
    refresh_proxy_server_request_body_snapshot(data)

    # Snapshot the requester-supplied metadata for downstream consumers.
    # Taking the deepcopy after the user_api_key_* / _pipeline_managed_guardrails
    # strip above prevents those proxy-internal slots — if a caller forged
    # them — from leaking into requester_metadata where guardrails and audit
    # paths may read from it.
    if "metadata" in data and isinstance(data["metadata"], dict):
        data[_metadata_variable_name]["requester_metadata"] = copy.deepcopy(data["metadata"])
        if _metadata_variable_name == "litellm_metadata":
            data[_metadata_variable_name].update(
                _promoted_trace_control_fields(
                    requester_metadata=data[_metadata_variable_name]["requester_metadata"],
                    litellm_metadata=data[_metadata_variable_name],
                )
            )

    # Merge litellm_metadata into the metadata variable (preserving existing
    # values). Runs after the user_api_key_* / _pipeline_managed_guardrails
    # strip above so those proxy-internal slots — if a caller forged them
    # into litellm_metadata — cannot cross-contaminate the admin-authoritative
    # metadata dict.
    if "litellm_metadata" in data and isinstance(data["litellm_metadata"], dict):
        for key, value in data["litellm_metadata"].items():
            if key not in data[_metadata_variable_name]:
                data[_metadata_variable_name][key] = value
        if _metadata_variable_name == "metadata":
            data["metadata"]["tags"] = LiteLLMProxyRequestSetup._merge_tags(  # pyright: ignore[reportPrivateUsage]  # same-module helper, budget blocks the unsuppressed idiom sibling call sites use
                request_tags=data["metadata"].get("tags"),
                tags_to_add=data["litellm_metadata"].get("tags"),
            )
    if _metadata_variable_name == "metadata":
        data.pop("litellm_metadata", None)

    data = LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata(
        data=data,
        user_api_key_dict=user_api_key_dict,
        _metadata_variable_name=_metadata_variable_name,
    )
    data[_metadata_variable_name]["litellm_api_version"] = version

    if general_settings is not None:
        data[_metadata_variable_name]["global_max_parallel_requests"] = general_settings.get(
            "global_max_parallel_requests", None
        )

    ### KEY-LEVEL Controls
    key_metadata: Final = user_api_key_dict.metadata
    data = LiteLLMProxyRequestSetup.add_key_level_controls(
        key_metadata=key_metadata,
        data=data,
        _metadata_variable_name=_metadata_variable_name,
    )
    ## TEAM-LEVEL SPEND LOGS/TAGS
    team_metadata: Final = user_api_key_dict.team_metadata or {}
    if "tags" in team_metadata and team_metadata["tags"] is not None:
        data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags(
            request_tags=data[_metadata_variable_name].get("tags"),
            tags_to_add=team_metadata["tags"],
        )
    if "disable_global_guardrails" in team_metadata and isinstance(team_metadata["disable_global_guardrails"], bool):
        data[_metadata_variable_name]["disable_global_guardrails"] = team_metadata["disable_global_guardrails"]
    if "opted_out_global_guardrails" in team_metadata and isinstance(
        team_metadata["opted_out_global_guardrails"], list
    ):
        data[_metadata_variable_name]["opted_out_global_guardrails"] = team_metadata["opted_out_global_guardrails"]
    if "spend_logs_metadata" in team_metadata and isinstance(team_metadata["spend_logs_metadata"], dict):
        if "spend_logs_metadata" in data[_metadata_variable_name] and isinstance(
            data[_metadata_variable_name]["spend_logs_metadata"], dict
        ):
            for key, value in team_metadata["spend_logs_metadata"].items():
                if (
                    key not in data[_metadata_variable_name]["spend_logs_metadata"]
                ):  # don't override k-v pair sent by request (user request)
                    data[_metadata_variable_name]["spend_logs_metadata"][key] = value
        else:
            data[_metadata_variable_name]["spend_logs_metadata"] = team_metadata["spend_logs_metadata"]

    ## PROJECT-LEVEL TAGS
    project_metadata: Final = user_api_key_dict.project_metadata or {}
    if "tags" in project_metadata and project_metadata["tags"] is not None:
        data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags(
            request_tags=data[_metadata_variable_name].get("tags"),
            tags_to_add=project_metadata["tags"],
        )

    # inherited_tags: every tag key/team/project policy contributed, read
    # directly from those three sources rather than snapshotted off the shared
    # "tags" list. A pre-auth pass (apply_client_tag_policy_pre_auth, run from
    # user_api_key_auth for _tag_max_budget_check) may already have merged the
    # caller's own header tags into that same list before this function ever
    # runs, so a snapshot taken here -- at any point in this function -- would
    # misattribute caller-supplied tags as policy-backed. tag_based_routing.py's
    # allow_fail_open reads this (rather than subtracting caller_tags from the
    # final merged set) so a caller can't strip an inherited "!"/"&"
    # constraint's protection just by resubmitting its exact value alongside a
    # conflicting one.
    _key_tags: Final = (key_metadata or MappingProxyType({})).get("tags") or ()
    _team_tags: Final = team_metadata.get("tags") or ()
    _project_tags: Final = project_metadata.get("tags") or ()
    data[_metadata_variable_name]["inherited_tags"] = tuple(  # rebind-ok: matches this file's data[...] mutation idiom
        dict.fromkeys((*_key_tags, *_team_tags, *_project_tags))
    )

    ## TEAM-LEVEL METADATA
    data = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata(
        data=data,
        management_endpoint_metadata=team_metadata,
        _metadata_variable_name=_metadata_variable_name,
    )

    # A key's OTel service name outranks its team's, so the key's values are
    # re-applied after the last-writer-wins team metadata merge above
    _key_otel_service_names: Final = {
        field: value
        for field, value in (key_metadata or {}).items()
        if field in OTEL_SERVICE_NAME_METADATA_KEYS and isinstance(value, str) and value.strip()
    }
    data = LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata(
        data=data,
        management_endpoint_metadata=_key_otel_service_names,
        _metadata_variable_name=_metadata_variable_name,
    )

    # Team spend, budget - used by prometheus.py
    data[_metadata_variable_name]["user_api_key_team_max_budget"] = user_api_key_dict.team_max_budget
    data[_metadata_variable_name]["user_api_key_team_spend"] = user_api_key_dict.team_spend
    data[_metadata_variable_name]["user_api_key_request_route"] = user_api_key_dict.request_route

    # API Key spend, budget - used by prometheus.py
    data[_metadata_variable_name]["user_api_key_spend"] = user_api_key_dict.spend
    data[_metadata_variable_name]["user_api_key_max_budget"] = user_api_key_dict.max_budget
    data[_metadata_variable_name]["user_api_key_model_max_budget"] = user_api_key_dict.model_max_budget
    data[_metadata_variable_name]["user_api_key_end_user_model_max_budget"] = (
        user_api_key_dict.end_user_model_max_budget
    )

    # User spend, budget - used by prometheus.py
    # Follow same pattern as team and API key budgets
    data[_metadata_variable_name]["user_api_key_user_spend"] = user_api_key_dict.user_spend
    data[_metadata_variable_name]["user_api_key_user_max_budget"] = user_api_key_dict.user_max_budget
    user_model_budget: Final = user_api_key_dict.user_model_max_budget
    data[_metadata_variable_name]["user_api_key_user_model_max_budget"] = user_model_budget  # rebind-ok: out-param

    data[_metadata_variable_name]["user_api_key_metadata"] = strip_callback_config(user_api_key_dict.metadata)
    data[_metadata_variable_name]["user_api_key_team_metadata"] = strip_callback_config(user_api_key_dict.team_metadata)
    data[_metadata_variable_name]["user_api_key_object_permission_id"] = getattr(
        user_api_key_dict, "object_permission_id", None
    )
    data[_metadata_variable_name]["user_api_key_team_object_permission_id"] = getattr(
        user_api_key_dict, "team_object_permission_id", None
    )
    data[_metadata_variable_name]["headers"] = _logging_safe_headers
    data[_metadata_variable_name]["endpoint"] = str(request.url)
    # Carry the proxy-receive instant via metadata (like `endpoint`) so the
    # OTel layer can compute pre-request latency, including on the failure
    # path after the logging object is popped.
    data[_metadata_variable_name]["litellm_received_at"] = getattr(request.state, "litellm_received_at", None)

    # OTEL Controls / Tracing
    # Add the OTEL Parent Trace before sending it LiteLLM
    data[_metadata_variable_name]["litellm_parent_otel_span"] = user_api_key_dict.parent_otel_span
    _add_otel_traceparent_to_data(data, request=request)

    ### END-USER SPECIFIC PARAMS ###
    if user_api_key_dict.allowed_model_region is not None:
        data["allowed_model_region"] = user_api_key_dict.allowed_model_region
    start_time: Final = time.time()
    ## [Enterprise Only]
    # Add User-IP Address
    requester_ip_address = ""
    if True:  # Always set the IP Address if available
        # logic for tracking IP Address

        # logic for tracking IP Address
        if (
            general_settings is not None
            and general_settings.get("use_x_forwarded_for") is True
            and request is not None
            and hasattr(request, "headers")
            and "x-forwarded-for" in request.headers
        ):
            requester_ip_address = request.headers["x-forwarded-for"]
        elif (
            request is not None
            and hasattr(request, "client")
            and hasattr(request.client, "host")
            and request.client is not None
        ):
            requester_ip_address = request.client.host
    data[_metadata_variable_name]["requester_ip_address"] = requester_ip_address

    # Add User-Agent
    user_agent = ""
    if request is not None and hasattr(request, "headers") and "user-agent" in request.headers:
        user_agent = request.headers["user-agent"]
    data[_metadata_variable_name]["user_agent"] = user_agent

    if should_auto_drop_params_for_agentic_cli(user_agent, data, proxy_config):
        data["drop_params"] = True

    # Merge caller-supplied tags (x-litellm-tags header, data["tags"] root-level)
    # into request metadata for tag-based routing and spend attribution.
    tags: Final = LiteLLMProxyRequestSetup.add_request_tag_to_metadata(
        llm_router=llm_router,
        headers=_headers,
        data=data,
    )

    if tags is not None:
        data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags(
            request_tags=data[_metadata_variable_name].get("tags"),
            tags_to_add=tags,
        )

    _caller_body_metadata: Final = data.get("metadata") if _metadata_variable_name != "metadata" else None
    _caller_body_tags: Final = (
        _caller_body_metadata.get("tags")
        if isinstance(_caller_body_metadata, dict) and isinstance(_caller_body_metadata.get("tags"), list)
        else None
    )
    if _caller_body_tags:
        data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags(  # rebind-ok: matches file idiom
            request_tags=data[_metadata_variable_name].get("tags"),
            tags_to_add=_caller_body_tags,
        )

    # caller_tags: exactly what this request itself supplied (x-litellm-tags header,
    # body "tags", or body "metadata.tags" on litellm_metadata routes), never
    # anything from key/team/project metadata. Read directly from the header and
    # body values here, the same way inherited_tags above is read directly from
    # key/team/project metadata -- neither is derived by inspecting the shared
    # "tags" list, which a pre-auth pass (apply_client_tag_policy_pre_auth) may
    # have already merged caller header tags into before this function runs.
    data[_metadata_variable_name]["caller_tags"] = tuple(  # rebind-ok: matches file idiom
        dict.fromkeys((*(tags or ()), *(_caller_body_tags or ())))
    )

    # Team Callbacks controls
    callback_settings_obj: Final = _get_dynamic_logging_metadata(
        user_api_key_dict=user_api_key_dict, proxy_config=proxy_config
    )
    if callback_settings_obj is not None:
        data["success_callback"] = callback_settings_obj.success_callback
        data["failure_callback"] = callback_settings_obj.failure_callback

        if callback_settings_obj.callback_vars is not None:
            # unpack callback_vars in data
            for k, v in callback_settings_obj.callback_vars.items():
                data[k] = v
            # Callbacks that must not honour request-supplied credentials read this
            # proxy-owned field instead of the raw request kwargs.
            data[TRUSTED_CALLBACK_VARS_FIELD] = callback_settings_obj.callback_vars

    # Add disabled callbacks from key metadata
    if user_api_key_dict.metadata and "litellm_disabled_callbacks" in user_api_key_dict.metadata:
        disabled_callbacks: Final = user_api_key_dict.metadata["litellm_disabled_callbacks"]
        if disabled_callbacks and isinstance(disabled_callbacks, list):
            data["litellm_disabled_callbacks"] = disabled_callbacks

    # Guardrails from key/team metadata and policy engine
    await move_guardrails_to_metadata(
        data=data,
        _metadata_variable_name=_metadata_variable_name,
        user_api_key_dict=user_api_key_dict,
    )

    # Save pre-alias model name for credential override lookup
    _pre_alias_model: Final = data.get("model")

    # Team Model Aliases
    _update_model_if_team_alias_exists(
        data=data,
        user_api_key_dict=user_api_key_dict,
    )

    # Key Model Aliases
    _update_model_if_key_alias_exists(
        data=data,
        user_api_key_dict=user_api_key_dict,
    )

    verbose_proxy_logger.debug("[PROXY] returned data from litellm_pre_call_utils: %s", data)

    # Team/Project credential overrides from model_config
    # Placed after the debug log to avoid leaking credential secrets in logs
    _apply_credential_overrides_from_model_config(
        data=data,
        user_api_key_dict=user_api_key_dict,
        pre_alias_model_name=_pre_alias_model,
        llm_router=llm_router,
    )

    ## ENFORCED PARAMS CHECK
    # loop through each enforced param
    # example enforced_params ['user', 'metadata', 'metadata.generation_name']
    _enforced_params_check(
        request_body=data,
        general_settings=general_settings,
        user_api_key_dict=user_api_key_dict,
        premium_user=premium_user,
    )

    end_time: Final = time.time()
    asyncio.create_task(
        service_logger_obj.async_service_success_hook(
            service=ServiceTypes.PROXY_PRE_CALL,
            duration=end_time - start_time,
            call_type="add_litellm_data_to_request",
            start_time=start_time,
            end_time=end_time,
            parent_otel_span=user_api_key_dict.parent_otel_span,
        )
    )

    return data


def _warn_stale_team_alias_once(warning_key: str, message: str, *args: str) -> None:
    if warning_key in _STALE_TEAM_ALIAS_WARNING_KEYS:
        return
    _STALE_TEAM_ALIAS_WARNING_KEYS[warning_key] = None
    while len(_STALE_TEAM_ALIAS_WARNING_KEYS) > _MAX_STALE_ALIAS_WARNING_KEYS:
        _STALE_TEAM_ALIAS_WARNING_KEYS.popitem(last=False)
    verbose_proxy_logger.warning(message, *args)


def _update_model_if_team_alias_exists(
    data: dict,
    user_api_key_dict: UserAPIKeyAuth,
) -> None:
    """
    Update the model if the team alias exists

    If a alias map has been set on a team, then we want to make the request with the model the team alias is pointing to

    eg.
        - user calls `gpt-4o`
        - team.model_alias_map = {
            "gpt-4o": "gpt-4o-team-1"
        }
        - requested_model = "gpt-4o-team-1"

    Note: model_aliases for team models are deprecated. This function only applies
    to legacy non-team-scoped aliases. Team-scoped deployments use team_public_model_name
    and are resolved via map_team_model in route_llm_request.

    An alias that targets a team-scoped internal name (``model_name_{team_id}_{uuid}``)
    with no live deployment behind it is never applied: the deployment was deleted, so
    the rewrite could only fail with an error naming a model the caller never sent.
    Keeping the requested model name lets it resolve against the deployments that still
    exist (e.g. a gateway-level model group shared with the team).
    """
    _model: Final = data.get("model")
    if not _model or not user_api_key_dict.team_model_aliases or _model not in user_api_key_dict.team_model_aliases:
        return

    from litellm.proxy.proxy_server import llm_router

    # Skip alias rewrite if this model resolves to team-specific deployments
    # (team models use team_public_model_name, not model_aliases)
    aliased_target: Final = user_api_key_dict.team_model_aliases[_model]

    # Optional bypass for stale aliases from pre-PR deployments:
    # only enabled via feature flag to preserve backwards compatibility.
    # Cached at module level to avoid hot-path secret lookups on every request.
    global _ENABLE_TEAM_STALE_ALIAS_BYPASS
    if _ENABLE_TEAM_STALE_ALIAS_BYPASS is None:
        _ENABLE_TEAM_STALE_ALIAS_BYPASS = get_secret_bool("LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS", False)
    enable_stale_alias_bypass: Final = _ENABLE_TEAM_STALE_ALIAS_BYPASS
    # Check if the alias points to a team-scoped UUID name
    # (format: "model_name_{team_id}_{uuid}")
    is_stale_team_alias: Final = aliased_target.startswith(f"model_name_{user_api_key_dict.team_id}_")
    if is_stale_team_alias and llm_router:
        if aliased_target not in llm_router.model_name_to_deployment_indices:
            _warn_stale_team_alias_once(
                f"deleted:{user_api_key_dict.team_id}:{_model}:{aliased_target}",
                "Team model alias for model='%s', team_id='%s' targets '%s', which has no live "
                "deployment. Routing with the requested model name instead; remove the stale "
                "entry from the team's model_aliases to silence this warning.",
                _sanitize_for_log(_model),
                _sanitize_for_log(user_api_key_dict.team_id),
                _sanitize_for_log(aliased_target),
            )
            return
        # This is a stale alias from pre-PR deployments.
        # Check if current team deployments exist for the public name.
        key: Final = (user_api_key_dict.team_id, _model)
        if key in llm_router.team_model_to_deployment_indices:
            if enable_stale_alias_bypass:
                # Team deployments exist; skip stale alias
                return
            _warn_stale_team_alias_once(
                f"{user_api_key_dict.team_id}:{_model}:{aliased_target}",
                "Stale team model alias detected for model='%s', team_id='%s'. "
                "New sibling deployments may be unreachable. "
                "Set LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS=true to enable "
                "team-scoped sibling routing.",
                _sanitize_for_log(_model),
                _sanitize_for_log(user_api_key_dict.team_id),
            )

    data["model"] = aliased_target


def _update_model_if_key_alias_exists(
    data: dict,
    user_api_key_dict: UserAPIKeyAuth,
) -> None:
    """
    Update the model if the key alias exists

    If an alias map has been set on a key, then we want to make the request with the model the key alias is pointing to

    eg.
        - user calls `modelAlias`
        - key.aliases = {
            "modelAlias": "xai/grok-4-fast-non-reasoning"
        }
        - requested_model = "xai/grok-4-fast-non-reasoning"
    """
    _model: Final = data.get("model")
    if (
        _model
        and user_api_key_dict.aliases
        and isinstance(user_api_key_dict.aliases, dict)
        and _model in user_api_key_dict.aliases
    ):
        data["model"] = user_api_key_dict.aliases[_model]


def _apply_credential_overrides_from_model_config(
    data: dict,
    user_api_key_dict: UserAPIKeyAuth,
    pre_alias_model_name: str | None = None,
    llm_router: Router | None = None,
) -> None:
    """
    Walk the model_config precedence chain in team/project metadata.
    If a matching credential is found, set api_base/api_key/api_version on data
    so they override deployment defaults in the router.

    Precedence (highest to lowest):
    1. Clientside credentials (already in data — skip if present)
    2. Project model-specific override
    3. Project default override (defaultconfig)
    4. Team model-specific override
    5. Team default override (defaultconfig)
    6. Deployment default (no action needed)
    """
    # Feature flag gate — disabled by default, opt in with litellm.enable_model_config_credential_overrides = True
    if not litellm.enable_model_config_credential_overrides:
        return

    # Respect clientside credentials — highest precedence
    if data.get("api_base") is not None or data.get("api_key") is not None:
        return

    model_name: Final = data.get("model")
    if not model_name:
        return

    project_metadata: Final = user_api_key_dict.project_metadata or {}
    team_metadata: Final = user_api_key_dict.team_metadata or {}

    project_model_config: Final = project_metadata.get("model_config")
    team_model_config: Final = team_metadata.get("model_config")

    if not project_model_config and not team_model_config:
        return

    # Extract provider hint from model name (e.g. "azure/gpt-4" -> "azure").
    # When the user-facing name has no provider prefix, fall back to the
    # deployment's litellm_params so multi-provider defaultconfig entries
    # don't silently match the first dict key (#27516).
    provider: str | None = None
    if "/" in model_name:
        provider = model_name.split("/", 1)[0]
    elif llm_router is not None:
        provider = _resolve_provider_from_deployment(
            llm_router=llm_router,
            model_name=model_name,
            pre_alias_model_name=pre_alias_model_name,
        )

    credential_name: Final = _resolve_credential_from_model_config(
        model_name=model_name,
        project_model_config=project_model_config,
        team_model_config=team_model_config,
        pre_alias_model_name=pre_alias_model_name,
        provider=provider,
    )

    if not credential_name:
        return

    credential_values: Final = CredentialAccessor.get_credential_values(credential_name)
    if not credential_values:
        _safe_cred = str(credential_name).replace("\n", "").replace("\r", "")
        verbose_proxy_logger.warning(
            "model_config references credential '%s' but it was not found or has no values",
            _safe_cred,
        )
        return

    # Apply credential overrides only for keys not already in the request
    for key in ("api_base", "api_key", "api_version"):
        if key in credential_values and key not in data:
            data[key] = credential_values[key]

    _safe_model: Final = str(model_name).replace("\n", "").replace("\r", "")
    _safe_cred = str(credential_name).replace("\n", "").replace("\r", "")
    verbose_proxy_logger.debug(
        "Applied credential override '%s' for model '%s'",
        _safe_cred,
        _safe_model,
    )


def _resolve_provider_from_deployment(
    llm_router: Router,
    model_name: str,
    pre_alias_model_name: str | None = None,
) -> str | None:
    """
    Resolve a provider hint from the deployment's litellm_params when the
    user-facing model name has no provider prefix.

    Tries the post-alias name first (the resolved model group), then the
    pre-alias name. Returns None if no deployment is found or the deployment
    has no usable provider info.
    """
    candidates: Final = [model_name]
    if pre_alias_model_name and pre_alias_model_name != model_name:
        candidates.append(pre_alias_model_name)

    for name in candidates:
        try:
            deployment = llm_router.get_deployment_by_model_group_name(model_group_name=name)
        except Exception:
            deployment = None
        if deployment is None:
            continue

        litellm_params = getattr(deployment, "litellm_params", None)
        if litellm_params is None:
            continue

        custom_provider = getattr(litellm_params, "custom_llm_provider", None)
        if custom_provider:
            return custom_provider

        deployment_model = getattr(litellm_params, "model", "") or ""
        if "/" in deployment_model:
            return deployment_model.split("/", 1)[0]

    return None


def _resolve_credential_from_model_config(
    model_name: str,
    project_model_config: dict | None,
    team_model_config: dict | None,
    pre_alias_model_name: str | None = None,
    provider: str | None = None,
) -> str | None:
    """
    Walk the precedence chain and return the first matching credential name.

    Checks (in order):
    1. project_model_config[model_name][provider] — project model-specific
    2. project_model_config[pre_alias_model_name][provider] — project pre-alias
    3. project_model_config["defaultconfig"][provider] — project default
    4. team_model_config[model_name][provider] — team model-specific
    5. team_model_config[pre_alias_model_name][provider] — team pre-alias
    6. team_model_config["defaultconfig"][provider] — team default

    When a model-specific entry exists but contains no litellm_credentials,
    the function falls through to defaultconfig. This is intentional —
    an entry without litellm_credentials is treated as incomplete config,
    not as an explicit "no override" signal.
    """
    # Build the list of model names to try (post-alias first, then pre-alias)
    model_names_to_try: Final = [model_name]
    if pre_alias_model_name and pre_alias_model_name != model_name:
        model_names_to_try.append(pre_alias_model_name)

    for model_config in (project_model_config, team_model_config):
        if not model_config or not isinstance(model_config, dict):
            continue

        # Model-specific check (try resolved name, then pre-alias name)
        for name in model_names_to_try:
            model_entry = model_config.get(name)
            if model_entry:
                credential_name = _extract_credential_from_entry(model_entry, provider=provider)
                if credential_name:
                    return credential_name
                _safe_name = str(name).replace("\n", "").replace("\r", "")
                verbose_proxy_logger.debug(
                    "model_config entry '%s' found but has no litellm_credentials, trying next candidate",
                    _safe_name,
                )

        # Default check
        default_entry = model_config.get("defaultconfig")
        if default_entry:
            credential_name = _extract_credential_from_entry(default_entry, provider=provider)
            if credential_name:
                return credential_name

    return None


def _extract_credential_from_entry(entry: dict, provider: str | None = None) -> str | None:
    """
    Extract litellm_credentials from a model_config entry.

    Entry structure: {"azure": {"litellm_credentials": "name"}, ...}

    When provider is given (e.g. "azure"), tries an exact provider match first.
    Falls back to the first credential found across all provider keys.
    """
    if not isinstance(entry, dict):
        return None

    # Prefer exact provider match when provider hint is available
    if provider and provider in entry:
        provider_config = entry[provider]
        if isinstance(provider_config, dict):
            credential_name = provider_config.get("litellm_credentials")
            if credential_name:
                return credential_name

    # Fall back to first available provider
    for provider_config in entry.values():
        if isinstance(provider_config, dict):
            credential_name = provider_config.get("litellm_credentials")
            if credential_name:
                return credential_name
    return None


def _get_enforced_params(general_settings: dict | None, user_api_key_dict: UserAPIKeyAuth) -> list | None:
    enforced_params: list | None = None
    if general_settings is not None:
        enforced_params = general_settings.get("enforced_params")
        if (
            "service_account_settings" in general_settings
            and check_if_token_is_service_account(user_api_key_dict) is True
        ):
            service_account_settings: Final = general_settings["service_account_settings"]
            if "enforced_params" in service_account_settings:
                if enforced_params is None:
                    enforced_params = []
                enforced_params.extend(service_account_settings["enforced_params"])
    if user_api_key_dict.metadata.get("enforced_params", None) is not None:
        if enforced_params is None:
            enforced_params = []
        enforced_params.extend(user_api_key_dict.metadata["enforced_params"])
    return enforced_params


def check_if_token_is_service_account(valid_token: UserAPIKeyAuth) -> bool:
    """
    Checks if the token is a service account

    Returns:
        bool: True if token is a service account

    """
    if valid_token.metadata:
        if "service_account_id" in valid_token.metadata:
            return True
    return False


def _enforced_params_check(
    request_body: dict,
    general_settings: dict | None,
    user_api_key_dict: UserAPIKeyAuth,
    premium_user: bool,
) -> bool:
    """
    If enforced params are set, check if the request body contains the enforced params.
    """
    enforced_params: Final[list | None] = _get_enforced_params(
        general_settings=general_settings, user_api_key_dict=user_api_key_dict
    )
    if enforced_params is None:
        return True
    if enforced_params and premium_user is not True:
        raise ValueError(
            f"Enforced Params is an Enterprise feature. Enforced Params: {enforced_params}. {CommonProxyErrors.not_premium_user.value}"
        )

    for enforced_param in enforced_params:
        _enforced_params = enforced_param.split(".")
        if len(_enforced_params) == 1:
            if _enforced_params[0] not in request_body:
                raise ValueError(
                    f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param"
                )
        elif len(_enforced_params) == 2:
            # this is a scenario where user requires request['metadata']['generation_name'] to exist
            if _enforced_params[0] not in request_body:
                raise ValueError(
                    f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param"
                )
            if _enforced_params[1] not in request_body[_enforced_params[0]]:
                raise ValueError(
                    f"BadRequest please pass param=[{_enforced_params[0]}][{_enforced_params[1]}] in request body. This is a required param"
                )
    return True


def _add_guardrails_from_key_or_team_metadata(
    key_metadata: dict | None,
    team_metadata: dict | None,
    data: dict,
    metadata_variable_name: str,
    project_metadata: dict | None = None,
) -> None:
    """
    Helper add guardrails from key, team, or project metadata to request data

    Key guardrails are set first, then team and project guardrails are appended (without duplicates).

    Args:
        key_metadata: The key metadata dictionary to check for guardrails
        team_metadata: The team metadata dictionary to check for guardrails
        data: The request data to update
        metadata_variable_name: The name of the metadata field in data
        project_metadata: The project metadata dictionary to check for guardrails

    """
    from litellm.proxy.utils import _premium_user_check

    # Initialize guardrails set (avoiding duplicates)
    combined_guardrails: Final = set()

    # Add key-level guardrails first
    if key_metadata and "guardrails" in key_metadata:
        if isinstance(key_metadata["guardrails"], list) and len(key_metadata["guardrails"]) > 0:
            _premium_user_check()
            combined_guardrails.update(key_metadata["guardrails"])

    # Add team-level guardrails (set automatically handles duplicates)
    if team_metadata and "guardrails" in team_metadata:
        if isinstance(team_metadata["guardrails"], list) and len(team_metadata["guardrails"]) > 0:
            _premium_user_check()
            combined_guardrails.update(team_metadata["guardrails"])

    # Add project-level guardrails (set automatically handles duplicates)
    if project_metadata and "guardrails" in project_metadata:
        if isinstance(project_metadata["guardrails"], list) and len(project_metadata["guardrails"]) > 0:
            _premium_user_check()
            combined_guardrails.update(project_metadata["guardrails"])

    # Set combined guardrails in metadata as list
    if combined_guardrails:
        data[metadata_variable_name]["guardrails"] = list(combined_guardrails)


def _add_guardrails_from_policies_in_metadata(
    key_metadata: dict | None,
    team_metadata: dict | None,
    data: dict,
    metadata_variable_name: str,
    project_metadata: dict | None = None,
) -> None:
    """
    Helper to resolve guardrails from policies attached to key/team/project metadata.

    This function:
    1. Gets policy names from key, team, and project metadata
    2. Resolves guardrails from those policies (including inheritance)
    3. Adds resolved guardrails to request metadata

    Args:
        key_metadata: The key metadata dictionary to check for policies
        team_metadata: The team metadata dictionary to check for policies
        data: The request data to update
        metadata_variable_name: The name of the metadata field in data
        project_metadata: The project metadata dictionary to check for policies
    """
    from litellm._logging import verbose_proxy_logger
    from litellm.proxy.policy_engine.policy_registry import get_policy_registry
    from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
    from litellm.proxy.utils import _premium_user_check
    from litellm.types.proxy.policy_engine import PolicyMatchContext

    # Collect policy names from key and team metadata
    policy_names: Final[set] = set()

    # Add key-level policies first
    if key_metadata and "policies" in key_metadata:
        if isinstance(key_metadata["policies"], list) and len(key_metadata["policies"]) > 0:
            _premium_user_check()
            policy_names.update(key_metadata["policies"])

    # Add team-level policies
    if team_metadata and "policies" in team_metadata:
        if isinstance(team_metadata["policies"], list) and len(team_metadata["policies"]) > 0:
            _premium_user_check()
            policy_names.update(team_metadata["policies"])

    # Add project-level policies
    if project_metadata and "policies" in project_metadata:
        if isinstance(project_metadata["policies"], list) and len(project_metadata["policies"]) > 0:
            _premium_user_check()
            policy_names.update(project_metadata["policies"])

    if not policy_names:
        return

    verbose_proxy_logger.debug("Policy engine: resolving guardrails from key/team policies: %s", policy_names)

    # Check if policy registry is initialized
    registry: Final = get_policy_registry()
    if not registry.is_initialized():
        verbose_proxy_logger.debug("Policy engine not initialized, skipping policy resolution from metadata")
        return

    # Build context for policy resolution (model from request data)
    context: Final = PolicyMatchContext(model=data.get("model"))

    # Get all policies from registry
    all_policies: Final = registry.get_all_policies()

    # Resolve guardrails from the specified policies
    resolved_guardrails: Final[set] = set()
    for policy_name in policy_names:
        if registry.has_policy(policy_name):
            resolved_policy = PolicyResolver.resolve_policy_guardrails(
                policy_name=policy_name,
                policies=all_policies,
                context=context,
            )
            resolved_guardrails.update(resolved_policy.guardrails)
            verbose_proxy_logger.debug(
                "Policy engine: resolved guardrails from policy '%s': %s", policy_name, resolved_policy.guardrails
            )
        else:
            verbose_proxy_logger.warning("Policy engine: policy '%s' not found in registry", policy_name)

    if not resolved_guardrails:
        return

    # Add resolved guardrails to request metadata
    if metadata_variable_name not in data:
        data[metadata_variable_name] = {}

    existing_guardrails = data[metadata_variable_name].get("guardrails", [])
    if not isinstance(existing_guardrails, list):
        existing_guardrails = []

    # Combine existing guardrails with policy-resolved guardrails (no duplicates)
    combined: Final = set(existing_guardrails)
    combined.update(resolved_guardrails)
    data[metadata_variable_name]["guardrails"] = list(combined)

    # Store applied policies in metadata for tracking
    if "applied_policies" not in data[metadata_variable_name]:
        data[metadata_variable_name]["applied_policies"] = []
    data[metadata_variable_name]["applied_policies"].extend(list(policy_names))

    verbose_proxy_logger.debug(
        "Policy engine: added guardrails from key/team policies to request metadata: %s", list(resolved_guardrails)
    )


async def move_guardrails_to_metadata(
    data: dict,
    _metadata_variable_name: str,
    user_api_key_dict: UserAPIKeyAuth,
):
    """
    Helper to add guardrails from request to metadata

    - If guardrails set on API Key metadata then sets guardrails on request metadata
    - If guardrails not set on API key, then checks request metadata
    - Adds guardrails from policies attached to key/team metadata
    - Adds guardrails from policy engine based on team/key/model context
    """
    # Early-out: skip all guardrails processing when nothing is configured
    key_metadata: Final = user_api_key_dict.metadata
    team_metadata: Final = user_api_key_dict.team_metadata
    project_metadata: Final = user_api_key_dict.project_metadata or {}

    has_key_config: Final = key_metadata and ("guardrails" in key_metadata or "policies" in key_metadata)
    has_team_config: Final = team_metadata and ("guardrails" in team_metadata or "policies" in team_metadata)
    has_project_config = project_metadata and ("guardrails" in project_metadata or "policies" in project_metadata)
    has_request_config: Final = "guardrails" in data or "guardrail_config" in data or "policies" in data

    # Only check policy engine if no local config (avoid import + registry lookup)
    if not (has_key_config or has_team_config or has_project_config or has_request_config):
        from litellm.proxy.policy_engine.policy_registry import get_policy_registry

        if not get_policy_registry().is_initialized():
            # Nothing configured anywhere - clean up request body fields and return
            data.pop("policies", None)
            return

    # Check key/team/project-level guardrails
    _add_guardrails_from_key_or_team_metadata(
        key_metadata=user_api_key_dict.metadata,
        team_metadata=user_api_key_dict.team_metadata,
        project_metadata=project_metadata,
        data=data,
        metadata_variable_name=_metadata_variable_name,
    )

    #########################################################################################
    # Add guardrails from policies attached to key/team/project metadata
    #########################################################################################
    _add_guardrails_from_policies_in_metadata(
        key_metadata=user_api_key_dict.metadata,
        team_metadata=user_api_key_dict.team_metadata,
        project_metadata=project_metadata,
        data=data,
        metadata_variable_name=_metadata_variable_name,
    )

    #########################################################################################
    # Add guardrails from policy engine based on team/key/model context
    #########################################################################################
    await add_guardrails_from_policy_engine(
        data=data,
        metadata_variable_name=_metadata_variable_name,
        user_api_key_dict=user_api_key_dict,
    )

    #########################################################################################
    # User's might send "guardrails" in the request body, we need to add them to the request metadata.
    # Since downstream logic requires "guardrails" to be in the request metadata
    #########################################################################################
    if "guardrails" in data:
        request_body_guardrails: Final = data.pop("guardrails")
        if "guardrails" in data[_metadata_variable_name] and isinstance(
            data[_metadata_variable_name]["guardrails"], list
        ):
            data[_metadata_variable_name]["guardrails"].extend(request_body_guardrails)
        else:
            data[_metadata_variable_name]["guardrails"] = request_body_guardrails

    #########################################################################################
    if "guardrail_config" in data:
        request_body_guardrail_config: Final = data.pop("guardrail_config")
        if "guardrail_config" in data[_metadata_variable_name] and isinstance(
            data[_metadata_variable_name]["guardrail_config"], dict
        ):
            data[_metadata_variable_name]["guardrail_config"].update(request_body_guardrail_config)
        else:
            data[_metadata_variable_name]["guardrail_config"] = request_body_guardrail_config


def _is_policy_version_id(s: str) -> bool:
    """Return True if string is a policy version ID (starts with policy_<uuid> prefix)."""
    from litellm.proxy.policy_engine.policy_registry import POLICY_VERSION_ID_PREFIX

    return isinstance(s, str) and s.startswith(POLICY_VERSION_ID_PREFIX)


def _extract_policy_id(s: str) -> str | None:
    """Extract raw UUID from policy_<uuid> string, or None if not a valid version ID."""
    from litellm.proxy.policy_engine.policy_registry import POLICY_VERSION_ID_PREFIX

    if not _is_policy_version_id(s):
        return None
    return s[len(POLICY_VERSION_ID_PREFIX) :].strip() or None


def _match_and_track_policies(
    data: dict,
    context: "PolicyMatchContext",
    request_body_policies: Any,
    policies_override: dict[str, Any] | None = None,
) -> tuple[list[str], dict[str, str]]:
    """
    Match policies via attachments and request body, track them in metadata.

    Returns:
        Tuple of (applied_policy_names, policy_reasons)
    """
    from litellm._logging import verbose_proxy_logger
    from litellm.proxy.common_utils.callback_utils import (
        add_policy_sources_to_metadata,
        add_policy_to_applied_policies_header,
    )
    from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry
    from litellm.proxy.policy_engine.policy_matcher import PolicyMatcher

    # Get matching policies via attachments (with match reasons for attribution)
    attachment_registry: Final = get_attachment_registry()
    matches_with_reasons: Final = attachment_registry.get_attached_policies_with_reasons(context)
    matching_policy_names: Final = [m["policy_name"] for m in matches_with_reasons]
    policy_reasons: Final = {m["policy_name"]: m["matched_via"] for m in matches_with_reasons}

    verbose_proxy_logger.debug("Policy engine: matched policies via attachments: %s", matching_policy_names)

    # Combine attachment-based policies with dynamic request body policies
    all_policy_names: Final = set(matching_policy_names)
    if request_body_policies and isinstance(request_body_policies, list):
        all_policy_names.update(request_body_policies)
        verbose_proxy_logger.debug("Policy engine: added dynamic policies from request body: %s", request_body_policies)

    if not all_policy_names:
        return [], {}

    # Filter to only policies whose conditions match the context
    applied_policy_names: Final = PolicyMatcher.get_policies_with_matching_conditions(
        policy_names=list(all_policy_names),
        context=context,
        policies=policies_override,
    )

    verbose_proxy_logger.debug("Policy engine: applied policies (conditions matched): %s", applied_policy_names)

    # Track applied policies in metadata for response headers
    for policy_name in applied_policy_names:
        add_policy_to_applied_policies_header(request_data=data, policy_name=policy_name)

    # Track policy attribution sources for x-litellm-policy-sources header
    applied_reasons: Final = {name: policy_reasons[name] for name in applied_policy_names if name in policy_reasons}
    add_policy_sources_to_metadata(request_data=data, policy_sources=applied_reasons)

    return applied_policy_names, policy_reasons


def _apply_resolved_guardrails_to_metadata(
    data: dict,
    metadata_variable_name: str,
    context: "PolicyMatchContext",
    policy_names: list[str] | None = None,
    policies: dict[str, Any] | None = None,
) -> None:
    """Apply resolved guardrails and pipelines to request metadata."""
    from litellm._logging import verbose_proxy_logger
    from litellm.proxy.policy_engine.policy_resolver import PolicyResolver

    # Resolve guardrails from matching policies
    resolved_guardrails: Final = PolicyResolver.resolve_guardrails_for_context(
        context=context,
        policies=policies,
        policy_names=policy_names,
    )

    verbose_proxy_logger.debug("Policy engine: resolved guardrails: %s", resolved_guardrails)

    # Resolve pipelines from matching policies
    pipelines: Final = PolicyResolver.resolve_pipelines_for_context(
        context=context,
        policies=policies,
        policy_names=policy_names,
    )

    # Add resolved guardrails to request metadata
    if metadata_variable_name not in data:
        data[metadata_variable_name] = {}

    # Track pipeline-managed guardrails to exclude from independent execution
    pipeline_managed_guardrails: set = set()
    if pipelines:
        pipeline_managed_guardrails = PolicyResolver.get_pipeline_managed_guardrails(pipelines)
        data[metadata_variable_name]["_guardrail_pipelines"] = pipelines
        data[metadata_variable_name]["_pipeline_managed_guardrails"] = pipeline_managed_guardrails
        verbose_proxy_logger.debug(
            "Policy engine: resolved %s pipeline(s), managed guardrails: %s",
            len(pipelines),
            pipeline_managed_guardrails,
        )

    if not resolved_guardrails and not pipelines:
        return

    existing_guardrails = data[metadata_variable_name].get("guardrails", [])
    if not isinstance(existing_guardrails, list):
        existing_guardrails = []

    # Combine existing guardrails with policy-resolved guardrails (no duplicates)
    # Exclude pipeline-managed guardrails from the flat list
    combined = set(existing_guardrails)
    combined.update(resolved_guardrails)
    combined -= pipeline_managed_guardrails
    data[metadata_variable_name]["guardrails"] = list(combined)

    verbose_proxy_logger.debug("Policy engine: added guardrails to request metadata: %s", list(combined))


async def add_guardrails_from_policy_engine(
    data: dict,
    metadata_variable_name: str,
    user_api_key_dict: UserAPIKeyAuth,
) -> None:
    """
    Add guardrails from the policy engine based on request context.

    This function:
    1. Extracts "policies" from request body (if present) for dynamic policy application
    2. Supports policy_<uuid> in policies to execute a specific version (e.g. published)
    3. Gets matching policies based on team_alias, key_alias, and model (via attachments)
    4. Combines dynamic policies with attachment-based policies
    5. Resolves guardrails from all policies (including inheritance)
    6. Adds guardrails to request metadata
    7. Tracks applied policies in metadata for response headers
    8. Removes "policies" from request body so it's not forwarded to LLM provider

    Args:
        data: The request data to update
        metadata_variable_name: The name of the metadata field in data
        user_api_key_dict: The user's API key authentication info
    """
    from litellm._logging import verbose_proxy_logger
    from litellm.proxy.common_utils.http_parsing_utils import get_tags_from_request_body
    from litellm.proxy.policy_engine.policy_registry import get_policy_registry
    from litellm.types.proxy.policy_engine import PolicyMatchContext

    # Extract dynamic policies from request body (if present)
    request_body_policies_raw: Final = data.pop("policies", None)

    registry: Final = get_policy_registry()
    verbose_proxy_logger.debug(
        "Policy engine: registry initialized=%s, policy_count=%s",
        registry.is_initialized(),
        len(registry.get_all_policies()),
    )
    if not registry.is_initialized():
        verbose_proxy_logger.debug("Policy engine not initialized, skipping policy matching")
        return

    # Extract tags and build context
    all_tags: Final = get_tags_from_request_body(data) or None
    _team_alias: Final = user_api_key_dict.team_alias
    _key_alias: Final = user_api_key_dict.key_alias
    context: Final = PolicyMatchContext(
        team_alias=_team_alias if isinstance(_team_alias, str) else None,
        key_alias=_key_alias if isinstance(_key_alias, str) else None,
        model=data.get("model"),
        tags=all_tags,
    )

    verbose_proxy_logger.debug(
        "Policy engine: matching policies for context team_alias=%s, key_alias=%s, model=%s, tags=%s",
        context.team_alias,
        context.key_alias,
        context.model,
        context.tags,
    )

    # Separate policy names from policy version IDs (policy_<uuid>)
    request_body_names: Final[list[str]] = []
    request_body_version_ids: Final[list[str]] = []
    if request_body_policies_raw and isinstance(request_body_policies_raw, list):
        for item in request_body_policies_raw:
            if not isinstance(item, str):
                continue
            if _is_policy_version_id(item):
                policy_id = _extract_policy_id(item)
                if policy_id:
                    request_body_version_ids.append(policy_id)
            else:
                request_body_names.append(item)

    # Resolve policy versions by ID from in-memory cache (populated by sync job; no DB in hot path)
    merged_policies: Final[dict[str, Any]] = dict(registry.get_all_policies())
    fetched_policy_names: Final[list[str]] = []
    for policy_id in request_body_version_ids:
        result = registry.get_policy_by_id_for_request(policy_id=policy_id)
        if result is not None:
            pname, policy = result
            merged_policies[pname] = policy
            fetched_policy_names.append(pname)
            verbose_proxy_logger.debug("Policy engine: loaded version by ID policy_%s -> %s", policy_id, pname)
        else:
            verbose_proxy_logger.debug("Policy engine: policy version %s not found in cache, skipping", policy_id)

    # Build request body list: names + policy names from fetched versions
    request_body_policies: Final = request_body_names + fetched_policy_names

    # Match and track policies (with merged_policies when we have version overrides)
    applied_policy_names, _ = _match_and_track_policies(
        data,
        context,
        request_body_policies,
        policies_override=merged_policies if request_body_version_ids else None,
    )

    # Resolve and apply guardrails. Use applied_policy_names so request-body policies
    # (names + version IDs) are included. Use merged_policies when we have version overrides.
    _apply_resolved_guardrails_to_metadata(
        data,
        metadata_variable_name,
        context,
        policy_names=applied_policy_names if applied_policy_names else None,
        policies=merged_policies if request_body_version_ids else None,
    )


_ANTHROPIC_API_HEADER_PROVIDERS: Final = ",".join(
    (LlmProviders.ANTHROPIC.value, LlmProviders.BEDROCK.value, LlmProviders.VERTEX_AI.value)
)
_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS: Final = LlmProviders.ANTHROPIC.value


def add_provider_specific_headers_to_request(
    data: dict,
    headers: dict,
):
    from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key

    anthropic_api_headers: Final = {header: headers[header] for header in ANTHROPIC_API_HEADERS if header in headers}
    anthropic_oauth_credential_headers: Final = {
        header: value
        for header, value in headers.items()
        if header.lower() == "authorization" and is_anthropic_oauth_key(value)
    }

    scoped_headers: Final = [
        ProviderSpecificHeader(custom_llm_provider=providers, extra_headers=extra_headers)
        for providers, extra_headers in (
            (_ANTHROPIC_API_HEADER_PROVIDERS, anthropic_api_headers),
            (_ANTHROPIC_OAUTH_CREDENTIAL_PROVIDERS, anthropic_oauth_credential_headers),
        )
        if extra_headers
    ]

    if scoped_headers:
        data["provider_specific_header"] = scoped_headers[0] if len(scoped_headers) == 1 else scoped_headers


def _add_otel_traceparent_to_data(data: dict, request: Request):
    from litellm.proxy.proxy_server import open_telemetry_logger

    if data is None:
        return
    if open_telemetry_logger is None:
        # if user is not use OTEL don't send extra_headers
        # relevant issue: https://github.com/BerriAI/litellm/issues/4448
        return

    if litellm.forward_traceparent_to_llm_provider is True:
        if request.headers:
            if "traceparent" in request.headers:
                # we want to forward this to the LLM Provider
                # Relevant issue: https://github.com/BerriAI/litellm/issues/4419
                # pass this in extra_headers
                if "extra_headers" not in data:
                    data["extra_headers"] = {}
                _exra_headers: Final = data["extra_headers"]
                if "traceparent" not in _exra_headers:
                    _exra_headers["traceparent"] = request.headers["traceparent"]
