From d688293c8143595bf4311ea0a8ed87a7fef813f5 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:22:44 -0700 Subject: [PATCH] fix(proxy): carry used_client_oauth_token onto failure spend rows for litellm_metadata routes --- .../proxy/hooks/proxy_track_cost_callback.py | 18 +++++---- .../hooks/test_proxy_track_cost_callback.py | 37 +++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index 4dfea5cd472..09deac1a5d1 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -83,6 +83,10 @@ _CAPTURED_IDENTITY_CALL_TYPES: Final[frozenset[str]] = frozenset( str(CallTypes.aretrieve_batch), ) ) +_FAILURE_ROW_KEYS_LIFTED_FROM_LITELLM_METADATA: Final[tuple[str, ...]] = ( + "standard_logging_guardrail_information", + "used_client_oauth_token", +) def _proxy_spend_writer() -> DBSpendUpdateWriter: @@ -195,13 +199,13 @@ class _ProxyDBLogger(CustomLogger): existing_metadata.update(_metadata) litellm_metadata_bucket: Final = request_data.get("litellm_metadata") - if ( - isinstance(litellm_metadata_bucket, dict) - and "standard_logging_guardrail_information" not in existing_metadata - ): - guardrail_info: Final = litellm_metadata_bucket.get("standard_logging_guardrail_information") - if guardrail_info is not None: - existing_metadata["standard_logging_guardrail_information"] = guardrail_info + existing_metadata.update( + (key, litellm_metadata_bucket[key]) + for key in _FAILURE_ROW_KEYS_LIFTED_FROM_LITELLM_METADATA + if isinstance(litellm_metadata_bucket, dict) + and key not in existing_metadata + and litellm_metadata_bucket.get(key) is not None + ) if "litellm_params" not in request_data: request_data["litellm_params"] = {} diff --git a/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py b/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py index b5e594db701..5f274e609ee 100644 --- a/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py +++ b/tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py @@ -160,6 +160,43 @@ async def test_async_post_call_failure_hook_does_not_clobber_guardrail_info_in_m assert metadata["standard_logging_guardrail_information"] == metadata_bucket_info +@pytest.mark.asyncio +@pytest.mark.parametrize("used_client_oauth_token", [True, False]) +async def test_async_post_call_failure_hook_carries_used_client_oauth_token_from_litellm_metadata( + used_client_oauth_token: bool, +): + """ + /v1/messages and /v1/responses stamp the proxy's own fields into request_data["litellm_metadata"] + and leave request_data["metadata"] to the caller's native metadata, so a failed request on those + routes wrote a spend row whose used_client_oauth_token was null instead of the stamped value + """ + logger = _ProxyDBLogger() + request_data = { + "model": "claude-sonnet-5", + "messages": [{"role": "user", "content": "Hello"}], + "metadata": {"user_id": "anthropic-native-metadata"}, + "litellm_metadata": {"used_client_oauth_token": used_client_oauth_token}, + "proxy_server_request": {"request_id": "test_request_id"}, + } + + with patch( + "litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter.update_database", + new_callable=AsyncMock, + ) as mock_update_database: + await logger.async_post_call_failure_hook( + request_data=request_data, + original_exception=Exception("rate limited"), + user_api_key_dict=UserAPIKeyAuth(api_key="test_api_key"), + ) + + call_kwargs = mock_update_database.call_args[1]["kwargs"] + assert call_kwargs["litellm_params"]["metadata"]["user_id"] == "anthropic-native-metadata" + payload = get_logging_payload( + kwargs=call_kwargs, response_obj={}, start_time=datetime.now(), end_time=datetime.now() + ) + assert json.loads(payload["metadata"])["used_client_oauth_token"] is used_client_oauth_token + + @pytest.mark.asyncio async def test_async_post_call_failure_hook_bills_guardrail_cost_on_blocked_request(): """LIT-5651: a request blocked by a guardrail never reaches the LLM, but the