Skip to content

Commit 7bf2e25

Browse files
Harshit28jclaude
andcommitted
fix: preserve API key metadata in streaming pass-through endpoint logs
When Anthropic and Vertex pass-through endpoints handled streaming responses, they passed empty kwargs={} to the logging payload creator, discarding user API key metadata (hash, alias, team_id, etc.). This prevented Langfuse traces from including key identification data for streaming requests. Fixed by retrieving litellm_params and passthrough_logging_payload from model_call_details (populated during request setup), following the existing OpenAI handler pattern. This ensures metadata reaches Langfuse callbacks for both streaming and non-streaming requests. Added tests to verify litellm_params with user_api_key metadata and passthrough_logging_payload are preserved in streaming kwargs. Affects: Anthropic and Vertex pass-through streaming endpoints, Langfuse logging integration Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 552066e commit 7bf2e25

File tree

3 files changed

+181
-3
lines changed

3 files changed

+181
-3
lines changed

litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,24 @@ def _handle_logging_anthropic_collected_chunks(
209209
"result": None,
210210
"kwargs": {},
211211
}
212+
# Preserve existing litellm_params to maintain metadata
213+
# (user_api_key_hash, user_api_key_alias, team_id, etc.)
214+
existing_litellm_params = litellm_logging_obj.model_call_details.get(
215+
"litellm_params", {}
216+
) or {}
217+
initial_kwargs: dict = {
218+
"litellm_params": existing_litellm_params.copy(),
219+
}
220+
passthrough_logging_payload = litellm_logging_obj.model_call_details.get(
221+
"passthrough_logging_payload"
222+
)
223+
if passthrough_logging_payload is not None:
224+
initial_kwargs["passthrough_logging_payload"] = passthrough_logging_payload
225+
212226
kwargs = AnthropicPassthroughLoggingHandler._create_anthropic_response_logging_payload(
213227
litellm_model_response=complete_streaming_response,
214228
model=model,
215-
kwargs={},
229+
kwargs=initial_kwargs,
216230
start_time=start_time,
217231
end_time=end_time,
218232
logging_obj=litellm_logging_obj,

litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,14 @@ def _handle_logging_vertex_collected_chunks(
341341
- Creates standard logging object
342342
- Logs in litellm callbacks
343343
"""
344-
kwargs: Dict[str, Any] = {}
344+
# Preserve existing litellm_params to maintain metadata
345+
# (user_api_key_hash, user_api_key_alias, team_id, etc.)
346+
existing_litellm_params = litellm_logging_obj.model_call_details.get(
347+
"litellm_params", {}
348+
) or {}
349+
kwargs: Dict[str, Any] = {
350+
"litellm_params": existing_litellm_params.copy(),
351+
}
345352
model = model or VertexPassthroughLoggingHandler.extract_model_from_url(
346353
url_route
347354
)

tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_anthropic_passthrough_logging_handler.py

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,4 +614,161 @@ def test_store_batch_managed_object_success(
614614
)
615615

616616
# Verify managed files hook was called
617-
mock_proxy_logging_obj.get_proxy_hook.assert_called_once_with("managed_files")
617+
mock_proxy_logging_obj.get_proxy_hook.assert_called_once_with("managed_files")
618+
619+
620+
class TestAnthropicStreamingMetadataPreservation:
621+
"""Test that metadata (user_api_key_hash, team_id, etc.) is preserved in streaming kwargs."""
622+
623+
def setup_method(self):
624+
self.start_time = datetime.now()
625+
self.end_time = datetime.now()
626+
self.mock_chunks = [
627+
'{"type": "message_start", "message": {"id": "msg_123", "model": "claude-3-sonnet-20240229"}}',
628+
'{"type": "content_block_delta", "delta": {"text": "Hello"}}',
629+
'{"type": "message_stop"}',
630+
]
631+
self.mock_metadata = {
632+
"user_api_key_hash": "sk-test-hash-1234",
633+
"user_api_key_alias": "my-test-key",
634+
"user_api_key_team_id": "team-abc",
635+
"user_api_key_org_id": "org-xyz",
636+
"user_api_key_user_id": "user-123",
637+
}
638+
639+
def _create_mock_logging_obj_with_metadata(self):
640+
mock_logging_obj = MagicMock()
641+
mock_logging_obj.model_call_details = {
642+
"model": "claude-3-sonnet-20240229",
643+
"litellm_params": {
644+
"metadata": self.mock_metadata.copy(),
645+
},
646+
"passthrough_logging_payload": {
647+
"url": "https://api.anthropic.com/v1/messages",
648+
"request_body": {
649+
"model": "claude-3-sonnet-20240229",
650+
"messages": [{"role": "user", "content": "Hello"}],
651+
},
652+
},
653+
}
654+
mock_logging_obj.litellm_call_id = "test-call-id"
655+
return mock_logging_obj
656+
657+
@patch.object(
658+
AnthropicPassthroughLoggingHandler, "_build_complete_streaming_response"
659+
)
660+
@patch.object(
661+
AnthropicPassthroughLoggingHandler,
662+
"_create_anthropic_response_logging_payload",
663+
)
664+
def test_streaming_handler_passes_litellm_params_in_kwargs(
665+
self, mock_create_payload, mock_build_response
666+
):
667+
"""Verify that litellm_params with metadata is passed to _create_anthropic_response_logging_payload."""
668+
mock_build_response.return_value = MagicMock()
669+
mock_create_payload.return_value = {
670+
"response_cost": 0.001,
671+
"model": "claude-3-sonnet-20240229",
672+
"litellm_params": {"metadata": self.mock_metadata.copy()},
673+
}
674+
logging_obj = self._create_mock_logging_obj_with_metadata()
675+
676+
result = AnthropicPassthroughLoggingHandler._handle_logging_anthropic_collected_chunks(
677+
litellm_logging_obj=logging_obj,
678+
passthrough_success_handler_obj=MagicMock(),
679+
url_route="/anthropic/v1/messages",
680+
request_body={"model": "claude-3-sonnet-20240229"},
681+
endpoint_type="messages",
682+
start_time=self.start_time,
683+
all_chunks=self.mock_chunks,
684+
end_time=self.end_time,
685+
)
686+
687+
# Verify _create_anthropic_response_logging_payload was called with kwargs containing litellm_params
688+
mock_create_payload.assert_called_once()
689+
call_kwargs = mock_create_payload.call_args[1]["kwargs"]
690+
assert "litellm_params" in call_kwargs
691+
assert "metadata" in call_kwargs["litellm_params"]
692+
assert (
693+
call_kwargs["litellm_params"]["metadata"]["user_api_key_hash"]
694+
== "sk-test-hash-1234"
695+
)
696+
assert (
697+
call_kwargs["litellm_params"]["metadata"]["user_api_key_alias"]
698+
== "my-test-key"
699+
)
700+
assert (
701+
call_kwargs["litellm_params"]["metadata"]["user_api_key_team_id"]
702+
== "team-abc"
703+
)
704+
705+
@patch.object(
706+
AnthropicPassthroughLoggingHandler, "_build_complete_streaming_response"
707+
)
708+
@patch.object(
709+
AnthropicPassthroughLoggingHandler,
710+
"_create_anthropic_response_logging_payload",
711+
)
712+
def test_streaming_handler_passes_passthrough_logging_payload(
713+
self, mock_create_payload, mock_build_response
714+
):
715+
"""Verify that passthrough_logging_payload is included in kwargs when present."""
716+
mock_build_response.return_value = MagicMock()
717+
mock_create_payload.return_value = {"response_cost": 0.001}
718+
logging_obj = self._create_mock_logging_obj_with_metadata()
719+
720+
AnthropicPassthroughLoggingHandler._handle_logging_anthropic_collected_chunks(
721+
litellm_logging_obj=logging_obj,
722+
passthrough_success_handler_obj=MagicMock(),
723+
url_route="/anthropic/v1/messages",
724+
request_body={"model": "claude-3-sonnet-20240229"},
725+
endpoint_type="messages",
726+
start_time=self.start_time,
727+
all_chunks=self.mock_chunks,
728+
end_time=self.end_time,
729+
)
730+
731+
call_kwargs = mock_create_payload.call_args[1]["kwargs"]
732+
assert "passthrough_logging_payload" in call_kwargs
733+
assert (
734+
call_kwargs["passthrough_logging_payload"]["url"]
735+
== "https://api.anthropic.com/v1/messages"
736+
)
737+
738+
@patch.object(
739+
AnthropicPassthroughLoggingHandler, "_build_complete_streaming_response"
740+
)
741+
@patch.object(
742+
AnthropicPassthroughLoggingHandler,
743+
"_create_anthropic_response_logging_payload",
744+
)
745+
def test_streaming_handler_without_passthrough_logging_payload(
746+
self, mock_create_payload, mock_build_response
747+
):
748+
"""Verify kwargs still contain litellm_params even when passthrough_logging_payload is absent."""
749+
mock_build_response.return_value = MagicMock()
750+
mock_create_payload.return_value = {"response_cost": 0.001}
751+
752+
logging_obj = MagicMock()
753+
logging_obj.model_call_details = {
754+
"model": "claude-3-sonnet-20240229",
755+
"litellm_params": {
756+
"metadata": self.mock_metadata.copy(),
757+
},
758+
}
759+
logging_obj.litellm_call_id = "test-call-id"
760+
761+
AnthropicPassthroughLoggingHandler._handle_logging_anthropic_collected_chunks(
762+
litellm_logging_obj=logging_obj,
763+
passthrough_success_handler_obj=MagicMock(),
764+
url_route="/anthropic/v1/messages",
765+
request_body={"model": "claude-3-sonnet-20240229"},
766+
endpoint_type="messages",
767+
start_time=self.start_time,
768+
all_chunks=self.mock_chunks,
769+
end_time=self.end_time,
770+
)
771+
772+
call_kwargs = mock_create_payload.call_args[1]["kwargs"]
773+
assert "litellm_params" in call_kwargs
774+
assert "passthrough_logging_payload" not in call_kwargs

0 commit comments

Comments
 (0)