{"record":{"id":"13701e155b146bc9","repo":"BerriAI/litellm","slug":"client-side-metadata-tags-not-allowed-in-request","errorCode":null,"errorMessage":"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.","messagePattern":"Client-side 'metadata\\.tags' not allowed in request\\. 'reject_clientside_metadata_tags'=(.+?)\\. Tags can only be set via API key metadata\\.","errorType":"http","errorClass":"ProxyException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/auth/auth_checks.py","lineNumber":537,"sourceCode":"    is_mcp_route: Final = route in LiteLLMRoutes.mcp_routes.value or RouteChecks.check_route_access(\n        route=route, allowed_routes=LiteLLMRoutes.mcp_routes.value\n    )\n\n    if is_post_method and is_openai_route and not is_mcp_route and \"user\" not in request_body:\n        raise Exception(f\"'user' param not passed in. 'enforce_user_param'={general_settings['enforce_user_param']}\")\n\n\ndef _reject_clientside_metadata_tags_check(general_settings: dict, request_body: dict, route: str) -> None:\n    if not general_settings.get(\"reject_clientside_metadata_tags\", False):\n        return\n\n    if (\n        RouteChecks.is_llm_api_route(route=route)\n        and \"metadata\" in request_body\n        and isinstance(request_body[\"metadata\"], dict)\n        and \"tags\" in request_body[\"metadata\"]\n    ):\n        raise ProxyException(\n            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.\",\n            type=ProxyErrorTypes.bad_request_error,\n            param=\"metadata.tags\",\n            code=status.HTTP_400_BAD_REQUEST,\n        )\n\n\ndef _global_proxy_budget_check(global_proxy_spend: float | None, skip_budget_checks: bool, route: str) -> None:\n    if (\n        litellm.max_budget > 0\n        and not skip_budget_checks\n        and global_proxy_spend is not None\n        and RouteChecks.is_llm_api_route(route=route)\n        and route != \"/v1/models\"\n        and route != \"/models\"\n    ):\n        if math.isfinite(litellm.max_budget) and global_proxy_spend > litellm.max_budget:\n            raise litellm.BudgetExceededError(","sourceCodeStart":519,"sourceCodeEnd":555,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/auth/auth_checks.py#L519-L555","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove metadata.tags from the client request payload","Set the tags on the virtual key instead: POST /key/update with metadata.tags for that key","Keep any other metadata you need, just drop the tags key from the dict"],"exampleFix":"# before\nbody = {\"model\": \"gpt-4o\", \"messages\": [...], \"metadata\": {\"tags\": [\"team-a\", \"prod\"]]}\n\n# after\nbody = {\"model\": \"gpt-4o\", \"messages\": [...], \"metadata\": {\"request_id\": \"abc\"}}\n# tags live on the key: curl -X POST /key/update -d '{\"key\": \"sk-...\", \"metadata\": {\"tags\": [\"team-a\", \"prod\"]}}'","handlingStrategy":"validation","validationCode":"def strip_client_tags(body: dict) -> dict:\n    metadata = body.get(\"metadata\")\n    if isinstance(metadata, dict):\n        metadata.pop(\"tags\", None)\n        if not metadata:\n            body.pop(\"metadata\")\n    return body\n\npayload = strip_client_tags(payload)","typeGuard":null,"tryCatchPattern":"try:\n    resp = client.chat.completions.create(**payload)\nexcept Exception as e:\n    if \"reject_clientside_metadata_tags\" in str(e):\n        payload = strip_client_tags(payload)\n        resp = client.chat.completions.create(**payload)\n    else:\n        raise","preventionTips":["Centralize tagging policy: tags belong on keys via key metadata, not on requests","Audit SDK callbacks (LangChain etc.) for metadata injection when enabling the flag","Announce the flag flip to all API consumers before enforcing it"],"tags":["metadata","tags","reject-clientside-metadata-tags","bad-request","litellm-proxy"],"backgroundTag":"rejected-request-metadata","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}