From 5b535b62a46a753f93557dea07bbb1708cb81c2a Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:30:45 +0530 Subject: [PATCH] fix(base_client): forward files in async _build_request The async AsyncAPIClient._build_request omitted the files argument when calling httpx build_request, so multipart file uploads made through the async post proxy (AsyncPortkey.post(url=..., files=...)) silently dropped the file body. The sync APIClient._build_request already passes files=options.files; mirror it on the async path so async and sync behave identically. Add offline regression tests that assert the outgoing request carries a multipart/form-data body for both the sync and async post file paths. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- portkey_ai/api_resources/base_client.py | 1 + tests/test_post_files.py | 58 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/test_post_files.py diff --git a/portkey_ai/api_resources/base_client.py b/portkey_ai/api_resources/base_client.py index ee98482a..004648a9 100644 --- a/portkey_ai/api_resources/base_client.py +++ b/portkey_ai/api_resources/base_client.py @@ -1314,6 +1314,7 @@ async def _build_request(self, options: Options) -> httpx.Request: headers=headers, params=params, json=json_body, + files=options.files, timeout=options.timeout, ) return request diff --git a/tests/test_post_files.py b/tests/test_post_files.py new file mode 100644 index 00000000..c76716c1 --- /dev/null +++ b/tests/test_post_files.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Dict + +import httpx +import pytest + +from portkey_ai import AsyncPortkey, Portkey + +BASE_URL = "https://api.portkey.ai/v1" + + +def _capture_transport(captured: Dict[str, object]) -> httpx.MockTransport: + def handler(request: httpx.Request) -> httpx.Response: + captured["content_type"] = request.headers.get("content-type", "") + captured["content"] = request.content + return httpx.Response(200, json={"success": True}) + + return httpx.MockTransport(handler) + + +def test_sync_post_sends_multipart_files() -> None: + captured: Dict[str, object] = {} + http_client = httpx.Client( + transport=_capture_transport(captured), base_url=BASE_URL + ) + client = Portkey(api_key="dummy", base_url=BASE_URL, http_client=http_client) + + client.post( + url="/files", + purpose="assistants", + files={"file": ("hello.txt", b"hello world", "text/plain")}, + ) + + assert "multipart/form-data" in captured["content_type"] + body = captured["content"] + assert b"hello.txt" in body + assert b"hello world" in body + + +@pytest.mark.asyncio +async def test_async_post_sends_multipart_files() -> None: + captured: Dict[str, object] = {} + http_client = httpx.AsyncClient( + transport=_capture_transport(captured), base_url=BASE_URL + ) + client = AsyncPortkey(api_key="dummy", base_url=BASE_URL, http_client=http_client) + + await client.post( + url="/files", + purpose="assistants", + files={"file": ("hello.txt", b"hello world", "text/plain")}, + ) + + assert "multipart/form-data" in captured["content_type"] + body = captured["content"] + assert b"hello.txt" in body + assert b"hello world" in body