Skip to content

fix(proxy): always merge caller-supplied tags into request metadata#27784

Merged
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_/vibrant-bose-d1a024
May 12, 2026
Merged

fix(proxy): always merge caller-supplied tags into request metadata#27784
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_/vibrant-bose-d1a024

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Summary

Caller-supplied tags (x-litellm-tags header, body tags, metadata.tags) were silently dropped from request metadata unless the calling key/team had metadata.allow_client_tags: true set. This broke two documented features:

  • Tag-based routing via x-litellm-tags — the documented per-request header for routing to tagged deployments.
  • Tag-based spend attributionrequest_tags going into spend logs and /spend/tags aggregations.

This PR restores the previous behavior: tags from the request always flow into metadata.tags and union with any admin-configured static tags from key/team/project metadata.

What changed

  • litellm/proxy/litellm_pre_call_utils.py — drop the conditional strip of caller tags from body / metadata / litellm_metadata and the conditional gate on the x-litellm-tags header merge. Both now run unconditionally.
  • The allow_client_tags flag is removed from the pre-call pipeline. It was only ever read here — no schema, types, or endpoint footprint — so existing values in key/team metadata are inert.

Test changes

  • Drop three tests that exercised the strip-when-not-opted-in path (no longer reachable).
  • Drop the "allow_client_tags": True fixture lines from the merge/union/multipart tests; their assertions about union behavior continue to hold.

Test plan

End-to-end verification against a running proxy:

  • x-litellm-tags: premium routes to the premium-tagged tag-routed deployment
  • x-litellm-tags: tenant:acme routes to the tenant:acme-tagged deployment
  • Header-only tags (x-litellm-tags) appear in LiteLLM_SpendLogs.request_tags
  • Body-root tags ({"tags": [...]}) appear in LiteLLM_SpendLogs.request_tags
  • metadata.tags body tags appear in LiteLLM_SpendLogs.request_tags
  • Untagged request still routes to default + returns 200
  • Admin-configured static key tags still merge into request_tags (no regression)

Pytest:

  • uv run pytest tests/test_litellm/proxy/test_litellm_pre_call_utils.py — 125 passed
  • uv run pytest tests/proxy_unit_tests/test_proxy_utils.py -k "spend_logs_metadata or duplicate_tags" — 69 passed
  • uv run pytest tests/test_litellm/router_strategy/test_router_tag_routing.py — 21 passed
  • uv run pytest tests/test_litellm/proxy/auth/test_auth_checks.py — 116 passed

Caller-supplied tags (`x-litellm-tags` header, body `tags`, `metadata.tags`)
were silently dropped unless the key/team had
`metadata.allow_client_tags: true` set. Restore the documented behavior:
tags from the request always flow into `metadata.tags` and union with any
admin-configured static tags from key/team/project metadata.

Removes the `allow_client_tags` opt-in flag from the pre-call pipeline.
The flag was only ever read here; it has no schema or endpoint footprint,
so leftover values in existing key metadata are inert.

Test cleanup mirrors the simplification: drop the three tests that
verified the strip-when-not-opted-in path, drop the `allow_client_tags`
fixture lines from the merge/union tests.
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR removes the allow_client_tags gate introduced in a prior change, restoring the original behavior where caller-supplied tags (x-litellm-tags header, body tags, metadata.tags) always merge into request metadata unconditionally. Three security-regression tests and all allow_client_tags: True fixtures are deleted to match.

  • The 38-line tag-stripping block and the _admin_allow_client_tags conditional on the header-tag merge are removed from add_litellm_data_to_request, making tag propagation unconditional for all authenticated callers.
  • Two inline comments that referenced "the strip" and "tags without opt-in" as active protections are now stale and should be updated to reflect the current behavior.

Confidence Score: 3/5

Merging restores documented tag-routing functionality but permanently removes the per-key/team tag restriction for all deployments, including those that may have been relying on the restriction as a security boundary.

The core change removes the ability for admins to block caller-supplied routing tags on a per-key basis. Any deployment using tag-based routing to gate access to restricted model tiers (e.g., premium, internal) now has that gate silently removed for all callers. The PR author notes allow_client_tags was never exposed in schema or documented endpoints, but the security intent was explicit in the removed code and tests. The safer path would have been to flip the default to permissive while keeping the flag as an opt-out.

litellm/proxy/litellm_pre_call_utils.py — the tag propagation logic and the two now-stale security comments warrant close review before merging.

Security Review

  • Tag-based routing bypass: Removing the allow_client_tags gate means any authenticated API caller can supply arbitrary x-litellm-tags values and route to tag-restricted deployments (e.g., premium, internal) without admin consent. Previously this required allow_client_tags: true on the key or team metadata.
  • Spend misattribution: Callers can now inject tags into request body or metadata to attribute their spend against arbitrary tag budgets, including those belonging to other teams/projects, without any admin opt-in.
  • Affected path: add_litellm_data_to_request in litellm/proxy/litellm_pre_call_utils.py, specifically the removal of the 38-line guard block and the _admin_allow_client_tags check on the header merge.

Important Files Changed

Filename Overview
litellm/proxy/litellm_pre_call_utils.py Removes the allow_client_tags security gate unconditionally, meaning all authenticated callers can now supply routing/spend tags without admin opt-in; two comments referencing the removed strip are now stale and misleading.
tests/test_litellm/proxy/test_litellm_pre_call_utils.py Drops three security-regression tests (strip-without-permission paths) and removes allow_client_tags: True fixtures from merge/union tests; remaining assertions are correct for the new unconditional behavior.
tests/proxy_unit_tests/test_proxy_utils.py Removes allow_client_tags: True from two test fixtures; logic and assertions are otherwise unchanged and remain valid.

Comments Outside Diff (3)

  1. litellm/proxy/litellm_pre_call_utils.py, line 1437-1449 (link)

    P1 security Backwards-incompatible removal of allow_client_tags security gate

    Any deployment that relied on allow_client_tags: false (the default before this change) to prevent authenticated callers from supplying arbitrary routing tags now silently accepts those tags. A caller can send x-litellm-tags: premium to reach a restricted premium-tagged deployment, or add body tags to misattribute spend to another team's tag budget — both with no admin consent. Per the project rule, removing a behavioral security gate without a user-controlled escape hatch risks breaking existing deployments that depend on the restriction. Consider keeping the flag but flipping its default to true and adding a deny_client_tags opt-out, rather than unconditionally removing the gate.

    Rule Used: What: avoid backwards-incompatible changes without... (source)

  2. litellm/proxy/litellm_pre_call_utils.py, line 1451-1455 (link)

    P2 Stale comment: the allow_client_tags strip that this comment describes no longer exists. The phrase "tags without opt-in" is inaccurate now that tags are always accepted, and could mislead future maintainers into thinking the protection is still in place.

  3. litellm/proxy/litellm_pre_call_utils.py, line 1461-1463 (link)

    P2 Stale comment: "runs AFTER the strip" no longer describes the real reason for ordering. The tag strip is gone, so the rationale should reflect the actual remaining guard (stripping user_api_key_* and _UNTRUSTED_METADATA_CONTROL_FIELDS from litellm_metadata).

Reviews (1): Last reviewed commit: "fix(proxy): always merge caller-supplied..." | Re-trigger Greptile

)

if tags is not None and _admin_allow_client_tags:
if tags is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: Caller-controlled tag routing

A normal key holder can now send x-litellm-tags or root-level tags for a tag assigned to a restricted deployment; this branch copies those values into request metadata, and router tag filtering later selects deployments whose litellm_params.tags match request metadata. Keep caller-supplied routing/budget tags behind an admin opt-in, or reject/ignore all client tag sources by default, including metadata.tags, litellm_metadata.tags, root tags, and x-litellm-tags.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate change - The features this change breaks does not justify the gaps this closes.

@veria-ai

veria-ai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

High: Caller-controlled tag routing

This PR makes caller-supplied tags flow into request metadata used by router tag filtering and spend attribution. A normal authenticated key holder can choose a tag assigned to a restricted deployment and have the router select that deployment.


Status: 1 new · 2 open
Risk: 8/10

The tag-strip block was removed in the parent commit but two surrounding
comments still referenced "tags without opt-in" and "runs AFTER the
strip". Update them to describe the remaining user_api_key_* and
_pipeline_managed_guardrails strip that the snapshot/merge ordering
actually protects against.
)

if tags is not None and _admin_allow_client_tags:
if tags is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: Caller-controlled tag routing

x-litellm-tags and root-level tags are attacker-controlled request fields, but get_deployments_for_tag() later treats metadata.tags as routing input. A normal key holder can send the tag for a restricted deployment and route traffic there; keep these tags stripped or gate them on an admin-controlled key/team opt-in before merging them into metadata.

@yuneng-berri yuneng-berri merged commit ad2a74c into litellm_internal_staging May 12, 2026
111 of 114 checks passed
@yuneng-berri yuneng-berri deleted the litellm_/vibrant-bose-d1a024 branch May 12, 2026 23:30
cursor Bot pushed a commit that referenced this pull request May 12, 2026
…ation fixes (#27787, #27784)

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants