From 0476a33d9ffe73e18d73ea39aa84eb15457ba5e5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 30 Sep 2025 15:01:38 -0700 Subject: [PATCH] [Bug Fix] Passthrough API Endpoints - Ensure query params are forwarded from origin url to downstream request (#15087) * test_pass_through_request_query_params_forwarding * fix: pass_through_request * test_azure_openai_assistants_e2e_operations_stream * test_azure_openai_assistants_e2e_operations_stream --- .../pass_through_config.yaml | 7 +- .../pass_through_endpoints.py | 4 +- .../test_openai_assistants_passthrough.py | 34 ++++++++ .../test_pass_through_endpoints.py | 84 +++++++++++++++++++ 4 files changed, 125 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/example_config_yaml/pass_through_config.yaml b/litellm/proxy/example_config_yaml/pass_through_config.yaml index ccc13f4d5a..f900f9cfc7 100644 --- a/litellm/proxy/example_config_yaml/pass_through_config.yaml +++ b/litellm/proxy/example_config_yaml/pass_through_config.yaml @@ -26,4 +26,9 @@ model_list: api_key: os.environ/ANTHROPIC_API_KEY general_settings: master_key: sk-1234 - custom_auth: custom_auth_basic.user_api_key_auth \ No newline at end of file + custom_auth: custom_auth_basic.user_api_key_auth + pass_through_endpoints: + - path: "/azure-config-passthrough" + target: os.environ/AZURE_API_BASE + headers: + Authorization: os.environ/AZURE_API_KEY \ No newline at end of file diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 0eacee3b4f..8f5ecec8a7 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -688,10 +688,8 @@ async def pass_through_request( # noqa: PLR0915 # combine url with query params for logging requested_query_params: Optional[dict] = ( - query_params or request.query_params.__dict__ + query_params or dict(request.query_params) ) - if requested_query_params == request.query_params.__dict__: - requested_query_params = None requested_query_params_str = None if requested_query_params: diff --git a/tests/pass_through_tests/test_openai_assistants_passthrough.py b/tests/pass_through_tests/test_openai_assistants_passthrough.py index 40361ab39f..974fd566cc 100644 --- a/tests/pass_through_tests/test_openai_assistants_passthrough.py +++ b/tests/pass_through_tests/test_openai_assistants_passthrough.py @@ -96,3 +96,37 @@ def test_openai_assistants_e2e_operations_stream(): event_handler=EventHandler(), ) as stream: stream.until_done() + + + +def test_azure_openai_assistants_e2e_operations_stream(): + client = openai.OpenAI(base_url="http://0.0.0.0:4000/azure-config-passthrough", api_key="sk-1234") + assistant = client.beta.assistants.create( + name="Math Tutor", + instructions="You are a personal math tutor. Write and run code to answer math questions.", + tools=[{"type": "code_interpreter"}], + model="gpt-4o", + ) + print("assistant created", assistant) + + thread = client.beta.threads.create() + print("thread created", thread) + + message = client.beta.threads.messages.create( + thread_id=thread.id, + role="user", + content="I need to solve the equation `3x + 11 = 14`. Can you help me?", + ) + print("message created", message) + + # Then, we use the `stream` SDK helper + # with the `EventHandler` class to create the Run + # and stream the response. + + with client.beta.threads.runs.stream( + thread_id=thread.id, + assistant_id=assistant.id, + instructions="Please address the user as Jane Doe. The user has a premium account.", + event_handler=EventHandler(), + ) as stream: + stream.until_done() \ No newline at end of file diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index 1157863aa2..1a26f8a39a 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -1252,6 +1252,90 @@ async def test_delete_pass_through_endpoint_empty_list(): +@pytest.mark.asyncio +async def test_pass_through_request_query_params_forwarding(): + """ + Test that query parameters from the original request are properly forwarded to the target URL. + + This test verifies the fix for the bug where query parameters like api-version were being lost + when forwarding requests to Azure OpenAI and other pass-through endpoints. + """ + with patch("litellm.proxy.proxy_server.proxy_logging_obj") as mock_proxy_logging: + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.HttpPassThroughEndpointHelpers.non_streaming_http_request_handler" + ) as mock_http_handler: + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.ProxyBaseLLMRequestProcessing" + ) as mock_processing: + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.pass_through_endpoint_logging.pass_through_async_success_handler" + ) as mock_success_handler: + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.get_response_body" + ) as mock_get_response_body: + # Setup mock for pre_call_hook + test_body = {"name": "Azure Assistant", "model": "gpt-4o"} + mock_proxy_logging.pre_call_hook = AsyncMock(return_value=test_body) + + # Setup mock for http response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.aread = AsyncMock(return_value=b'{"id": "asst_123", "object": "assistant"}') + mock_response.text = '{"id": "asst_123", "object": "assistant"}' + mock_response.raise_for_status = MagicMock() + + # Mock the HTTP request handler to capture the call + mock_http_handler.return_value = mock_response + + # Mock response body parser + mock_get_response_body.return_value = {"id": "asst_123", "object": "assistant"} + + # Mock headers for custom headers + mock_processing.get_custom_headers.return_value = {} + + # Mock success handler + mock_success_handler.return_value = None + + # Create mock request with query parameters (Azure API version) + mock_request = MagicMock(spec=Request) + mock_request.method = "POST" + mock_request.url = "http://localhost:4000/azure-assistant/openai/assistants" + mock_request.body = AsyncMock(return_value=json.dumps(test_body).encode()) + mock_request.headers = Headers({"Content-Type": "application/json"}) + + # Create QueryParams with api-version parameter + mock_request.query_params = QueryParams([("api-version", "2025-01-01-preview")]) + + # Create mock user API key dict + mock_user_api_key_dict = MagicMock() + mock_user_api_key_dict.api_key = "sk-1234" + + # Call pass_through_request + result = await pass_through_request( + request=mock_request, + target="https://krris-m2f9a9i7-eastus2.openai.azure.com/openai/assistants", + custom_headers={"Authorization": "Bearer azure_token"}, + user_api_key_dict=mock_user_api_key_dict, + ) + + # Verify the HTTP handler was called + mock_http_handler.assert_called_once() + + # Extract the call arguments to verify query parameters were passed + call_kwargs = mock_http_handler.call_args[1] + + # The key assertion: query parameters should be preserved and passed to the HTTP handler + assert "requested_query_params" in call_kwargs + assert call_kwargs["requested_query_params"] == {"api-version": "2025-01-01-preview"} + + # Verify the target URL is correct + assert str(call_kwargs["url"]) == "https://krris-m2f9a9i7-eastus2.openai.azure.com/openai/assistants" + + # Verify the request body is preserved + assert call_kwargs["_parsed_body"] == test_body + + @pytest.mark.asyncio async def test_pass_through_with_httpbin_redirect(): """