fix(spend): preserve deployment identity in spend logs when litellm_metadata is present

Spend logging reads litellm_metadata when a request carries one, but the router
writes the selected deployment's identity (model_info.id, model_group, deployment
name) into metadata. add_missing_spend_metadata_to_litellm_metadata only copied
user_api_key* keys, so openai/ passthrough deployments logged a blank model_id and
a provider-prefix-stripped model for requests that included litellm_metadata.

Fixes #35472

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-01 19:09:51 +00:00
parent 23de7a15d9
commit 61e5ac0930
2 changed files with 87 additions and 3 deletions

View file

@ -164,17 +164,33 @@ def remove_items_at_indices(items: Optional[List[Any]], indices: Iterable[int])
items.pop(index)
SPEND_TRACKING_DEPLOYMENT_IDENTITY_KEYS = (
"model_info",
"model_group",
"deployment",
"deployment_model_name",
)
def add_missing_spend_metadata_to_litellm_metadata(litellm_metadata: dict, metadata: dict) -> dict:
"""
Helper to get litellm metadata for spend tracking
PATCH for issue where both `litellm_metadata` and `metadata` are present in the kwargs
and user_api_key values are in 'metadata'.
and spend-tracking values are in 'metadata'.
The router always writes the selected deployment's identity (model_info.id,
model_group, deployment name) into `metadata`, but spend logging reads from
`litellm_metadata` whenever a request carries one (e.g. caller-supplied tags).
Without copying these over, the same deployment logs a blank `model_id` and a
provider-prefix-stripped `model` for requests that include `litellm_metadata`.
"""
potential_spend_tracking_metadata_substring = "user_api_key"
for key, value in metadata.items():
if potential_spend_tracking_metadata_substring in key:
if "user_api_key" in key:
litellm_metadata[key] = value
for key in SPEND_TRACKING_DEPLOYMENT_IDENTITY_KEYS:
if key not in litellm_metadata and key in metadata:
litellm_metadata[key] = metadata[key]
return litellm_metadata

View file

@ -4,6 +4,7 @@ import pytest
from litellm.litellm_core_utils.core_helpers import (
_FINISH_REASON_MAP,
get_litellm_metadata_from_kwargs,
get_or_create_metadata_bucket,
map_finish_reason,
reconstruct_model_name,
@ -11,6 +12,73 @@ from litellm.litellm_core_utils.core_helpers import (
)
def _spend_log_identity(kwargs: dict) -> dict:
"""Mirror how spend logging derives model / model_id from request kwargs."""
metadata = get_litellm_metadata_from_kwargs(kwargs)
return {
"model": reconstruct_model_name(
kwargs.get("model") or "", kwargs.get("custom_llm_provider"), metadata or {}
),
"model_id": metadata.get("model_info", {}).get("id", ""),
"model_group": metadata.get("model_group", ""),
}
def test_spend_log_deployment_identity_consistent_with_litellm_metadata():
"""Regression for #35472: an openai/ passthrough deployment must log the same
model / model_id whether or not the request also carries litellm_metadata.
The router writes the deployment identity into `metadata`, but spend logging
reads `litellm_metadata` when present, so those keys must be carried over."""
router_metadata = {
"deployment": "openai/anthropic/claude-sonnet-5",
"model_info": {"id": "9da5dfc9-2223-4f77-b3c9-f9100d9cb2a0"},
"model_group": "my-model",
"user_api_key_hash": "abc",
}
without_litellm_metadata = _spend_log_identity(
{
"model": "anthropic/claude-sonnet-5",
"custom_llm_provider": "openai",
"litellm_params": {"metadata": dict(router_metadata)},
}
)
with_litellm_metadata = _spend_log_identity(
{
"model": "anthropic/claude-sonnet-5",
"custom_llm_provider": "openai",
"litellm_params": {
"metadata": dict(router_metadata),
"litellm_metadata": {"tags": ["t1"]},
},
}
)
expected = {
"model": "openai/anthropic/claude-sonnet-5",
"model_id": "9da5dfc9-2223-4f77-b3c9-f9100d9cb2a0",
"model_group": "my-model",
}
assert without_litellm_metadata == expected
assert with_litellm_metadata == expected
def test_get_litellm_metadata_from_kwargs_does_not_overwrite_existing_identity():
"""Caller-supplied identity keys in litellm_metadata must win over metadata."""
kwargs = {
"litellm_params": {
"metadata": {"model_group": "router-group", "deployment": "router-dep"},
"litellm_metadata": {"model_group": "caller-group"},
}
}
metadata = get_litellm_metadata_from_kwargs(kwargs)
assert metadata["model_group"] == "caller-group"
assert metadata["deployment"] == "router-dep"
class TestGetOrCreateMetadataBucket:
"""The single owner every guardrail writer and reader shares, so the response
header and the spend log can never disagree about which dict a record lives in."""