Skip to content

fix: apply empty-content guard to empty-string chat tool outputs#3395

Closed
ioleksiuk wants to merge 1 commit into
openai:mainfrom
ioleksiuk:fix/empty-string-chat-tool-output
Closed

fix: apply empty-content guard to empty-string chat tool outputs#3395
ioleksiuk wants to merge 1 commit into
openai:mainfrom
ioleksiuk:fix/empty-string-chat-tool-output

Conversation

@ioleksiuk
Copy link
Copy Markdown
Contributor

Summary

Extends the fix in #3312 (`fix: avoid empty chat tool outputs`) to one case the original guard missed.

#3312 added an empty-content guard that swaps the tool message body with `_OMITTED_TOOL_OUTPUT_PLACEHOLDER` when the converted content is empty. The guard was placed inside the `else` branch of `isinstance(all_output_content, str)` at `chatcmpl_converter.py:749-769`, so it only triggered when the output was a non-text-only list. When the output is the empty string `""`, `extract_all_content("")` returns `""` (the string branch), `tool_result_content` is set to `""` without ever reaching the guard, and the Chat Completions message ends up with `"content": ""` — exactly the broken shape #3310/#3312 aimed to prevent.

Repro on current `main` (before this PR):

```python

from agents.models.chatcmpl_converter import Converter
items = [
... {"type": "function_call", "id": "fc_1", "call_id": "c1",
... "name": "get_status", "arguments": "{}"},
... {"type": "function_call_output", "call_id": "c1", "output": ""},
... ]
[m for m in Converter.items_to_messages(items) if m.get("role") == "tool"]
[{'role': 'tool', 'tool_call_id': 'c1', 'content': ''}]

Chat Completions API rejects: messages.X.content must be non-empty

```

Lift the guard above the `if/else` so both branches go through it. `not tool_result_content` is truthy for both `""` and `[]`, so the single check covers both empty shapes.

Test plan

  • New `test_items_to_messages_with_empty_string_function_output_uses_placeholder_by_default` — verifies the default-mode placeholder + warning for `output=""`.
  • New `test_items_to_messages_with_empty_string_function_output_raises_in_strict_mode` — verifies strict-mode UserError.
  • `uv run pytest tests/models/test_openai_chatcompletions_converter.py -k empty` — 4 passed (both pre-existing empty-list tests + the two new empty-string tests).
  • `uv run ruff check` on the touched files — All checks passed.

Issue number

N/A — found while auditing for the same fix pattern that #3312 applied to empty lists.

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation (no doc changes needed)
  • I've run `make lint` and `make format`
  • I've made sure tests pass

Extends the fix in openai#3312 (`fix: avoid empty chat tool outputs`) to one
case the original guard missed.

openai#3312 added an empty-content guard that swaps the tool message body
with `_OMITTED_TOOL_OUTPUT_PLACEHOLDER` when the converted content is
empty. The guard was placed inside the `else` branch of the
`isinstance(all_output_content, str)` check, so it only triggered when
the output was a non-text-only list. When the output is the empty
string `""`, `extract_all_content("")` returns `""` (the string
branch), `tool_result_content` is set to `""` without ever reaching
the guard, and the Chat Completions message ends up with
`"content": ""` — exactly the broken shape openai#3310/openai#3312 aimed to
prevent.

Repro on current main:

    items = [
        {"type": "function_call", "id": "fc_1", "call_id": "c1",
         "name": "get_status", "arguments": "{}"},
        {"type": "function_call_output", "call_id": "c1", "output": ""},
    ]
    msgs = Converter.items_to_messages(items)
    # tool msg content: ''   <-- API rejects: content must be non-empty

Lift the guard above the if/else so both branches go through it.
`not tool_result_content` is truthy for both `""` and `[]`, so the
single check covers both empty shapes.
@ioleksiuk
Copy link
Copy Markdown
Contributor Author

Closing — on second read of #3310, the issue reporter explicitly listed "String tool outputs remain unchanged." as a deliberate edge case. The placement of #3312's guard in the else branch was intentional, not an oversight.

Concrete reasons this PR doesn't make sense after all:

  1. ChatCompletionToolMessageParam.content is Required[Union[str, Iterable[...]]] per openai-python — empty string is a valid value of str, the API type does not reject it.
  2. When a user writes return "" from a function tool, the empty string is their intentional output. The SDK should not silently rewrite it to [tool output omitted].
  3. The bug fix: #3310 avoid empty chat tool outputs #3312 fixed was lossy conversion: a tool returns image/audio content → the converter strips it → message ends up empty. Empty-string output isn't lossy — it's faithful to the tool's return value.

My PR conflated "empty content after filtering non-representable parts" with "user explicitly returned empty string" — those are semantically different.

Apologies for the noise.

@ioleksiuk ioleksiuk closed this May 13, 2026
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.

1 participant