BerriAI/litellm · error · ProxyException

Client-side 'metadata.tags' not allowed in request…

Error message

Client-side 'metadata.tags' not allowed in request. 'reject_clientside_metadata_tags'={general_settings['reject_clientside_metadata_tags']}. Tags can only be set via API key metadata.

What it means

Raised by _reject_clientside_metadata_tags_check when general_settings.reject_clientside_metadata_tags is true and a POST to an LLM route carries metadata.tags in the body. With this flag, tags become server-managed: they may only be attached to virtual keys via key metadata, never injected by the caller, so clients cannot spoof cost grouping.

Solutions

  1. Remove metadata.tags from the client request payload
  2. Set the tags on the virtual key instead: POST /key/update with metadata.tags for that key
  3. Keep any other metadata you need, just drop the tags key from the dict

Example fix

# before
body = {"model": "gpt-4o", "messages": [...], "metadata": {"tags": ["team-a", "prod"]]}

# after
body = {"model": "gpt-4o", "messages": [...], "metadata": {"request_id": "abc"}}
# tags live on the key: curl -X POST /key/update -d '{"key": "sk-...", "metadata": {"tags": ["team-a", "prod"]}}'
Defensive patterns

Strategy: validation

Validate before calling

def strip_client_tags(body: dict) -> dict:
    metadata = body.get("metadata")
    if isinstance(metadata, dict):
        metadata.pop("tags", None)
        if not metadata:
            body.pop("metadata")
    return body

payload = strip_client_tags(payload)

Try / catch

try:
    resp = client.chat.completions.create(**payload)
except Exception as e:
    if "reject_clientside_metadata_tags" in str(e):
        payload = strip_client_tags(payload)
        resp = client.chat.completions.create(**payload)
    else:
        raise

Prevention

When it happens

Trigger: Flag enabled in config, then any request containing {"metadata": {"tags": ["team-a"]}} in the body hits /v1/chat/completions or another LLM route. Only dict-valued metadata containing a tags key is rejected.

Common situations: After the operator enables the flag, LangChain callbacks or internal dashboards that tag requests client-side for cost reporting start getting 400s; shared SDK code sends metadata.tags by default.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/13701e155b146bc9. Report an issue: GitHub.

Appendix: source

Thrown at litellm/proxy/auth/auth_checks.py:537

    is_mcp_route: Final = route in LiteLLMRoutes.mcp_routes.value or RouteChecks.check_route_access(
        route=route, allowed_routes=LiteLLMRoutes.mcp_routes.value
    )

    if is_post_method and is_openai_route and not is_mcp_route and "user" not in request_body:
        raise Exception(f"'user' param not passed in. 'enforce_user_param'={general_settings['enforce_user_param']}")


def _reject_clientside_metadata_tags_check(general_settings: dict, request_body: dict, route: str) -> None:
    if not general_settings.get("reject_clientside_metadata_tags", False):
        return

    if (
        RouteChecks.is_llm_api_route(route=route)
        and "metadata" in request_body
        and isinstance(request_body["metadata"], dict)
        and "tags" in request_body["metadata"]
    ):
        raise ProxyException(
            message=f"Client-side 'metadata.tags' not allowed in request. 'reject_clientside_metadata_tags'={general_settings['reject_clientside_metadata_tags']}. Tags can only be set via API key metadata.",
            type=ProxyErrorTypes.bad_request_error,
            param="metadata.tags",
            code=status.HTTP_400_BAD_REQUEST,
        )


def _global_proxy_budget_check(global_proxy_spend: float | None, skip_budget_checks: bool, route: str) -> None:
    if (
        litellm.max_budget > 0
        and not skip_budget_checks
        and global_proxy_spend is not None
        and RouteChecks.is_llm_api_route(route=route)
        and route != "/v1/models"
        and route != "/models"
    ):
        if math.isfinite(litellm.max_budget) and global_proxy_spend > litellm.max_budget:
            raise litellm.BudgetExceededError(

View on GitHub (pinned to 77b7c6c40c)