From 73cd2964183e42117e7a6d10debd37933b9c0c96 Mon Sep 17 00:00:00 2001 From: moe-berri Date: Tue, 27 Oct 2026 17:03:28 -0700 Subject: [PATCH] fix(chatgpt): send system input items to Codex as developer (#48668) * fix(chatgpt): send system input items to Codex as developer The Codex backend rejects {"type": "message", "role": "system"} input items with 400 "System messages are not allowed", while it accepts the same item with role=developer. The chat completions bridge emits that shape for every non-leading system message, and typed Responses API clients send it directly, so both paths failed. Rewrite system items to developer in place, which keeps their position and content. Fixes #48562 Co-Authored-By: Claude Opus 5.5 * fix(chatgpt): annotate system-item rewrite with read-only types Co-Authored-By: Claude Opus 5.5 * fix(chatgpt): rewrite system items on the typed input before the base transform Keeps the rewrite on known types so basedpyright sees no new unknowns. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../llms/chatgpt/responses/transformation.py | 15 +++++++++++- .../test_chatgpt_responses_transformation.py | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index 2352de35b5b..040b7b8c1d8 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Final, cast import httpx +from openai.types.responses.response_input_param import ResponseInputItemParam from litellm.exceptions import AuthenticationError from litellm.litellm_core_utils.core_helpers import process_response_headers @@ -42,6 +43,16 @@ if TYPE_CHECKING: _CHATGPT_SERVICE_TIERS: Final = {"default": "default", "priority": "priority", "fast": "priority"} +def _system_item_as_developer(item: ResponseInputItemParam) -> ResponseInputItemParam: + # The Codex backend 400s on {"type": "message", "role": "system"} items ("System messages are not + # allowed") but accepts role=developer in the same position, which is what the Codex CLI sends. + if item.get("role") != "system": + return item + return cast( # cast-ok: only the role changes, and developer is valid wherever system is + ResponseInputItemParam, {**item, "role": "developer"} + ) + + class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): def __init__(self, authenticator: Authenticator | None = None) -> None: super().__init__() @@ -101,7 +112,9 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): ) -> dict: request: Final = super().transform_responses_api_request( model, - [{"role": "user", "content": input}] if isinstance(input, str) else input, + [{"role": "user", "content": input}] + if isinstance(input, str) + else [_system_item_as_developer(item) for item in input], response_api_optional_request_params, litellm_params, headers, diff --git a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py index d88bdfc0b41..8985d0b7fd8 100644 --- a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py +++ b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py @@ -74,6 +74,30 @@ class TestChatGPTResponsesAPITransformation: assert "service_tier" not in request + def test_chatgpt_sends_system_input_items_as_developer(self) -> None: + system_string_item: Final = {"role": "system", "content": "Be brief."} + system_blocks_item: Final = { + "type": "message", + "role": "system", + "content": [{"type": "input_text", "text": "Answer in French."}], + } + user_item: Final = {"role": "user", "content": "Hello"} + developer_item: Final = {"role": "developer", "content": "Use metric units."} + request: Final = ChatGPTResponsesAPIConfig().transform_responses_api_request( + model="chatgpt/gpt-6.1-sol", + input=[system_string_item, user_item, system_blocks_item, developer_item], + response_api_optional_request_params={}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert request["input"] == [ + {**system_string_item, "role": "developer"}, + user_item, + {**system_blocks_item, "role": "developer"}, + developer_item, + ] + @pytest.mark.parametrize( "model_name", [