{"record":{"id":"1d381021b6655d1e","repo":"BerriAI/litellm","slug":"only-proxy-admins-can-set-estimated-output-tokens","errorCode":null,"errorMessage":"Only proxy admins can set {ESTIMATED_OUTPUT_TOKENS_FIELD} or {ESTIMATED_OUTPUT_TOKENS_PER_MODEL_FIELD} on a {entity}. They decide how many output tokens the rate limiter reserves for a request that omits max_tokens.","messagePattern":"Only proxy admins can set (.+?) or (.+?) on a (.+?)\\. They decide how many output tokens the rate limiter reserves for a request that omits max_tokens\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"litellm/proxy/auth/auth_utils.py","lineNumber":1162,"sourceCode":"    \"\"\"Only a proxy admin may change what a key or team declares its models emit.\n\n    That declaration is what the TPM limiter reserves for a request omitting\n    ``max_tokens``, so lowering or clearing it under-reserves against every\n    window the request is charged against, including the team and organization\n    ones the writer may not own. A key's metadata is writable by its holder and\n    a team's by its team admin, so neither is a trustworthy source for a value\n    that weakens a limit set above them. Gated on the resulting value rather\n    than on presence, so a form resending the stored declaration stays a no-op.\n    \"\"\"\n    if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:\n        return\n    stored: Final[Mapping[str, object]] = existing_metadata or {}\n    if _requested_output_token_estimates(data, stored) == (\n        stored.get(ESTIMATED_OUTPUT_TOKENS_FIELD),\n        stored.get(ESTIMATED_OUTPUT_TOKENS_PER_MODEL_FIELD),\n    ):\n        return\n    raise HTTPException(\n        status_code=403,\n        detail={\n            \"error\": f\"Only proxy admins can set {ESTIMATED_OUTPUT_TOKENS_FIELD} or \"\n            f\"{ESTIMATED_OUTPUT_TOKENS_PER_MODEL_FIELD} on a {entity}. They decide how many output tokens \"\n            \"the rate limiter reserves for a request that omits max_tokens.\"\n        },\n    )\n\n\ndef get_model_rate_limit_from_metadata(\n    user_api_key_dict: UserAPIKeyAuth,\n    metadata_accessor_key: Literal[\"team_metadata\", \"organization_metadata\", \"project_metadata\"],\n    rate_limit_key: Literal[\"model_rpm_limit\", \"model_tpm_limit\"],\n) -> dict[str, int] | None:\n    if getattr(user_api_key_dict, metadata_accessor_key):\n        return getattr(user_api_key_dict, metadata_accessor_key).get(rate_limit_key)\n    return None\n","sourceCodeStart":1144,"sourceCodeEnd":1180,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/auth/auth_utils.py#L1144-L1180","documentation":"Raised as HTTP 403 by the estimated-output-tokens guard in litellm/proxy/auth/auth_utils.py when a caller whose role is not PROXY_ADMIN tries to change the rate-limiter reservation fields (estimated_output_tokens / estimated_output_tokens_per_model) on an entity (team, user, or key). Those fields decide how many output tokens the rate limiter pre-reserves for requests that omit max_tokens, so letting a team admin or end user weaken them would undermine limits set above them. The check compares the requested values against the stored ones, so resubmitting the unchanged stored declaration is a no-op and passes.","triggerScenarios":"POST /team/update, /user/update, or /key/update by a non-proxy-admin (team admin, internal user, app user) whose metadata payload sets estimated_output_tokens or estimated_output_tokens_per_model to values different from the ones already stored on the entity.","commonSituations":"A team admin self-serving via the UI or API tries to raise estimated_output_tokens to make the rate limiter reserve more (or fewer) tokens for their team; automation scripts that copy admin-curated metadata blocks into update calls and accidentally alter the values; after an admin sets the fields, a non-admin edit that touches other metadata but rewrites these fields with stale/different values.","solutions":["Have a proxy admin (role proxy_admin) make the change, since only that role may set these fields","Remove estimated_output_tokens / estimated_output_tokens_per_model from the update payload if the caller is not a proxy admin","If resubmitting an unchanged form, make sure the values sent exactly match the stored ones - equality with stored metadata is allowed"],"exampleFix":"# before: team admin sends changed reservation values\nawait client.post(\"/team/update\", json={\"team_id\": tid, \"metadata\": {\"estimated_output_tokens\": 999999}})\n\n# after: non-admin omits the admin-only fields\nawait client.post(\"/team/update\", json={\"team_id\": tid, \"metadata\": {\"notes\": \"updated by team admin\"}})","handlingStrategy":"validation","validationCode":"ADMIN_ONLY_FIELDS = {\"estimated_output_tokens\", \"estimated_output_tokens_per_model\"}\n\ndef sanitize_metadata_for_role(metadata: dict, user_role: str) -> dict:\n    if user_role != \"proxy_admin\":\n        return {k: v for k, v in metadata.items() if k not in ADMIN_ONLY_FIELDS}\n    return metadata","typeGuard":"def is_proxy_admin(user_api_key_dict) -> bool:\n    \"\"\"True only for the role allowed to set rate-limiter reservation fields.\"\"\"\n    return getattr(user_api_key_dict, \"user_role\", None) == \"proxy_admin\"","tryCatchPattern":"from fastapi import HTTPException\n\ntry:\n    await client.post(\"/team/update\", json=payload)\nexcept HTTPException as e:\n    if e.status_code == 403 and \"estimated_output_tokens\" in e.detail.get(\"error\", \"\"):\n        escalate_to_proxy_admin(payload)  # only admins may set these fields\n    else:\n        raise","preventionTips":["Strip estimated_output_tokens(_per_model) from update payloads in non-admin tooling and UIs","Keep admin-curated metadata blocks out of automated team/user update scripts","Resubmitting stored values unchanged is a no-op - diff against current metadata before sending"],"tags":["authorization","forbidden","rate-limit","metadata","rbac","proxy"],"backgroundTag":"insufficient-permissions","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}