diff --git a/src/openai/resources/chat/completions/completions.py b/src/openai/resources/chat/completions/completions.py index b2b65683f9..973c962268 100644 --- a/src/openai/resources/chat/completions/completions.py +++ b/src/openai/resources/chat/completions/completions.py @@ -195,8 +195,10 @@ def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseForma "/chat/completions", body=maybe_transform( { - "messages": messages, "model": model, + "stream": False, + "service_tier": service_tier, + "messages": messages, "audio": audio, "frequency_penalty": frequency_penalty, "function_call": function_call, @@ -219,10 +221,8 @@ def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseForma "response_format": _type_to_response_format(response_format), "safety_identifier": safety_identifier, "seed": seed, - "service_tier": service_tier, "stop": stop, "store": store, - "stream": False, "stream_options": stream_options, "temperature": temperature, "tool_choice": tool_choice, @@ -1829,8 +1829,10 @@ def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseForma "/chat/completions", body=await async_maybe_transform( { - "messages": messages, "model": model, + "stream": False, + "service_tier": service_tier, + "messages": messages, "audio": audio, "frequency_penalty": frequency_penalty, "function_call": function_call, @@ -1853,10 +1855,8 @@ def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseForma "response_format": _type_to_response_format(response_format), "safety_identifier": safety_identifier, "seed": seed, - "service_tier": service_tier, "store": store, "stop": stop, - "stream": False, "stream_options": stream_options, "temperature": temperature, "tool_choice": tool_choice, diff --git a/src/openai/resources/responses/responses.py b/src/openai/resources/responses/responses.py index 25cc854f53..f8023a6f87 100644 --- a/src/openai/resources/responses/responses.py +++ b/src/openai/resources/responses/responses.py @@ -1397,6 +1397,9 @@ def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: "/responses", body=maybe_transform( { + "model": model, + "stream": stream, + "service_tier": service_tier, "access_programs": access_programs, "background": background, "context_management": context_management, @@ -1407,7 +1410,6 @@ def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: "max_output_tokens": max_output_tokens, "max_tool_calls": max_tool_calls, "metadata": metadata, - "model": model, "moderation": moderation, "parallel_tool_calls": parallel_tool_calls, "previous_response_id": previous_response_id, @@ -1417,9 +1419,7 @@ def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: "prompt_cache_retention": prompt_cache_retention, "reasoning": reasoning, "safety_identifier": safety_identifier, - "service_tier": service_tier, "store": store, - "stream": stream, "stream_options": stream_options, "temperature": temperature, "text": text, @@ -3305,6 +3305,9 @@ def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: "/responses", body=maybe_transform( { + "model": model, + "stream": stream, + "service_tier": service_tier, "access_programs": access_programs, "background": background, "context_management": context_management, @@ -3315,7 +3318,6 @@ def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: "max_output_tokens": max_output_tokens, "max_tool_calls": max_tool_calls, "metadata": metadata, - "model": model, "moderation": moderation, "parallel_tool_calls": parallel_tool_calls, "previous_response_id": previous_response_id, @@ -3325,9 +3327,7 @@ def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: "prompt_cache_retention": prompt_cache_retention, "reasoning": reasoning, "safety_identifier": safety_identifier, - "service_tier": service_tier, "store": store, - "stream": stream, "stream_options": stream_options, "temperature": temperature, "text": text, diff --git a/tests/test_request_body_routing_order.py b/tests/test_request_body_routing_order.py new file mode 100644 index 0000000000..b9b556d2a6 --- /dev/null +++ b/tests/test_request_body_routing_order.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import json +from typing import Any + +import httpx2 +import pytest + +from openai import OpenAI, AsyncOpenAI + +LARGE_PAYLOAD = "x" * (1024 * 1024) + + +def response_for(request: httpx2.Request) -> httpx2.Response: + if request.url.path.endswith("/responses"): + return httpx2.Response( + 200, + json={ + "id": "resp_test", + "created_at": 0, + "model": "gpt-test", + "object": "response", + "output": [], + "parallel_tool_calls": True, + "status": "completed", + "tool_choice": "auto", + "tools": [], + }, + ) + + return httpx2.Response( + 200, + json={ + "id": "chatcmpl_test", + "choices": [], + "created": 0, + "model": "gpt-test", + "object": "chat.completion", + }, + ) + + +def request_keys(request: httpx2.Request) -> list[str]: + pairs: list[tuple[str, Any]] = json.loads(request.content, object_pairs_hook=lambda items: items) + return [key for key, _ in pairs] + + +def assert_request_keys( + request: httpx2.Request, + *, + prefix: list[str], + absent: tuple[str, ...] = (), +) -> None: + keys = request_keys(request) + assert keys[: len(prefix)] == prefix + assert not set(absent).intersection(keys) + + +def test_parse_helpers_prioritize_routing_fields_on_the_wire() -> None: + requests: list[httpx2.Request] = [] + + def record(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + return response_for(request) + + with OpenAI( + api_key="test-key", + base_url="https://example.test/v1", + http_client=httpx2.Client(transport=httpx2.MockTransport(record), trust_env=False), + ) as client: + client.responses.parse( + model="gpt-test", + stream=False, + service_tier="default", + input=LARGE_PAYLOAD, + ) + client.chat.completions.parse( + model="gpt-test", + service_tier="default", + messages=[{"role": "user", "content": LARGE_PAYLOAD}], + ) + client.responses.parse(model="gpt-test", input=LARGE_PAYLOAD) + client.chat.completions.parse( + model="gpt-test", + messages=[{"role": "user", "content": LARGE_PAYLOAD}], + ) + + assert_request_keys( + requests[0], + prefix=["model", "stream", "service_tier", "input"], + ) + assert_request_keys( + requests[1], + prefix=["model", "stream", "service_tier", "messages"], + ) + assert_request_keys( + requests[2], + prefix=["model", "input"], + absent=("stream", "service_tier"), + ) + assert_request_keys( + requests[3], + prefix=["model", "stream", "messages"], + absent=("service_tier",), + ) + + +@pytest.mark.asyncio +async def test_async_parse_helpers_prioritize_routing_fields_on_the_wire() -> None: + requests: list[httpx2.Request] = [] + + def record(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + return response_for(request) + + async with AsyncOpenAI( + api_key="test-key", + base_url="https://example.test/v1", + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(record), trust_env=False), + ) as client: + await client.responses.parse( + model="gpt-test", + stream=False, + service_tier="default", + input=LARGE_PAYLOAD, + ) + await client.chat.completions.parse( + model="gpt-test", + service_tier="default", + messages=[{"role": "user", "content": LARGE_PAYLOAD}], + ) + await client.responses.parse(model="gpt-test", input=LARGE_PAYLOAD) + await client.chat.completions.parse( + model="gpt-test", + messages=[{"role": "user", "content": LARGE_PAYLOAD}], + ) + + assert_request_keys( + requests[0], + prefix=["model", "stream", "service_tier", "input"], + ) + assert_request_keys( + requests[1], + prefix=["model", "stream", "service_tier", "messages"], + ) + assert_request_keys( + requests[2], + prefix=["model", "input"], + absent=("stream", "service_tier"), + ) + assert_request_keys( + requests[3], + prefix=["model", "stream", "messages"], + absent=("service_tier",), + )