Skip to content

Commit 4eac0f6

Browse files
authored
[Feat] Pass through endpoints - ensure PassthroughStandardLoggingPayload is logged and contains method, url, request/response body (#10194)
* ensure passthrough_logging_payload is filled in kwargs * test_assistants_passthrough_logging * test_assistants_passthrough_logging * test_assistants_passthrough_logging * test_threads_passthrough_logging * test _init_kwargs_for_pass_through_endpoint * _init_kwargs_for_pass_through_endpoint
1 parent 4a50cf1 commit 4eac0f6

File tree

11 files changed

+244
-18
lines changed

11 files changed

+244
-18
lines changed

litellm/llms/anthropic/experimental_pass_through/messages/handler.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
- call /messages on Anthropic API
3-
- Make streaming + non-streaming request - just pass it through direct to Anthropic. No need to do anything special here
3+
- Make streaming + non-streaming request - just pass it through direct to Anthropic. No need to do anything special here
44
- Ensure requests are logged in the DB - stream + non-stream
55
66
"""
@@ -43,7 +43,9 @@ async def _handle_anthropic_streaming(
4343
from litellm.proxy.pass_through_endpoints.success_handler import (
4444
PassThroughEndpointLogging,
4545
)
46-
from litellm.proxy.pass_through_endpoints.types import EndpointType
46+
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
47+
EndpointType,
48+
)
4749

4850
# Create success handler object
4951
passthrough_success_handler_obj = PassThroughEndpointLogging()
@@ -98,11 +100,11 @@ async def anthropic_messages(
98100
api_base=optional_params.api_base,
99101
api_key=optional_params.api_key,
100102
)
101-
anthropic_messages_provider_config: Optional[
102-
BaseAnthropicMessagesConfig
103-
] = ProviderConfigManager.get_provider_anthropic_messages_config(
104-
model=model,
105-
provider=litellm.LlmProviders(_custom_llm_provider),
103+
anthropic_messages_provider_config: Optional[BaseAnthropicMessagesConfig] = (
104+
ProviderConfigManager.get_provider_anthropic_messages_config(
105+
model=model,
106+
provider=litellm.LlmProviders(_custom_llm_provider),
107+
)
106108
)
107109
if anthropic_messages_provider_config is None:
108110
raise ValueError(

litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
1414
from litellm.proxy._types import PassThroughEndpointLoggingTypedDict
1515
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
16-
from litellm.proxy.pass_through_endpoints.types import PassthroughStandardLoggingPayload
16+
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
17+
PassthroughStandardLoggingPayload,
18+
)
1719
from litellm.types.utils import ModelResponse, TextCompletionResponse
1820

1921
if TYPE_CHECKING:
@@ -122,9 +124,9 @@ def _create_anthropic_response_logging_payload(
122124
litellm_model_response.id = logging_obj.litellm_call_id
123125
litellm_model_response.model = model
124126
logging_obj.model_call_details["model"] = model
125-
logging_obj.model_call_details[
126-
"custom_llm_provider"
127-
] = litellm.LlmProviders.ANTHROPIC.value
127+
logging_obj.model_call_details["custom_llm_provider"] = (
128+
litellm.LlmProviders.ANTHROPIC.value
129+
)
128130
return kwargs
129131
except Exception as e:
130132
verbose_proxy_logger.exception(

litellm/proxy/pass_through_endpoints/llm_provider_handlers/assembly_passthrough_logging_handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
get_standard_logging_object_payload,
1515
)
1616
from litellm.litellm_core_utils.thread_pool_executor import executor
17-
from litellm.proxy.pass_through_endpoints.types import PassthroughStandardLoggingPayload
1817
from litellm.types.passthrough_endpoints.assembly_ai import (
1918
ASSEMBLY_AI_MAX_POLLING_ATTEMPTS,
2019
ASSEMBLY_AI_POLLING_INTERVAL,
2120
)
21+
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
22+
PassthroughStandardLoggingPayload,
23+
)
2224

2325

2426
class AssemblyAITranscriptResponse(TypedDict, total=False):

litellm/proxy/pass_through_endpoints/llm_provider_handlers/base_passthrough_logging_handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from litellm.llms.base_llm.chat.transformation import BaseConfig
1414
from litellm.proxy._types import PassThroughEndpointLoggingTypedDict
1515
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
16-
from litellm.proxy.pass_through_endpoints.types import PassthroughStandardLoggingPayload
16+
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
17+
PassthroughStandardLoggingPayload,
18+
)
1719
from litellm.types.utils import LlmProviders, ModelResponse, TextCompletionResponse
1820

1921
if TYPE_CHECKING:

litellm/proxy/pass_through_endpoints/pass_through_endpoints.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import litellm
2424
from litellm._logging import verbose_proxy_logger
2525
from litellm.integrations.custom_logger import CustomLogger
26+
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
2627
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
2728
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
2829
from litellm.proxy._types import (
@@ -38,11 +39,14 @@
3839
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
3940
from litellm.secret_managers.main import get_secret_str
4041
from litellm.types.llms.custom_http import httpxSpecialProvider
42+
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
43+
EndpointType,
44+
PassthroughStandardLoggingPayload,
45+
)
4146
from litellm.types.utils import StandardLoggingUserAPIKeyMetadata
4247

4348
from .streaming_handler import PassThroughStreamingHandler
4449
from .success_handler import PassThroughEndpointLogging
45-
from .types import EndpointType, PassthroughStandardLoggingPayload
4650

4751
router = APIRouter()
4852

@@ -530,13 +534,15 @@ async def pass_through_request( # noqa: PLR0915
530534
passthrough_logging_payload = PassthroughStandardLoggingPayload(
531535
url=str(url),
532536
request_body=_parsed_body,
537+
request_method=getattr(request, "method", None),
533538
)
534539
kwargs = _init_kwargs_for_pass_through_endpoint(
535540
user_api_key_dict=user_api_key_dict,
536541
_parsed_body=_parsed_body,
537542
passthrough_logging_payload=passthrough_logging_payload,
538543
litellm_call_id=litellm_call_id,
539544
request=request,
545+
logging_obj=logging_obj,
540546
)
541547
# done for supporting 'parallel_request_limiter.py' with pass-through endpoints
542548
logging_obj.update_environment_variables(
@@ -741,6 +747,7 @@ def _init_kwargs_for_pass_through_endpoint(
741747
request: Request,
742748
user_api_key_dict: UserAPIKeyAuth,
743749
passthrough_logging_payload: PassthroughStandardLoggingPayload,
750+
logging_obj: LiteLLMLoggingObj,
744751
_parsed_body: Optional[dict] = None,
745752
litellm_call_id: Optional[str] = None,
746753
) -> dict:
@@ -775,6 +782,11 @@ def _init_kwargs_for_pass_through_endpoint(
775782
"litellm_call_id": litellm_call_id,
776783
"passthrough_logging_payload": passthrough_logging_payload,
777784
}
785+
786+
logging_obj.model_call_details["passthrough_logging_payload"] = (
787+
passthrough_logging_payload
788+
)
789+
778790
return kwargs
779791

780792

litellm/proxy/pass_through_endpoints/streaming_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from litellm._logging import verbose_proxy_logger
99
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
1010
from litellm.proxy._types import PassThroughEndpointLoggingResultValues
11+
from litellm.types.passthrough_endpoints.pass_through_endpoints import EndpointType
1112
from litellm.types.utils import StandardPassThroughResponseObject
1213

1314
from .llm_provider_handlers.anthropic_passthrough_logging_handler import (
@@ -17,7 +18,6 @@
1718
VertexPassthroughLoggingHandler,
1819
)
1920
from .success_handler import PassThroughEndpointLogging
20-
from .types import EndpointType
2121

2222

2323
class PassThroughStreamingHandler:

litellm/proxy/pass_through_endpoints/success_handler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
99
from litellm.proxy._types import PassThroughEndpointLoggingResultValues
10+
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
11+
PassthroughStandardLoggingPayload,
12+
)
1013
from litellm.types.utils import StandardPassThroughResponseObject
1114
from litellm.utils import executor as thread_pool_executor
1215

@@ -92,11 +95,15 @@ async def pass_through_async_success_handler(
9295
end_time: datetime,
9396
cache_hit: bool,
9497
request_body: dict,
98+
passthrough_logging_payload: PassthroughStandardLoggingPayload,
9599
**kwargs,
96100
):
97101
standard_logging_response_object: Optional[
98102
PassThroughEndpointLoggingResultValues
99103
] = None
104+
logging_obj.model_call_details["passthrough_logging_payload"] = (
105+
passthrough_logging_payload
106+
)
100107
if self.is_vertex_route(url_route):
101108
vertex_passthrough_logging_handler_result = (
102109
VertexPassthroughLoggingHandler.vertex_passthrough_handler(

litellm/proxy/pass_through_endpoints/types.py renamed to litellm/types/passthrough_endpoints/pass_through_endpoints.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,21 @@ class PassthroughStandardLoggingPayload(TypedDict, total=False):
1414
"""
1515

1616
url: str
17+
"""
18+
The full url of the request
19+
"""
20+
21+
request_method: Optional[str]
22+
"""
23+
The method of the request
24+
"GET", "POST", "PUT", "DELETE", etc.
25+
"""
26+
1727
request_body: Optional[dict]
28+
"""
29+
The body of the request
30+
"""
1831
response_body: Optional[dict] # only tracked for non-streaming responses
32+
"""
33+
The body of the response
34+
"""
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import json
2+
import os
3+
import sys
4+
from datetime import datetime
5+
from unittest.mock import AsyncMock, Mock, patch, MagicMock
6+
from typing import Optional
7+
from fastapi import Request
8+
import pytest
9+
import asyncio
10+
11+
sys.path.insert(
12+
0, os.path.abspath("../..")
13+
) # Adds the parent directory to the system path
14+
15+
import litellm
16+
from litellm.proxy._types import UserAPIKeyAuth
17+
from litellm.types.passthrough_endpoints.pass_through_endpoints import PassthroughStandardLoggingPayload
18+
from litellm.integrations.custom_logger import CustomLogger
19+
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import pass_through_request
20+
21+
class TestCustomLogger(CustomLogger):
22+
def __init__(self):
23+
self.logged_kwargs: Optional[dict] = None
24+
25+
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
26+
print("in async log success event kwargs", json.dumps(kwargs, indent=4, default=str))
27+
self.logged_kwargs = kwargs
28+
29+
@pytest.mark.asyncio
30+
async def test_assistants_passthrough_logging():
31+
test_custom_logger = TestCustomLogger()
32+
litellm._async_success_callback = [test_custom_logger]
33+
34+
TARGET_URL = "https://api.openai.com/v1/assistants"
35+
REQUEST_BODY = {
36+
"instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.",
37+
"name": "Math Tutor",
38+
"tools": [{"type": "code_interpreter"}],
39+
"model": "gpt-4o"
40+
}
41+
TARGET_METHOD = "POST"
42+
43+
result = await pass_through_request(
44+
request=Request(
45+
scope={
46+
"type": "http",
47+
"method": TARGET_METHOD,
48+
"path": "/v1/assistants",
49+
"query_string": b"",
50+
"headers": [
51+
(b"content-type", b"application/json"),
52+
(b"authorization", f"Bearer {os.getenv('OPENAI_API_KEY')}".encode()),
53+
(b"openai-beta", b"assistants=v2")
54+
]
55+
},
56+
),
57+
target=TARGET_URL,
58+
custom_headers={
59+
"Content-Type": "application/json",
60+
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
61+
"OpenAI-Beta": "assistants=v2"
62+
},
63+
user_api_key_dict=UserAPIKeyAuth(
64+
api_key="test",
65+
user_id="test",
66+
team_id="test",
67+
end_user_id="test",
68+
),
69+
custom_body=REQUEST_BODY,
70+
forward_headers=False,
71+
merge_query_params=False,
72+
)
73+
74+
print("got result", result)
75+
print("result status code", result.status_code)
76+
print("result content", result.body)
77+
78+
await asyncio.sleep(1)
79+
80+
assert test_custom_logger.logged_kwargs is not None
81+
passthrough_logging_payload: Optional[PassthroughStandardLoggingPayload] = test_custom_logger.logged_kwargs["passthrough_logging_payload"]
82+
assert passthrough_logging_payload is not None
83+
assert passthrough_logging_payload["url"] == TARGET_URL
84+
assert passthrough_logging_payload["request_body"] == REQUEST_BODY
85+
86+
# assert that the response body content matches the response body content
87+
client_facing_response_body = json.loads(result.body)
88+
assert passthrough_logging_payload["response_body"] == client_facing_response_body
89+
90+
# assert that the request method is correct
91+
assert passthrough_logging_payload["request_method"] == TARGET_METHOD
92+
93+
@pytest.mark.asyncio
94+
async def test_threads_passthrough_logging():
95+
test_custom_logger = TestCustomLogger()
96+
litellm._async_success_callback = [test_custom_logger]
97+
98+
TARGET_URL = "https://api.openai.com/v1/threads"
99+
REQUEST_BODY = {}
100+
TARGET_METHOD = "POST"
101+
102+
result = await pass_through_request(
103+
request=Request(
104+
scope={
105+
"type": "http",
106+
"method": TARGET_METHOD,
107+
"path": "/v1/threads",
108+
"query_string": b"",
109+
"headers": [
110+
(b"content-type", b"application/json"),
111+
(b"authorization", f"Bearer {os.getenv('OPENAI_API_KEY')}".encode()),
112+
(b"openai-beta", b"assistants=v2")
113+
]
114+
},
115+
),
116+
target=TARGET_URL,
117+
custom_headers={
118+
"Content-Type": "application/json",
119+
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
120+
"OpenAI-Beta": "assistants=v2"
121+
},
122+
user_api_key_dict=UserAPIKeyAuth(
123+
api_key="test",
124+
user_id="test",
125+
team_id="test",
126+
end_user_id="test",
127+
),
128+
custom_body=REQUEST_BODY,
129+
forward_headers=False,
130+
merge_query_params=False,
131+
)
132+
133+
print("got result", result)
134+
print("result status code", result.status_code)
135+
print("result content", result.body)
136+
137+
await asyncio.sleep(1)
138+
139+
assert test_custom_logger.logged_kwargs is not None
140+
passthrough_logging_payload = test_custom_logger.logged_kwargs["passthrough_logging_payload"]
141+
assert passthrough_logging_payload is not None
142+
143+
# Fix for TypedDict access errors
144+
assert passthrough_logging_payload.get("url") == TARGET_URL
145+
assert passthrough_logging_payload.get("request_body") == REQUEST_BODY
146+
147+
# Fix for json.loads error with potential memoryview
148+
response_body = result.body
149+
client_facing_response_body = json.loads(response_body)
150+
151+
assert passthrough_logging_payload.get("response_body") == client_facing_response_body
152+
assert passthrough_logging_payload.get("request_method") == TARGET_METHOD
153+
154+
155+

0 commit comments

Comments
 (0)