Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/openai/_utils/_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ async def _async_transform_recursive(

if origin == dict and is_mapping(data):
items_type = get_args(stripped_type)[1]
return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
return {key: await _async_transform_recursive(value, annotation=items_type) for key, value in data.items()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve model excludes in async dict recursion

For async transforms of a Dict[str, Model] whose values are pydantic models with client-only fields excluded via __api_exclude__ (for example ParsedResponseFunctionToolCall excludes parsed_arguments), this new path now routes those values through _async_transform_recursive. Unlike the sync branch it replaced, the async BaseModel dump does not pass exclude=getattr(data, "__api_exclude__", None), so those dict-valued async request params will now serialize fields that were previously stripped. Please make the async BaseModel handling honor __api_exclude__ before switching dict values onto it.

Useful? React with 👍 / 👎.


if (
# List[T]
Expand Down
47 changes: 47 additions & 0 deletions tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,53 @@ async def test_base64_file_input(use_async: bool) -> None:
} # type: ignore[comparison-overlap]


@parametrize
@pytest.mark.asyncio
async def test_dict_of_base64_file_input(use_async: bool) -> None:
# a base64 file field nested inside a `Dict[str, T]` value is still transformed correctly
assert await transform({"key": {"foo": SAMPLE_FILE_PATH}}, Dict[str, TypedDictBase64Input], use_async) == {
"key": {"foo": "SGVsbG8sIHdvcmxkIQo="}
} # type: ignore[comparison-overlap]


@pytest.mark.asyncio
async def test_async_dict_values_use_async_transform(monkeypatch: pytest.MonkeyPatch) -> None:
# Regression test: the async transform pipeline must recurse into `Dict[str, T]`
# values using the *async* helpers so that file/base64 reads don't block the event
# loop. Previously the async dict branch mistakenly called the synchronous
# `_transform_recursive`, which reads files with a blocking `Path.read_bytes()`
# instead of the non-blocking `anyio.Path(...).read_bytes()`.
import openai._utils._transform as transform_mod

sync_calls = 0
async_calls = 0

real_format_data = transform_mod._format_data
real_async_format_data = transform_mod._async_format_data

def counting_format_data(*args: Any, **kwargs: Any) -> object:
nonlocal sync_calls
sync_calls += 1
return real_format_data(*args, **kwargs)

async def counting_async_format_data(*args: Any, **kwargs: Any) -> object:
nonlocal async_calls
async_calls += 1
return await real_async_format_data(*args, **kwargs)

monkeypatch.setattr(transform_mod, "_format_data", counting_format_data)
monkeypatch.setattr(transform_mod, "_async_format_data", counting_async_format_data)

result = await _async_transform(
{"key": {"foo": SAMPLE_FILE_PATH}},
expected_type=Dict[str, TypedDictBase64Input],
)

assert result == {"key": {"foo": "SGVsbG8sIHdvcmxkIQo="}}
assert async_calls >= 1, "async transform must use the async file-read path for dict values"
assert sync_calls == 0, "async transform must not fall back to the blocking sync file-read path"


@parametrize
@pytest.mark.asyncio
async def test_transform_skipping(use_async: bool) -> None:
Expand Down