Skip to content

fix(logging): preserve proxy key-auth metadata on /v1/messages Langfuse traces#25448

Merged
krrish-berri-2 merged 3 commits intoBerriAI:litellm_internal_staging_04_11_2026from
michelligabriele:fix/anthropic-messages-langfuse-metadata
Apr 11, 2026
Merged

fix(logging): preserve proxy key-auth metadata on /v1/messages Langfuse traces#25448
krrish-berri-2 merged 3 commits intoBerriAI:litellm_internal_staging_04_11_2026from
michelligabriele:fix/anthropic-messages-langfuse-metadata

Conversation

@michelligabriele
Copy link
Copy Markdown
Collaborator

update_from_kwargs() overwrites proxy metadata (user_api_key_hash, etc.) with Anthropic's native metadata when both exist. Merge instead of replace.

Relevant issues

Fixes missing user_api_key_hash, user_api_key_alias, user_api_key_team_id in Langfuse traces for /v1/messages (Anthropic Messages API) requests sent by Claude Code through the LiteLLM proxy.

Related PRs: #23259, #24003

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🐛 Bug Fix

Changes

Root cause: async_anthropic_messages_handler in llm_http_handler.py calls logging_obj.update_from_kwargs() with litellm_params containing **anthropic_messages_optional_request_params. Since AnthropicMessagesRequestOptionalParams includes a metadata field (Anthropic's native metadata, e.g. {user_id: "..."}), the base_litellm_params.update(litellm_params) call in update_from_kwargs() overwrites the proxy key-auth fields (user_api_key_hash, user_api_key_alias, user_api_key_team_id, etc.) that were already merged from litellm_metadata.

Fix: In update_from_kwargs(), pop metadata from litellm_params before the .update(), then merge it back without overwriting existing keys. This preserves both proxy key-auth fields and Anthropic's native metadata.

Files changed:

  • litellm/litellm_core_utils/litellm_logging.py — merge metadata in update_from_kwargs() instead of overwriting
  • tests/test_litellm/litellm_core_utils/test_litellm_logging.py — test reproducing the exact bug scenario

…se traces

update_from_kwargs() overwrites proxy metadata (user_api_key_hash, etc.)
with Anthropic's native metadata when both exist. Merge instead of replace.
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 9, 2026 10:47pm

Request Review

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Apr 9, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing michelligabriele:fix/anthropic-messages-langfuse-metadata (a201f23) with main (3a6db70)

Open in CodSpeed

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This PR fixes a metadata overwrite bug in update_from_kwargs() where Anthropic's native metadata field (e.g. {user_id: "..."}) passed via litellm_params would silently clobber proxy key-auth fields (user_api_key_hash, user_api_key_alias, user_api_key_team_id, etc.) that were already loaded from kwargs/litellm_metadata. The fix pops metadata from litellm_params before the .update() call and re-merges it using a setdefault-style loop so that kwargs-sourced metadata always wins on key conflicts.

Confidence Score: 5/5

Safe to merge — fix correctly addresses the metadata overwrite bug with no new P1/P0 issues introduced.

The merge logic is correct: kwargs-sourced proxy metadata is built first, then litellm_params metadata is merged in with setdefault semantics so existing keys are never overwritten. The new conflict-resolution test (test_kwargs_metadata_wins_over_caller_metadata_in_conflict) directly validates the fix. All current call sites pass freshly constructed dicts, so the previously flagged pop() mutation concern is not a live defect. All remaining findings are P2 or lower.

No files require special attention.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/litellm_logging.py update_from_kwargs() now pops metadata from litellm_params before bulk update and re-merges carefully; fix is logically correct but pop() still mutates the caller's dict (flagged previously, unaddressed)
tests/test_litellm/litellm_core_utils/test_litellm_logging.py Adds explicit conflict-resolution test (test_kwargs_metadata_wins_over_caller_metadata_in_conflict) alongside prior merge test; no real network calls, stays in unit-test scope

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["update_from_kwargs(kwargs, litellm_params)"] --> B["Build base_litellm_params from kwargs['metadata'] and kwargs['litellm_metadata']"]
    B --> C{litellm_params provided?}
    C -- No --> G
    C -- Yes --> D["lp_metadata = litellm_params.pop('metadata', None)"]
    D --> E["base_litellm_params.update(litellm_params)"]
    E --> F{lp_metadata is non-empty dict?}
    F -- No --> G["update_environment_variables(base_litellm_params)"]
    F -- Yes --> H["setdefault('metadata', {})"] 
    H --> I["Merge lp_metadata keys NOT already in base metadata\n(kwargs metadata wins on conflicts)"]
    I --> G

    style D fill:#ffd700,color:#000
    style I fill:#90ee90,color:#000
Loading

Reviews (2): Last reviewed commit: "test: add explicit conflict-resolution t..." | Re-trigger Greptile

# from kwargs/litellm_metadata with the caller's litellm_params metadata.
# e.g. anthropic_messages passes Anthropic's native metadata ({user_id: ...})
# in litellm_params, which would overwrite proxy key-auth fields.
lp_metadata = litellm_params.pop("metadata", 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.

P2 pop() mutates the caller's dict

litellm_params.pop("metadata", None) modifies the dict passed in. All current call sites pass freshly constructed dicts, so this is safe today. However, any future caller that reuses the dict after this call would silently lose its metadata key. Using get plus a filtered update is safer:

Suggested change
lp_metadata = litellm_params.pop("metadata", None)
lp_metadata = litellm_params.get("metadata", None)
litellm_params_without_meta = {k: v for k, v in litellm_params.items() if k != "metadata"}
base_litellm_params.update(litellm_params_without_meta)

Comment on lines 431 to +441
def test_caller_litellm_params_win_over_kwargs(self, logging_obj):
"""Explicit litellm_params from the caller should override auto-extracted values."""
"""Explicit litellm_params metadata merges into kwargs metadata without overwriting."""
kwargs = {"metadata": {"from_kwargs": True}}

logging_obj.update_from_kwargs(
kwargs=kwargs,
litellm_params={"metadata": {"from_caller": True}, "litellm_call_id": "x"},
)

assert logging_obj.litellm_params["metadata"] == {"from_caller": True}
# kwargs metadata is preserved, caller metadata is merged in
assert logging_obj.litellm_params["metadata"] == {"from_kwargs": True, "from_caller": True}
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.

P2 Misleading test name and missing conflict-resolution scenario

The name test_caller_litellm_params_win_over_kwargs says the caller wins, but the fix intentionally makes kwargs (proxy metadata) win when the same key appears in both sources. The test doesn't actually exercise that conflict — "from_kwargs" and "from_caller" are different keys, so both always end up present regardless of which side "wins."

Consider renaming and adding an explicit conflict assertion:

Suggested change
def test_caller_litellm_params_win_over_kwargs(self, logging_obj):
"""Explicit litellm_params from the caller should override auto-extracted values."""
"""Explicit litellm_params metadata merges into kwargs metadata without overwriting."""
kwargs = {"metadata": {"from_kwargs": True}}
logging_obj.update_from_kwargs(
kwargs=kwargs,
litellm_params={"metadata": {"from_caller": True}, "litellm_call_id": "x"},
)
assert logging_obj.litellm_params["metadata"] == {"from_caller": True}
# kwargs metadata is preserved, caller metadata is merged in
assert logging_obj.litellm_params["metadata"] == {"from_kwargs": True, "from_caller": True}
def test_kwargs_metadata_wins_over_caller_metadata_in_conflict(self, logging_obj):
"""kwargs metadata keys take precedence; caller litellm_params metadata is merged in without overwriting."""
kwargs = {"metadata": {"from_kwargs": True, "shared_key": "kwargs_value"}}
logging_obj.update_from_kwargs(
kwargs=kwargs,
litellm_params={"metadata": {"from_caller": True, "shared_key": "caller_value"}, "litellm_call_id": "x"},
)
# kwargs metadata is preserved (shared_key keeps the kwargs value), caller-only keys are added
assert logging_obj.litellm_params["metadata"] == {
"from_kwargs": True,
"from_caller": True,
"shared_key": "kwargs_value", # kwargs wins on conflict
}

@krrish-berri-2 krrish-berri-2 changed the base branch from main to litellm_internal_staging_04_11_2026 April 11, 2026 16:29
@krrish-berri-2 krrish-berri-2 merged commit c9e4949 into BerriAI:litellm_internal_staging_04_11_2026 Apr 11, 2026
50 of 51 checks passed
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