This commit is contained in:
Devon Krisman 2026-10-23 18:44:16 +00:00 • committed by GitHub
commit 4687804ec0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 89 additions and 9 deletions

View file

@ -2,6 +2,7 @@ from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Final, cast
import httpx
from pydantic import TypeAdapter, ValidationError
from litellm.exceptions import AuthenticationError
from litellm.litellm_core_utils.core_helpers import process_response_headers
@ -39,6 +40,29 @@ if TYPE_CHECKING:
_CHATGPT_SERVICE_TIERS: Final = {"default": "default", "priority": "priority", "fast": "priority"}
_STRUCTURED_OUTPUT_FORMAT_TYPES: Final = frozenset({"json_schema", "json_object"})
_REQUEST_FIELDS: Final = TypeAdapter(dict[str, object])
_MAPPING_FIELDS: Final = TypeAdapter(Mapping[str, object])
def _as_mapping(value: object) -> "Mapping[str, object] | None":
try:
return _MAPPING_FIELDS.validate_python(value)
except ValidationError:
return None
def _has_structured_output_format(text: object) -> bool:
text_mapping: Final = _as_mapping(text)
if text_mapping is None:
return False
format_config: Final = _as_mapping(text_mapping.get("format"))
if format_config is None:
return False
return format_config.get("type") in _STRUCTURED_OUTPUT_FORMAT_TYPES
class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
def __init__(self, authenticator: Authenticator | None = None) -> None:
@ -77,23 +101,28 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> dict:
request: Final = super().transform_responses_api_request(
model,
input,
response_api_optional_request_params,
litellm_params,
headers,
request: Final = _REQUEST_FIELDS.validate_python(
super().transform_responses_api_request(
model,
input,
response_api_optional_request_params,
litellm_params,
headers,
)
)
base_instructions: Final = get_chatgpt_default_instructions()
existing_instructions: Final = request.get("instructions")
if existing_instructions:
if isinstance(existing_instructions, str) and existing_instructions:
if base_instructions not in existing_instructions:
request["instructions"] = f"{base_instructions}\n\n{existing_instructions}"
else:
request["instructions"] = base_instructions
request["store"] = False
request["stream"] = True
include: Final = list(request.get("include") or [])
include_value: Final = request.get("include")
include: Final[list[object]] = ( # mutable-ok: request["include"] is a JSON array on the wire
list(include_value) if isinstance(include_value, list) else []
)
if "reasoning.encrypted_content" not in include:
include.append("reasoning.encrypted_content")
request["include"] = include
@ -110,10 +139,12 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
"reasoning",
"previous_response_id",
"truncation",
*(("text",) if _has_structured_output_format(request.get("text")) else ()),
}
filtered: Final = {k: v for k, v in request.items() if k in allowed_keys}
service_tier: Final = _CHATGPT_SERVICE_TIERS.get(request.get("service_tier"))
requested_tier: Final = request.get("service_tier")
service_tier: Final = _CHATGPT_SERVICE_TIERS.get(requested_tier) if isinstance(requested_tier, str) else None
if service_tier is not None:
filtered["service_tier"] = service_tier
return filtered

View file

@ -238,6 +238,55 @@ class TestChatGPTResponsesAPITransformation:
"function": {"name": "hello"},
}
@pytest.mark.parametrize(
"text_param",
[
{
"format": {
"type": "json_schema",
"name": "verdict",
"schema": {
"type": "object",
"properties": {"block": {"type": "boolean"}},
"required": ["block"],
},
}
},
{"format": {"type": "json_object"}},
],
)
def test_chatgpt_preserves_structured_output_text(self, text_param):
config = ChatGPTResponsesAPIConfig()
request = config.transform_responses_api_request(
model="chatgpt/gpt-5.3-codex",
input="hi",
response_api_optional_request_params={"text": text_param},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert request["text"] == text_param
@pytest.mark.parametrize(
"text_param",
[
{"format": {"type": "text"}},
{"verbosity": "low"},
"not-a-dict",
],
)
def test_chatgpt_drops_non_structured_text_param(self, text_param):
config = ChatGPTResponsesAPIConfig()
request = config.transform_responses_api_request(
model="chatgpt/gpt-5.3-codex",
input="hi",
response_api_optional_request_params={"text": text_param},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert "text" not in request
@pytest.mark.parametrize(
("model_name", "response_model"),
[