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
5 changes: 4 additions & 1 deletion src/agents/models/chatcmpl_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,10 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
content_items = reasoning_item.get("content", [])
encrypted_content = reasoning_item.get("encrypted_content")

item_provider_data: dict[str, Any] = reasoning_item.get("provider_data", {}) # type: ignore[assignment]
raw_provider_data = reasoning_item.get("provider_data")
item_provider_data: dict[str, Any] = (
raw_provider_data if isinstance(raw_provider_data, dict) else {}
)
item_model = item_provider_data.get("model", "")
should_replay = False

Expand Down
97 changes: 97 additions & 0 deletions tests/models/test_chatcmpl_reasoning_provider_data_none.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Regression test for chatcmpl_converter handling of reasoning items
with ``provider_data`` explicitly set to ``None``.

JSON roundtripping (and some external producers) can store a reasoning item
where ``provider_data`` is the literal ``None`` rather than missing or a dict.
``Converter.items_to_messages`` previously assumed the field was always either
absent or a dict and called ``.get("model", "")`` directly on it, which raised
``AttributeError: 'NoneType' object has no attribute 'get'`` for these
otherwise valid items.
"""

from __future__ import annotations

import json
from typing import Any, cast

import pytest

from agents.items import TResponseInputItem
from agents.models.chatcmpl_converter import Converter


def _reasoning_item_dict_with_provider_data_none() -> dict[str, Any]:
return {
"type": "reasoning",
"id": "__fake_id__",
"summary": [{"type": "summary_text", "text": "thinking"}],
"content": [{"type": "reasoning_text", "text": "step"}],
"encrypted_content": None,
"status": None,
"provider_data": None,
}


def test_items_to_messages_handles_provider_data_none() -> None:
"""Converter must not crash when a reasoning item has provider_data=None."""
items = [cast(TResponseInputItem, _reasoning_item_dict_with_provider_data_none())]

# The bug was a hard AttributeError raised at conversion time. The exact
# contents of the resulting messages are not the focus here — we only need
# to assert that conversion completes for both Claude and non-Claude
# targets and returns a list.
messages_claude = Converter.items_to_messages(
items,
model="claude-sonnet-4",
preserve_thinking_blocks=True,
)
assert isinstance(messages_claude, list)

messages_gpt = Converter.items_to_messages(
items,
model="gpt-4o",
preserve_thinking_blocks=False,
)
assert isinstance(messages_gpt, list)


def test_items_to_messages_handles_provider_data_none_after_json_roundtrip() -> None:
"""JSON serialization preserves a None value, exercising the same path."""
item_dict = _reasoning_item_dict_with_provider_data_none()
roundtripped: dict[str, Any] = json.loads(json.dumps(item_dict))

# Sanity check: the None survives the roundtrip.
assert roundtripped["provider_data"] is None

messages = Converter.items_to_messages(
[cast(TResponseInputItem, roundtripped)],
model="claude-sonnet-4",
preserve_thinking_blocks=True,
)
assert isinstance(messages, list)


@pytest.mark.parametrize(
"bogus_provider_data",
[123, "not-a-dict", ["model", "x"]],
)
def test_items_to_messages_handles_non_dict_provider_data(
bogus_provider_data: object,
) -> None:
"""Non-dict provider_data values are treated as missing rather than crashing."""
item_dict: dict[str, Any] = {
"type": "reasoning",
"id": "__fake_id__",
"summary": [{"type": "summary_text", "text": "thinking"}],
"content": [{"type": "reasoning_text", "text": "step"}],
"encrypted_content": None,
"status": None,
"provider_data": bogus_provider_data,
}

messages = Converter.items_to_messages(
[cast(TResponseInputItem, item_dict)],
model="claude-sonnet-4",
preserve_thinking_blocks=True,
)
assert isinstance(messages, list)
Loading