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 <noreply@anthropic.com>

* fix(chatgpt): annotate system-item rewrite with read-only types

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moe-berri 2026-10-27 17:03:28 -07:00 • committed by GitHub
parent 1b932332c1
commit 73cd296418
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 38 additions and 1 deletions

View file

@ -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,

View file

@ -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",
[