mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
Merge e10ff10247 into aa3d59d086
This commit is contained in:
commit
f92b1b461e
2 changed files with 168 additions and 0 deletions
|
|
@ -41,6 +41,27 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
def custom_llm_provider(self) -> LlmProviders:
|
||||
return LlmProviders.CHATGPT
|
||||
|
||||
@staticmethod
|
||||
def _normalized_responses_input(
|
||||
request_input: str | list[Any], # mutable-ok: outbound Responses payload is a JSON list by spec
|
||||
) -> list[Any]: # mutable-ok: same — the ChatGPT backend requires the message-list form
|
||||
"""Return the ChatGPT-compatible form of a Responses API ``input`` value.
|
||||
|
||||
The public Responses API accepts the string shorthand; the ChatGPT OAuth
|
||||
backend requires the message-list form. Non-string values are returned
|
||||
unchanged.
|
||||
"""
|
||||
if isinstance(request_input, str):
|
||||
return [ # mutable-ok: ChatGPT backend requires a JSON message list
|
||||
{ # mutable-ok: message payload is inherently a JSON object
|
||||
"role": "user",
|
||||
"content": [ # mutable-ok: ditto
|
||||
{"type": "input_text", "text": request_input}, # mutable-ok: ditto
|
||||
],
|
||||
}
|
||||
]
|
||||
return request_input
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
@ -76,6 +97,9 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
litellm_params,
|
||||
headers,
|
||||
)
|
||||
request["input"] = self._normalized_responses_input(
|
||||
request_input=request.get("input", []), # mutable-ok: fallback is an empty Responses input list
|
||||
)
|
||||
base_instructions: Final = get_chatgpt_default_instructions()
|
||||
existing_instructions: Final = request.get("instructions")
|
||||
if existing_instructions:
|
||||
|
|
@ -106,6 +130,34 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
|
||||
return {k: v for k, v in request.items() if k in allowed_keys}
|
||||
|
||||
def sign_request(
|
||||
self,
|
||||
headers: dict, # mutable-ok: signature is pinned by BaseResponsesAPIConfig.sign_request
|
||||
optional_params: dict, # mutable-ok: ditto
|
||||
request_data: dict, # mutable-ok: ditto
|
||||
api_base: str,
|
||||
api_key: str | None = None,
|
||||
model: str | None = None,
|
||||
stream: bool | None = None,
|
||||
fake_stream: bool | None = None,
|
||||
) -> tuple[dict, bytes | None]: # mutable-ok: signature is pinned by BaseResponsesAPIConfig.sign_request
|
||||
"""Re-normalize ``input`` after the handler's ``extra_body`` merge.
|
||||
|
||||
The HTTP handlers merge ``extra_body`` into the transformed request *after*
|
||||
``transform_responses_api_request`` and call this hook last, right before the
|
||||
body is sent — so ``extra_body`` can reintroduce the plain-string ``input``
|
||||
shorthand that the transform already normalized away. Re-run the normalizer
|
||||
here as the final defense. No signing is needed: the ChatGPT backend takes a
|
||||
bearer token, so this only repairs the body and returns it unsigned (the
|
||||
handlers send ``request_data`` itself as JSON when signed body bytes are
|
||||
``None``).
|
||||
"""
|
||||
current_input: Final = request_data.get("input")
|
||||
if isinstance(current_input, str):
|
||||
normalized: Final = self._normalized_responses_input(request_input=current_input)
|
||||
request_data["input"] = normalized # rebind-ok: handler sends this same dict as the outbound JSON body
|
||||
return headers, None
|
||||
|
||||
def transform_response_api_response(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -101,9 +101,125 @@ class TestChatGPTResponsesAPITransformation:
|
|||
)
|
||||
|
||||
assert request["stream"] is True
|
||||
assert request["input"] == [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "hi"}],
|
||||
}
|
||||
]
|
||||
assert "reasoning.encrypted_content" in request["include"]
|
||||
assert request["instructions"].startswith("You are Codex, based on GPT-5.")
|
||||
|
||||
def test_chatgpt_normalizes_string_input_with_tools(self):
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
tools = [{"type": "function", "name": "echo", "parameters": {}}]
|
||||
request = config.transform_responses_api_request(
|
||||
model="chatgpt/gpt-5.3-codex",
|
||||
input="Call echo with ping.",
|
||||
response_api_optional_request_params={
|
||||
"tools": tools,
|
||||
"tool_choice": "required",
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert request["input"] == [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "Call echo with ping."}],
|
||||
}
|
||||
]
|
||||
assert request["tools"] == tools
|
||||
assert request["tool_choice"] == "required"
|
||||
|
||||
def test_sign_request_repairs_string_input_after_extra_body_merge(self):
|
||||
"""sign_request must be the last line of defense against extra_body.
|
||||
|
||||
The HTTP handlers merge ``extra_body`` into the request AFTER
|
||||
``transform_responses_api_request`` and call ``sign_request`` last, so a
|
||||
string ``input`` arriving via ``extra_body`` bypasses the transform-level
|
||||
normalization. Mirror the handler order here: transform -> extra_body
|
||||
merge -> sign_request.
|
||||
"""
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
tools = [{"type": "function", "name": "echo", "parameters": {}}]
|
||||
extra_body = {
|
||||
"input": "hello from extra_body",
|
||||
"tools": tools,
|
||||
"tool_choice": "required",
|
||||
}
|
||||
|
||||
data = config.transform_responses_api_request(
|
||||
model="chatgpt/gpt-5.3-codex",
|
||||
input="original input",
|
||||
response_api_optional_request_params={},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
# Same merge the sync/async Responses HTTP handlers perform (data.update(extra_body))
|
||||
data.update(extra_body)
|
||||
assert data["input"] == "hello from extra_body" # transform normalization was overridden
|
||||
|
||||
headers = {"Authorization": "Bearer token", "session_id": "session-123"}
|
||||
returned_headers, signed_body = config.sign_request(
|
||||
headers=headers,
|
||||
optional_params={},
|
||||
request_data=data,
|
||||
api_base="https://chatgpt.example.com/responses",
|
||||
)
|
||||
|
||||
assert data["input"] == [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "hello from extra_body"},
|
||||
],
|
||||
}
|
||||
]
|
||||
# tools / tool_choice survive the repair untouched
|
||||
assert data["tools"] == tools
|
||||
assert data["tool_choice"] == "required"
|
||||
# no request signing for ChatGPT: headers pass through, body stays JSON-serialized by the handler
|
||||
assert returned_headers == headers
|
||||
assert signed_body is None
|
||||
|
||||
def test_sign_request_keeps_list_input_unchanged(self):
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
list_input = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "already normalized"}],
|
||||
}
|
||||
]
|
||||
data = {"input": list_input, "model": "gpt-5.3-codex"}
|
||||
|
||||
returned_headers, signed_body = config.sign_request(
|
||||
headers={"content-type": "application/json"},
|
||||
optional_params={},
|
||||
request_data=data,
|
||||
api_base="https://chatgpt.example.com/responses",
|
||||
)
|
||||
|
||||
assert data["input"] is list_input # same object, not rebuilt
|
||||
assert returned_headers == {"content-type": "application/json"}
|
||||
assert signed_body is None
|
||||
|
||||
def test_sign_request_noop_without_input(self):
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
data = {"model": "gpt-5.3-codex", "stream": True}
|
||||
|
||||
returned_headers, signed_body = config.sign_request(
|
||||
headers={},
|
||||
optional_params={},
|
||||
request_data=data,
|
||||
api_base="https://chatgpt.example.com/responses",
|
||||
)
|
||||
|
||||
assert data == {"model": "gpt-5.3-codex", "stream": True}
|
||||
assert returned_headers == {}
|
||||
assert signed_body is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue