Skip to content

fix: support PathLike content in upload tuples#3474

Open
VectorPeak wants to merge 1 commit into
openai:mainfrom
VectorPeak:fix
Open

fix: support PathLike content in upload tuples#3474
VectorPeak wants to merge 1 commit into
openai:mainfrom
VectorPeak:fix

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 8, 2026

Copy link
Copy Markdown
  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

Fix multipart upload tuple handling so PathLike content is normalized before the tuple is passed through to httpx.

What Problem This Solves

The README documents upload parameters as accepting bytes, PathLike, or a tuple such as (filename, contents, media type). In practice, the SDK already handled a direct PathLike value correctly, for example:

files.create(file=Path("input.jsonl"), purpose="fine-tune")

However, the tuple form did not get the same normalization when the tuple's content element was path-like:

files.create(file=("custom.txt", Path("input.jsonl")), purpose="fine-tune")

That tuple shape is useful because it lets callers provide a custom upload filename while still asking the SDK to read the file contents from disk. It also supports the existing extended tuple forms for content type and headers:

("custom.txt", Path("input.jsonl"), "application/jsonl")
("custom.txt", Path("input.jsonl"), "application/jsonl", {"X-Test": "1"})

Before this change, tuple inputs could be classified as generic file content before the tuple-specific branch had a chance to inspect and normalize the second element. As a result, callers following the documented upload shape could not reliably combine a custom filename, media type, or headers with PathLike file content.

Conceptually, the bug was a normalization-order issue: the SDK handled "a path as the whole file value", but not "a path inside the file tuple". Those two forms should reach httpx in the same normalized shape: a filename plus bytes-like file content.

Change

This updates both the synchronous and asynchronous file normalization helpers so tuple values are handled before generic file-content detection.

The core code movement is intentionally small: tuple values now get normalized first, so the tuple's second element can still be read through the existing file-content helpers instead of being treated as final multipart content too early.

 def _transform_file(file: FileTypes) -> HttpxFileTypes:
+    if is_tuple_t(file):
+        return (file[0], read_file_content(file[1]), *file[2:])
+
     if is_file_content(file):
         if isinstance(file, os.PathLike):
             path = pathlib.Path(file)
             return (path.name, path.read_bytes())
 
         return file
 
-    if is_tuple_t(file):
-        return (file[0], read_file_content(file[1]), *file[2:])
-
     raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
 
 
 async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
+    if is_tuple_t(file):
+        return (file[0], await async_read_file_content(file[1]), *file[2:])
+
     if is_file_content(file):
         if isinstance(file, os.PathLike):
             path = anyio.Path(file)
             return (path.name, await path.read_bytes())
 
         return file
 
-    if is_tuple_t(file):
-        return (file[0], await async_read_file_content(file[1]), *file[2:])
-
     raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")

For tuple inputs, the SDK now preserves the tuple metadata while normalizing only the content element:

  • the first tuple element, such as "custom.txt", remains the upload filename;
  • the second tuple element is passed through read_file_content() or async_read_file_content(), so PathLike values are converted into bytes;
  • any remaining tuple elements, such as media type or headers, are preserved unchanged.

The direct PathLike path remains unchanged. Passing Path("input.jsonl") directly still uses the path's filename and reads the file contents as before. The change only makes the nested tuple case follow the same documented contract.

Evidence

Focused tests now cover the previously missing tuple shapes for both sync and async normalization.

The sync tests verify that to_httpx_files() handles:

{"file": ("custom.txt", readme_path)}
[("file", ("custom.txt", readme_path))]
{"file": ("custom.txt", readme_path, "text/markdown")}
{"file": ("custom.txt", readme_path, "text/markdown", {"X-Test": "1"})}

The async tests mirror the same cases through async_to_httpx_files() using anyio.Path, covering:

{"file": ("custom.txt", anyio.Path(readme_path))}
[("file", ("custom.txt", anyio.Path(readme_path)))]
{"file": ("custom.txt", anyio.Path(readme_path), "text/markdown")}
{"file": ("custom.txt", anyio.Path(readme_path), "text/markdown", {"X-Test": "1"})}

These tests check the important boundary behavior: after normalization, httpx receives tuple content as bytes, while the caller-provided filename, media type, and headers remain intact.

Validation run:

mamba run -n prw-openai-python-py3.9 python -m pytest tests/test_files.py -q --tb=short
git diff --check

Possible call chain / impact

A typical public call can start with an upload parameter like:

files.create(file=("custom.txt", Path("input.jsonl")), purpose="fine-tune")

That value is converted into request files, passed through to_httpx_files() or async_to_httpx_files(), and then handed off to httpx for multipart encoding.

The intended boundary is:

public upload API
-> request file extraction
-> to_httpx_files() / async_to_httpx_files()
-> tuple metadata preserved
-> PathLike content read into bytes
-> httpx multipart upload

This PR keeps the OpenAI SDK responsible for its documented PathLike extension before the value crosses into httpx's multipart layer. It does not broaden the accepted file input types; it only makes the already-documented tuple form behave consistently with direct PathLike uploads.

The affected surface is limited to multipart file normalization. Existing direct bytes, direct PathLike, file-like objects, tuple filenames, tuple media types, and tuple headers continue to flow through the same helper functions and are covered by the added regression tests.

Additional context & links

Fixes #3473

Validation:

  • mamba run -n prw-openai-python-py3.9 python -m pytest tests/test_files.py -q --tb=short
  • git diff --check

Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
@VectorPeak VectorPeak requested a review from a team as a code owner July 8, 2026 03:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PathLike file contents fail when passed inside upload tuples

1 participant