diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index edb63849cb7..81c3131eace 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -600,7 +600,11 @@ def _chat_messages_to_responses_input( # Input preflight / validation # --------------------------------------------------------------------------- -def _preflight_codex_input_items(raw_items: Any) -> List[Dict[str, Any]]: +def _preflight_codex_input_items( + raw_items: Any, + *, + is_github_responses: bool = False, +) -> List[Dict[str, Any]]: if not isinstance(raw_items, list): raise ValueError("Codex Responses input must be a list of input items.") @@ -741,7 +745,11 @@ def _preflight_codex_input_items(raw_items: Any) -> List[Dict[str, Any]]: "content": normalized_content, } item_id = item.get("id") - if isinstance(item_id, str) and item_id.strip(): + if ( + not is_github_responses + and isinstance(item_id, str) + and item_id.strip() + ): stripped_id = item_id.strip() if len(stripped_id) <= _MAX_RESPONSES_ITEM_ID_LENGTH: normalized_item["id"] = stripped_id @@ -816,6 +824,7 @@ def _preflight_codex_api_kwargs( api_kwargs: Any, *, allow_stream: bool = False, + is_github_responses: bool = False, ) -> Dict[str, Any]: if not isinstance(api_kwargs, dict): raise ValueError("Codex Responses request must be a dict.") @@ -837,7 +846,10 @@ def _preflight_codex_api_kwargs( instructions = str(instructions) instructions = instructions.strip() or DEFAULT_AGENT_IDENTITY - normalized_input = _preflight_codex_input_items(api_kwargs.get("input")) + normalized_input = _preflight_codex_input_items( + api_kwargs.get("input"), + is_github_responses=is_github_responses, + ) tools = api_kwargs.get("tools") normalized_tools = None diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index ce62ffcde1a..f3b2bc0d39e 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -1167,7 +1167,11 @@ def run_conversation( if agent._force_ascii_payload: _sanitize_structure_non_ascii(api_kwargs) if agent.api_mode == "codex_responses": - api_kwargs = agent._get_transport().preflight_kwargs(api_kwargs, allow_stream=False) + api_kwargs = agent._get_transport().preflight_kwargs( + api_kwargs, + allow_stream=False, + is_github_responses=agent._is_copilot_url(), + ) # Copilot x-initiator: the first API call of a user turn is # marked "user" so Copilot bills a premium request; tool-loop # follow-ups keep the default "agent" header (#3040). @@ -1314,6 +1318,12 @@ def run_conversation( _use_streaming = False def _perform_api_call(next_api_kwargs): + if agent.api_mode == "codex_responses": + next_api_kwargs = agent._get_transport().preflight_kwargs( + next_api_kwargs, + allow_stream=False, + is_github_responses=agent._is_copilot_url(), + ) if _use_streaming: return agent._interruptible_streaming_api_call( next_api_kwargs, on_first_delta=_stop_spinner diff --git a/agent/transports/codex.py b/agent/transports/codex.py index c362173696a..a592df35035 100644 --- a/agent/transports/codex.py +++ b/agent/transports/codex.py @@ -440,13 +440,23 @@ class ResponsesApiTransport(ProviderTransport): return False return True - def preflight_kwargs(self, api_kwargs: Any, *, allow_stream: bool = False) -> dict: + def preflight_kwargs( + self, + api_kwargs: Any, + *, + allow_stream: bool = False, + is_github_responses: bool = False, + ) -> dict: """Validate and sanitize Codex API kwargs before the call. Normalizes input items, strips unsupported fields, validates structure. """ from agent.codex_responses_adapter import _preflight_codex_api_kwargs - return _preflight_codex_api_kwargs(api_kwargs, allow_stream=allow_stream) + return _preflight_codex_api_kwargs( + api_kwargs, + allow_stream=allow_stream, + is_github_responses=is_github_responses, + ) def map_finish_reason(self, raw_reason: str) -> str: """Map Codex response.status to OpenAI finish_reason. diff --git a/tests/agent/test_codex_responses_adapter.py b/tests/agent/test_codex_responses_adapter.py index e3c699d3ede..72b8787e02e 100644 --- a/tests/agent/test_codex_responses_adapter.py +++ b/tests/agent/test_codex_responses_adapter.py @@ -238,6 +238,27 @@ def test_preflight_codex_input_items_keeps_short_message_id(): assert items[0]["id"] == _VALID_ITEM_ID +def test_preflight_codex_input_items_drops_short_id_for_github_responses(): + items = _preflight_codex_input_items( + [ + { + "type": "message", + "role": "assistant", + "status": "in_progress", + "content": [{"type": "output_text", "text": "pong"}], + "id": _VALID_ITEM_ID, + "phase": "final_answer", + } + ], + is_github_responses=True, + ) + + assert "id" not in items[0] + assert items[0]["status"] == "in_progress" + assert items[0]["phase"] == "final_answer" + assert items[0]["content"] == [{"type": "output_text", "text": "pong"}] + + def test_preflight_codex_api_kwargs_drops_oversized_message_id_end_to_end(): kwargs = _preflight_codex_api_kwargs( { diff --git a/tests/agent/transports/test_codex_transport.py b/tests/agent/transports/test_codex_transport.py index 84a269feef8..ccbf0963673 100644 --- a/tests/agent/transports/test_codex_transport.py +++ b/tests/agent/transports/test_codex_transport.py @@ -181,6 +181,34 @@ class TestCodexBuildKwargs: message_item = next(item for item in kw["input"] if item.get("type") == "message") assert message_item["id"] == "msg_short_id" + def test_github_preflight_drops_id_reintroduced_by_request_override(self, transport): + injected = { + "type": "message", + "role": "assistant", + "status": "in_progress", + "content": [{"type": "output_text", "text": "pong"}], + "id": "stale_short", + "phase": "final_answer", + } + kw = transport.build_kwargs( + model="gpt-5.5", + messages=[{"role": "user", "content": "continue"}], + tools=[], + is_github_responses=True, + request_overrides={"input": [injected]}, + ) + + preflight = transport.preflight_kwargs( + kw, + is_github_responses=True, + ) + + message_item = preflight["input"][0] + assert "id" not in message_item + assert message_item["status"] == "in_progress" + assert message_item["phase"] == "final_answer" + assert message_item["content"] == [{"type": "output_text", "text": "pong"}] + def test_non_github_responses_keeps_message_item_id_end_to_end(self, transport): messages = [ {"role": "system", "content": "You are Hermes."},