diff --git a/agent/transports/types.py b/agent/transports/types.py index 5199a5db1d..74481f85cd 100644 --- a/agent/transports/types.py +++ b/agent/transports/types.py @@ -61,6 +61,20 @@ class ToolCall: """Codex response_item_id from provider_data.""" return (self.provider_data or {}).get("response_item_id") + @property + def extra_content(self) -> Optional[Dict[str, Any]]: + """Gemini extra_content (thought_signature) from provider_data. + + Gemini 3 thinking models attach ``extra_content`` with a + ``thought_signature`` to each tool call. This signature must be + replayed on subsequent API calls — without it the API rejects the + request with HTTP 400. The chat_completions transport stores this + in ``provider_data["extra_content"]``; this property exposes it so + ``_build_assistant_message`` can ``getattr(tc, "extra_content")`` + uniformly. + """ + return (self.provider_data or {}).get("extra_content") + @dataclass class Usage: diff --git a/tests/agent/transports/test_types.py b/tests/agent/transports/test_types.py index 8391342496..dd3aadf1e1 100644 --- a/tests/agent/transports/test_types.py +++ b/tests/agent/transports/test_types.py @@ -200,6 +200,35 @@ class TestToolCallBackwardCompat: tc_no_pd = ToolCall(id="1", name="fn", arguments="{}") assert getattr(tc_no_pd, "call_id", None) is None + def test_extra_content_from_provider_data(self): + """Gemini thought_signature stored in provider_data is exposed via property.""" + ec = {"google": {"thought_signature": "SIG_ABC123"}} + tc = ToolCall(id="1", name="fn", arguments="{}", provider_data={"extra_content": ec}) + assert tc.extra_content == ec + + def test_extra_content_none_when_no_provider_data(self): + tc = ToolCall(id="1", name="fn", arguments="{}", provider_data=None) + assert tc.extra_content is None + + def test_extra_content_none_when_key_absent(self): + tc = ToolCall(id="1", name="fn", arguments="{}", provider_data={"call_id": "c1"}) + assert tc.extra_content is None + + def test_extra_content_getattr_pattern(self): + """_build_assistant_message uses getattr(tc, 'extra_content', None). + + This is the exact pattern that was broken before the extra_content + property was added — ToolCall lacked the property so getattr always + returned None, silently dropping the Gemini thought_signature and + causing HTTP 400 on subsequent turns (issue #14488). + """ + ec = {"google": {"thought_signature": "SIG_ABC123"}} + tc = ToolCall(id="1", name="fn", arguments="{}", provider_data={"extra_content": ec}) + assert getattr(tc, "extra_content", None) == ec + + tc_no_extra = ToolCall(id="1", name="fn", arguments="{}") + assert getattr(tc_no_extra, "extra_content", None) is None + class TestNormalizedResponseBackwardCompat: """Test properties that replaced _nr_to_assistant_message() shim."""