diff --git a/.castiron.stats.yml b/.castiron.stats.yml
index 92eb4f5552..d003338ffb 100644
--- a/.castiron.stats.yml
+++ b/.castiron.stats.yml
@@ -1,7 +1,7 @@
schema_version: 1
-generation_id: edefdf16-46c7-4f93-94a6-10028a087b3e
-openapi_spec_hash: 0c25fc15ac5b26e2054196d579cc67a0
-openapi_transformed_spec_hash: 651cea16581547a33af56bd631894a03
+generation_id: 5dfacb1b-1abf-4d69-9083-e6586471a6f6
+openapi_spec_hash: 53e44788ffb3382947f27b9cf1d14cfe
+openapi_transformed_spec_hash: 1385724889e077e80e65b0e8da123eca
config_hash: 8a684aec06907ff8ced7809753e16225
-codegen_sha: a1dd47dd01ce07be1ecbc39e2f66f2156d89c6f3
-codegen_hash: 2c7827fd0ccd38edcd80a97839ae5c10cf89f8da20ea468d4b20c41468607b81
+codegen_sha: 11cd26ec903a4168e95565cd46fb7f33f657afca
+codegen_hash: 5a9715f0fcf2bbdc00a4d9ad8b7c749a51e4d0531ee776d1ae7707881e0adf2a
diff --git a/api_reference/openapi.transformed.yml b/api_reference/openapi.transformed.yml
index ff6378fcfa..12f30ba247 100644
--- a/api_reference/openapi.transformed.yml
+++ b/api_reference/openapi.transformed.yml
@@ -64749,8 +64749,8 @@ components:
ResponseConfigurationUpdate:
type: object
description: |
- A configuration update that applies to subsequent responses until it is
- replaced by another configuration update.
+ A configuration update for subsequent responses. Each setting stays in effect
+ until a later update replaces that setting.
properties:
id:
type: string
@@ -64772,15 +64772,27 @@ components:
description: |
The reasoning effort used for subsequent responses until another
configuration update replaces it.
+ text:
+ type: object
+ description: The text output configuration for subsequent responses.
+ properties:
+ format:
+ description: Use a JSON schema for structured output or text to return to plain text.
+ discriminator:
+ propertyName: type
+ anyOf:
+ - $ref: '#/components/schemas/ResponseFormatText'
+ - $ref: '#/components/schemas/TextResponseFormatJsonSchema'
+ required:
+ - format
required:
- id
- type
ResponseConfigurationUpdateItemParam:
type: object
description: |
- An update to the conversation's response configuration. The configuration
- remains in effect for subsequent responses until it is replaced by another
- configuration update.
+ An update to the conversation's response configuration. Supply reasoning,
+ text, or both. Each setting stays in effect until a later update replaces it.
properties:
id:
anyOf:
@@ -64804,6 +64816,19 @@ components:
description: |
The reasoning effort to use for subsequent responses until another
configuration update replaces it.
+ text:
+ type: object
+ description: The text output configuration for subsequent responses.
+ properties:
+ format:
+ description: Use a JSON schema for structured output or text to return to plain text.
+ discriminator:
+ propertyName: type
+ anyOf:
+ - $ref: '#/components/schemas/ResponseFormatText'
+ - $ref: '#/components/schemas/TextResponseFormatJsonSchema'
+ required:
+ - format
required:
- type
ResponseContentPartAddedEvent:
@@ -70390,13 +70415,13 @@ components:
Setting to `{ "type": "json_object" }` enables the older JSON mode, which
ensures the message the model generates is valid JSON. Using `json_schema`
is preferred for models that support it.
+ discriminator:
+ propertyName: type
anyOf:
- $ref: '#/components/schemas/ResponseFormatText'
- $ref: '#/components/schemas/TextResponseFormatJsonSchema'
x-openai-ruby-json-schema-input: true
- $ref: '#/components/schemas/ResponseFormatJsonObject'
- discriminator:
- propertyName: type
TextResponseFormatJsonSchema:
x-openai-structured-output: json_schema_default_strict
type: object
@@ -98392,12 +98417,12 @@ components:
Setting to `{ "type": "json_object" }` enables the older JSON mode, which
ensures the message the model generates is valid JSON. Using `json_schema`
is preferred for models that support it.
+ discriminator:
+ propertyName: type
anyOf:
- $ref: '#/components/schemas/BetaResponseFormatText'
- $ref: '#/components/schemas/BetaTextResponseFormatJsonSchema'
- $ref: '#/components/schemas/BetaResponseFormatJsonObject'
- discriminator:
- propertyName: type
BetaResponseFormatJsonObject:
type: object
title: JSON object
@@ -101460,9 +101485,8 @@ components:
BetaResponseConfigurationUpdateItemParam:
type: object
description: |
- An update to the conversation's response configuration. The configuration
- remains in effect for subsequent responses until it is replaced by another
- configuration update.
+ An update to the conversation's response configuration. Supply reasoning,
+ text, or both. Each setting stays in effect until a later update replaces it.
properties:
agent:
anyOf:
@@ -101491,6 +101515,19 @@ components:
description: |
The reasoning effort to use for subsequent responses until another
configuration update replaces it.
+ text:
+ type: object
+ description: The text output configuration for subsequent responses.
+ properties:
+ format:
+ description: Use a JSON schema for structured output or text to return to plain text.
+ discriminator:
+ propertyName: type
+ anyOf:
+ - $ref: '#/components/schemas/BetaResponseFormatText'
+ - $ref: '#/components/schemas/BetaTextResponseFormatJsonSchema'
+ required:
+ - format
required:
- type
BetaAdditionalToolsItemParam:
@@ -104217,8 +104254,8 @@ components:
BetaResponseConfigurationUpdate:
type: object
description: |
- A configuration update that applies to subsequent responses until it is
- replaced by another configuration update.
+ A configuration update for subsequent responses. Each setting stays in effect
+ until a later update replaces that setting.
properties:
agent:
$ref: '#/components/schemas/BetaAgentTag'
@@ -104243,6 +104280,19 @@ components:
description: |
The reasoning effort used for subsequent responses until another
configuration update replaces it.
+ text:
+ type: object
+ description: The text output configuration for subsequent responses.
+ properties:
+ format:
+ description: Use a JSON schema for structured output or text to return to plain text.
+ discriminator:
+ propertyName: type
+ anyOf:
+ - $ref: '#/components/schemas/BetaResponseFormatText'
+ - $ref: '#/components/schemas/BetaTextResponseFormatJsonSchema'
+ required:
+ - format
required:
- id
- type
diff --git a/helpers.md b/helpers.md
index c979675254..8be574b2ce 100644
--- a/helpers.md
+++ b/helpers.md
@@ -63,6 +63,41 @@ output into a Pydantic model. The same parsing rules apply to
Use `output_parsed` for the structured result and retain `output` when replaying
messages so their phases are preserved.
+When the output schema changes during a conversation, pass `parse_as` to
+`client.responses.parse()` to select the local output type. For example, if a
+previous turn switched to `DetailedAnswer`, continue with this call.
+
+```py
+response = client.responses.parse(
+ model=model,
+ previous_response_id=previous.id,
+ input="Explain the evidence.",
+ parse_as=DetailedAnswer,
+)
+```
+
+The SDK parses the answer as `DetailedAnswer`. The request inherits its output
+format from the previous turn. If you also pass `text_format=ShortAnswer`,
+the request sends that class's schema as the top-level format. Pass
+`parse_as=None` to read unparsed text when the previous turn reset the format.
+You can also pass a format update directly in `input`. For example, given a
+JSON schema format named `detailed_format`:
+
+```py
+response = client.responses.parse(
+ model=model,
+ input=[
+ {"type": "configuration_update", "text": {"format": detailed_format}},
+ {"role": "user", "content": "Explain the evidence."},
+ ],
+ parse_as=DetailedAnswer,
+)
+```
+
+To switch back to plain text, send `{"type": "text"}` as the update's format.
+A reasoning-only update keeps the active format. Retrieved configuration items
+include `item.text.format` when the input supplied it.
+
## Auto-parsing function tool calls
The `.parse()` method will also automatically parse `function` tool calls if:
diff --git a/scripts/castiron/generated-files.json b/scripts/castiron/generated-files.json
index 0fa0d0b98e..fcfa0ab308 100644
--- a/scripts/castiron/generated-files.json
+++ b/scripts/castiron/generated-files.json
@@ -619,7 +619,7 @@
},
"src/openai/resources/responses/api.md": {
"mode": "100644",
- "sha256": "e7815b1278aa2780bb885ac5a249fe00859dff67d3899b708d25d2968e734785"
+ "sha256": "b635f37005fb7126cc69a7b69e3f14a9df135d9523271cc4407b1ffcb9bd5c8c"
},
"src/openai/resources/responses/input_items.py": {
"mode": "100644",
@@ -631,7 +631,7 @@
},
"src/openai/resources/responses/responses.py": {
"mode": "100644",
- "sha256": "b9a2ef1b61d33450318babc49e2b25f604628983804e7ebf4291f8802f372aee"
+ "sha256": "e74696203a015c8a87a6e1117d5e73449a1dd4b591c57d4c68d1cfda2641934a"
},
"src/openai/resources/safety/__init__.py": {
"mode": "100644",
@@ -2523,15 +2523,15 @@
},
"src/openai/types/beta/beta_response_configuration_update_item.py": {
"mode": "100644",
- "sha256": "4bd97e32e710edbd688bf8c6eac0dd8f24d1d0a64da60efda522ffefa0790046"
+ "sha256": "eebab46ab26a7650a94cf353d3b98c86fc12378284a9431cd4379808515ba6b0"
},
"src/openai/types/beta/beta_response_configuration_update_item_param.py": {
"mode": "100644",
- "sha256": "eb06f107b27ef1fa3a9266bfaf8921b68384faf7c0dd0d300e554767bb7aed30"
+ "sha256": "c10511a06c2bd67530ce2f6ab2e94b63e49602e4e29df8cfb64e4bb73359e3a9"
},
"src/openai/types/beta/beta_response_configuration_update_item_param_param.py": {
"mode": "100644",
- "sha256": "3c9bcd2d7e730b102b0674424e56cbd35e6bed0664a58acc1830466eea28d2be"
+ "sha256": "269015eac1b4dd3a8bfbd1053218e64c9a2fd3cac4b50db961089f3813c6bbd0"
},
"src/openai/types/beta/beta_response_container_reference.py": {
"mode": "100644",
@@ -6167,15 +6167,15 @@
},
"src/openai/types/responses/response_configuration_update_item.py": {
"mode": "100644",
- "sha256": "4d07aca0f5dbb6f87f1db31fc16dbba2e4a49d0670edbd46256907066605a703"
+ "sha256": "89a39d84322be94f3805145e1ca70bae0c2b0500f89102ea93918700ecc65a02"
},
"src/openai/types/responses/response_configuration_update_item_param.py": {
"mode": "100644",
- "sha256": "c8eb1b18f489c4d820bc17c10e505e97a4da8059592e903882aae5ed3ca919ad"
+ "sha256": "fb7a2cb3b72d2a5dd73fc78ad6194bce51eb4e1b805d9ccc85c2725e3c0bab3f"
},
"src/openai/types/responses/response_configuration_update_item_param_param.py": {
"mode": "100644",
- "sha256": "8e177dbfa309934a8aedbc5bd278b48bddfa3365a42cfb661ead8677aa924a9b"
+ "sha256": "d7e43648f155a90a35d2f57d31e8f8481c463ada28805dd810d17db00304ce23"
},
"src/openai/types/responses/response_container_reference.py": {
"mode": "100644",
diff --git a/src/openai/lib/_response_resources.py b/src/openai/lib/_response_resources.py
index 27795b7a8d..0d0050d740 100644
--- a/src/openai/lib/_response_resources.py
+++ b/src/openai/lib/_response_resources.py
@@ -8,7 +8,7 @@
import httpx2
from openai._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit
-from openai._utils import is_given, maybe_transform
+from openai._utils import is_dict, is_list, is_given, maybe_transform
from openai._streaming import Stream
from openai.lib._tools import PydanticFunctionTool, ResponsesPydanticFunctionTool
from openai._base_client import make_request_options
@@ -36,10 +36,41 @@
from openai.resources.responses.responses import Responses, AsyncResponses
+def _output_text_parser(
+ *,
+ input: object,
+ text: ResponseTextConfigParam | Omit,
+ text_format: type[TextFormatT] | Omit,
+ parse_as: type[TextFormatT] | None | Omit,
+) -> type[TextFormatT] | Omit:
+ if parse_as is None:
+ return omit
+
+ if is_list(input):
+ for item in reversed(input):
+ if not is_dict(item) or item.get("type") != "configuration_update":
+ continue
+ update = item.get("text")
+ if not is_dict(update) or not is_dict(fmt := update.get("format")):
+ continue
+ if fmt.get("type") == "text":
+ if is_given(parse_as):
+ raise TypeError("Cannot use parse_as with a plain-text format update")
+ return omit
+ if is_given(text_format) and not is_given(parse_as) and (not is_given(text) or fmt != text.get("format")):
+ raise TypeError(
+ "Pass parse_as for the updated output schema, or parse_as=None to read plain output_text"
+ )
+ break
+
+ return parse_as if is_given(parse_as) else text_format
+
+
def parse(
resource: Responses,
*,
text_format: type[TextFormatT] | Omit = omit,
+ parse_as: type[TextFormatT] | None | Omit = omit,
access_programs: response_create_params.AccessPrograms | Omit = omit,
background: Optional[bool] | Omit = omit,
context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
@@ -89,12 +120,18 @@ def parse(
text = copy(text)
text["format"] = _type_to_text_format_param(text_format)
+ output_format = _output_text_parser(
+ input=extra_body.get("input", input) if is_dict(extra_body) else input,
+ text=text,
+ text_format=text_format,
+ parse_as=parse_as,
+ )
tools = _make_tools(tools)
def parser(raw_response: Response) -> ParsedResponse[TextFormatT]:
return parse_response(
input_tools=tools,
- text_format=text_format,
+ text_format=output_format,
response=raw_response,
)
@@ -156,6 +193,7 @@ async def async_parse(
resource: AsyncResponses,
*,
text_format: type[TextFormatT] | Omit = omit,
+ parse_as: type[TextFormatT] | None | Omit = omit,
access_programs: response_create_params.AccessPrograms | Omit = omit,
background: Optional[bool] | Omit = omit,
context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
@@ -205,12 +243,18 @@ async def async_parse(
text = copy(text)
text["format"] = _type_to_text_format_param(text_format)
+ output_format = _output_text_parser(
+ input=extra_body.get("input", input) if is_dict(extra_body) else input,
+ text=text,
+ text_format=text_format,
+ parse_as=parse_as,
+ )
tools = _make_tools(tools)
def parser(raw_response: Response) -> ParsedResponse[TextFormatT]:
return parse_response(
input_tools=tools,
- text_format=text_format,
+ text_format=output_format,
response=raw_response,
)
diff --git a/src/openai/lib/responses_tool_runner.py b/src/openai/lib/responses_tool_runner.py
index 09b5b70298..59a3b9e6f4 100644
--- a/src/openai/lib/responses_tool_runner.py
+++ b/src/openai/lib/responses_tool_runner.py
@@ -49,6 +49,7 @@
from ..types.responses.tool_param import ToolParam, FunctionToolParam
from .streaming.responses._events import ResponseStreamEvent
from .streaming.responses._responses import ResponseStream, AsyncResponseStream
+from ..types.responses.response_input import ResponseInput
from ..types.responses.response_input_param import ComputerCallOutput, FunctionCallOutput, ResponseInputParam
from ..types.responses.response_output_item import McpCall, McpApprovalRequest, ResponseOutputItem
from ..types.responses.response_create_params import ResponseCreateParamsStreaming, ResponseCreateParamsNonStreaming
@@ -506,8 +507,9 @@ def _history_calls(history: ResponseInputParam, tools: list[ToolParam], *, budge
# Modern computer calls can omit the legacy checklist; validate these with
# the same action parser as dispatch, without changing the saved input.
try:
+ # The input models alias wire fields such as "schema" that are reserved by Pydantic v1.
validate_type(
- type_=ResponseInputParam,
+ type_=ResponseInput,
value=[item for item in history if item.get("type") not in ("computer_call", "computer_call_output")],
)
except pydantic.ValidationError:
@@ -522,6 +524,9 @@ def _history_calls(history: ResponseInputParam, tools: list[ToolParam], *, budge
answered: set[str] = set()
for index, item in enumerate(cast(list[dict[str, object]], history)):
kind = item.get("type", "message")
+ if kind == "configuration_update":
+ # Already schema-validated above. There is no tool call to pair.
+ continue
if budget_only:
if kind == "compaction_trigger":
if index != len(history) - 1:
diff --git a/src/openai/resources/responses/api.md b/src/openai/resources/responses/api.md
index 9ef00fb631..c45a2b2ae7 100644
--- a/src/openai/resources/responses/api.md
+++ b/src/openai/resources/responses/api.md
@@ -180,7 +180,7 @@ Methods:
- client.responses.delete(response_id) -> None
- client.responses.cancel(response_id) -> Response
- client.responses.compact(\*\*params) -> CompactedResponse
-- client.responses.parse(\*, text_format=..., access_programs=..., background=..., context_management=..., conversation=..., include=..., input=..., instructions=..., max_output_tokens=..., max_tool_calls=..., metadata=..., model=..., moderation=..., parallel_tool_calls=..., previous_response_id=..., prompt=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning=..., safety_identifier=..., service_tier=..., store=..., stream=..., stream_options=..., temperature=..., text=..., tool_choice=..., tools=..., top_logprobs=..., top_p=..., truncation=..., user=..., verbosity=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ParsedResponse[TextFormatT]
+- client.responses.parse(\*, text_format=..., parse_as=..., access_programs=..., background=..., context_management=..., conversation=..., include=..., input=..., instructions=..., max_output_tokens=..., max_tool_calls=..., metadata=..., model=..., moderation=..., parallel_tool_calls=..., previous_response_id=..., prompt=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning=..., safety_identifier=..., service_tier=..., store=..., stream=..., stream_options=..., temperature=..., text=..., tool_choice=..., tools=..., top_logprobs=..., top_p=..., truncation=..., user=..., verbosity=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ParsedResponse[Any]
- client.responses.stream(\*, response_id=..., input=..., model=..., access_programs=..., background=..., context_management=..., text_format=..., tools=..., conversation=..., include=..., instructions=..., max_output_tokens=..., max_tool_calls=..., metadata=..., parallel_tool_calls=..., moderation=..., previous_response_id=..., prompt=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning=..., safety_identifier=..., service_tier=..., store=..., stream_options=..., temperature=..., text=..., tool_choice=..., top_logprobs=..., top_p=..., truncation=..., user=..., starting_after=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ResponseStreamManager[TextFormatT]
## InputItems
diff --git a/src/openai/resources/responses/responses.py b/src/openai/resources/responses/responses.py
index 1eac3b2e96..56b9ad67fd 100644
--- a/src/openai/resources/responses/responses.py
+++ b/src/openai/resources/responses/responses.py
@@ -114,10 +114,101 @@ def with_streaming_response(self) -> ResponsesWithStreamingResponse:
"""
return ResponsesWithStreamingResponse(self)
+ @overload
+ def parse(
+ self,
+ *,
+ text_format: type | Omit = omit,
+ parse_as: type[TextFormatT] | None,
+ access_programs: response_create_params.AccessPrograms | Omit = omit,
+ background: Optional[bool] | Omit = omit,
+ context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
+ conversation: Optional[response_create_params.Conversation] | Omit = omit,
+ include: Optional[List[ResponseIncludable]] | Omit = omit,
+ input: Union[str, ResponseInputParam] | Omit = omit,
+ instructions: Optional[str] | Omit = omit,
+ max_output_tokens: Optional[int] | Omit = omit,
+ max_tool_calls: Optional[int] | Omit = omit,
+ metadata: Optional[Metadata] | Omit = omit,
+ model: ResponsesModel | Omit = omit,
+ moderation: Optional[response_create_params.Moderation] | Omit = omit,
+ parallel_tool_calls: Optional[bool] | Omit = omit,
+ previous_response_id: Optional[str] | Omit = omit,
+ prompt: Optional[ResponsePromptParam] | Omit = omit,
+ prompt_cache_key: Optional[str] | Omit = omit,
+ prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit,
+ prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit,
+ reasoning: Optional[Reasoning] | Omit = omit,
+ safety_identifier: Optional[str] | Omit = omit,
+ service_tier: Optional[ServiceTier] | Omit = omit,
+ store: Optional[bool] | Omit = omit,
+ stream: Optional[Literal[False]] | Literal[True] | Omit = omit,
+ stream_options: Optional[response_create_params.StreamOptions] | Omit = omit,
+ temperature: Optional[float] | Omit = omit,
+ text: ResponseTextConfigParam | Omit = omit,
+ tool_choice: response_create_params.ToolChoice | Omit = omit,
+ tools: Iterable[ParseableToolParam] | Omit = omit,
+ top_logprobs: Optional[int] | Omit = omit,
+ top_p: Optional[float] | Omit = omit,
+ truncation: Optional[Literal["auto", "disabled"]] | Omit = omit,
+ user: str | Omit = omit,
+ verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx2.Timeout | None | NotGiven = not_given,
+ ) -> ParsedResponse[TextFormatT]: ...
+
+ @overload
def parse(
self,
*,
text_format: type[TextFormatT] | Omit = omit,
+ parse_as: Omit = omit,
+ access_programs: response_create_params.AccessPrograms | Omit = omit,
+ background: Optional[bool] | Omit = omit,
+ context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
+ conversation: Optional[response_create_params.Conversation] | Omit = omit,
+ include: Optional[List[ResponseIncludable]] | Omit = omit,
+ input: Union[str, ResponseInputParam] | Omit = omit,
+ instructions: Optional[str] | Omit = omit,
+ max_output_tokens: Optional[int] | Omit = omit,
+ max_tool_calls: Optional[int] | Omit = omit,
+ metadata: Optional[Metadata] | Omit = omit,
+ model: ResponsesModel | Omit = omit,
+ moderation: Optional[response_create_params.Moderation] | Omit = omit,
+ parallel_tool_calls: Optional[bool] | Omit = omit,
+ previous_response_id: Optional[str] | Omit = omit,
+ prompt: Optional[ResponsePromptParam] | Omit = omit,
+ prompt_cache_key: Optional[str] | Omit = omit,
+ prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit,
+ prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit,
+ reasoning: Optional[Reasoning] | Omit = omit,
+ safety_identifier: Optional[str] | Omit = omit,
+ service_tier: Optional[ServiceTier] | Omit = omit,
+ store: Optional[bool] | Omit = omit,
+ stream: Optional[Literal[False]] | Literal[True] | Omit = omit,
+ stream_options: Optional[response_create_params.StreamOptions] | Omit = omit,
+ temperature: Optional[float] | Omit = omit,
+ text: ResponseTextConfigParam | Omit = omit,
+ tool_choice: response_create_params.ToolChoice | Omit = omit,
+ tools: Iterable[ParseableToolParam] | Omit = omit,
+ top_logprobs: Optional[int] | Omit = omit,
+ top_p: Optional[float] | Omit = omit,
+ truncation: Optional[Literal["auto", "disabled"]] | Omit = omit,
+ user: str | Omit = omit,
+ verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx2.Timeout | None | NotGiven = not_given,
+ ) -> ParsedResponse[TextFormatT]: ...
+
+ def parse(
+ self,
+ *,
+ text_format: type | Omit = omit,
+ parse_as: type | None | Omit = omit,
access_programs: response_create_params.AccessPrograms | Omit = omit,
background: Optional[bool] | Omit = omit,
context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
@@ -155,11 +246,12 @@ def parse(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
- ) -> ParsedResponse[TextFormatT]:
- """parse"""
+ ) -> ParsedResponse[Any]:
+ """Parse output with `parse_as` when supplied. Only `text_format` sets the request schema."""
return parse(
self,
text_format=text_format,
+ parse_as=parse_as,
access_programs=access_programs,
background=background,
context_management=context_management,
@@ -1940,10 +2032,101 @@ def with_streaming_response(self) -> AsyncResponsesWithStreamingResponse:
"""
return AsyncResponsesWithStreamingResponse(self)
+ @overload
+ async def parse(
+ self,
+ *,
+ text_format: type | Omit = omit,
+ parse_as: type[TextFormatT] | None,
+ access_programs: response_create_params.AccessPrograms | Omit = omit,
+ background: Optional[bool] | Omit = omit,
+ context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
+ conversation: Optional[response_create_params.Conversation] | Omit = omit,
+ include: Optional[List[ResponseIncludable]] | Omit = omit,
+ input: Union[str, ResponseInputParam] | Omit = omit,
+ instructions: Optional[str] | Omit = omit,
+ max_output_tokens: Optional[int] | Omit = omit,
+ max_tool_calls: Optional[int] | Omit = omit,
+ metadata: Optional[Metadata] | Omit = omit,
+ model: ResponsesModel | Omit = omit,
+ moderation: Optional[response_create_params.Moderation] | Omit = omit,
+ parallel_tool_calls: Optional[bool] | Omit = omit,
+ previous_response_id: Optional[str] | Omit = omit,
+ prompt: Optional[ResponsePromptParam] | Omit = omit,
+ prompt_cache_key: Optional[str] | Omit = omit,
+ prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit,
+ prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit,
+ reasoning: Optional[Reasoning] | Omit = omit,
+ safety_identifier: Optional[str] | Omit = omit,
+ service_tier: Optional[ServiceTier] | Omit = omit,
+ store: Optional[bool] | Omit = omit,
+ stream: Optional[Literal[False]] | Literal[True] | Omit = omit,
+ stream_options: Optional[response_create_params.StreamOptions] | Omit = omit,
+ temperature: Optional[float] | Omit = omit,
+ text: ResponseTextConfigParam | Omit = omit,
+ tool_choice: response_create_params.ToolChoice | Omit = omit,
+ tools: Iterable[ParseableToolParam] | Omit = omit,
+ top_logprobs: Optional[int] | Omit = omit,
+ top_p: Optional[float] | Omit = omit,
+ truncation: Optional[Literal["auto", "disabled"]] | Omit = omit,
+ user: str | Omit = omit,
+ verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx2.Timeout | None | NotGiven = not_given,
+ ) -> ParsedResponse[TextFormatT]: ...
+
+ @overload
async def parse(
self,
*,
text_format: type[TextFormatT] | Omit = omit,
+ parse_as: Omit = omit,
+ access_programs: response_create_params.AccessPrograms | Omit = omit,
+ background: Optional[bool] | Omit = omit,
+ context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
+ conversation: Optional[response_create_params.Conversation] | Omit = omit,
+ include: Optional[List[ResponseIncludable]] | Omit = omit,
+ input: Union[str, ResponseInputParam] | Omit = omit,
+ instructions: Optional[str] | Omit = omit,
+ max_output_tokens: Optional[int] | Omit = omit,
+ max_tool_calls: Optional[int] | Omit = omit,
+ metadata: Optional[Metadata] | Omit = omit,
+ model: ResponsesModel | Omit = omit,
+ moderation: Optional[response_create_params.Moderation] | Omit = omit,
+ parallel_tool_calls: Optional[bool] | Omit = omit,
+ previous_response_id: Optional[str] | Omit = omit,
+ prompt: Optional[ResponsePromptParam] | Omit = omit,
+ prompt_cache_key: Optional[str] | Omit = omit,
+ prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit,
+ prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit,
+ reasoning: Optional[Reasoning] | Omit = omit,
+ safety_identifier: Optional[str] | Omit = omit,
+ service_tier: Optional[ServiceTier] | Omit = omit,
+ store: Optional[bool] | Omit = omit,
+ stream: Optional[Literal[False]] | Literal[True] | Omit = omit,
+ stream_options: Optional[response_create_params.StreamOptions] | Omit = omit,
+ temperature: Optional[float] | Omit = omit,
+ text: ResponseTextConfigParam | Omit = omit,
+ tool_choice: response_create_params.ToolChoice | Omit = omit,
+ tools: Iterable[ParseableToolParam] | Omit = omit,
+ top_logprobs: Optional[int] | Omit = omit,
+ top_p: Optional[float] | Omit = omit,
+ truncation: Optional[Literal["auto", "disabled"]] | Omit = omit,
+ user: str | Omit = omit,
+ verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx2.Timeout | None | NotGiven = not_given,
+ ) -> ParsedResponse[TextFormatT]: ...
+
+ async def parse(
+ self,
+ *,
+ text_format: type | Omit = omit,
+ parse_as: type | None | Omit = omit,
access_programs: response_create_params.AccessPrograms | Omit = omit,
background: Optional[bool] | Omit = omit,
context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
@@ -1981,11 +2164,12 @@ async def parse(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
- ) -> ParsedResponse[TextFormatT]:
- """parse"""
+ ) -> ParsedResponse[Any]:
+ """Parse output with `parse_as` when supplied. Only `text_format` sets the request schema."""
return await async_parse(
self,
text_format=text_format,
+ parse_as=parse_as,
access_programs=access_programs,
background=background,
context_management=context_management,
diff --git a/src/openai/types/beta/beta_response_configuration_update_item.py b/src/openai/types/beta/beta_response_configuration_update_item.py
index a6d1780521..e0779ef6d3 100644
--- a/src/openai/types/beta/beta_response_configuration_update_item.py
+++ b/src/openai/types/beta/beta_response_configuration_update_item.py
@@ -1,11 +1,13 @@
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
-from typing import Optional
-from typing_extensions import Literal
+from typing import Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .beta_response_format_text_json_schema_config import BetaResponseFormatTextJSONSchemaConfig
-__all__ = ["BetaResponseConfigurationUpdateItem", "Agent", "Reasoning"]
+__all__ = ["BetaResponseConfigurationUpdateItem", "Agent", "Reasoning", "Text", "TextFormat", "TextFormatText"]
class Agent(BaseModel):
@@ -25,10 +27,30 @@ class Reasoning(BaseModel):
"""
+class TextFormatText(BaseModel):
+ """Default response format. Used to generate text responses."""
+
+ type: Literal["text"]
+ """The type of response format being defined. Always `text`."""
+
+
+TextFormat: TypeAlias = Annotated[
+ Union[TextFormatText, BetaResponseFormatTextJSONSchemaConfig], PropertyInfo(discriminator="type")
+]
+
+
+class Text(BaseModel):
+ """The text output configuration for subsequent responses."""
+
+ format: TextFormat
+ """Use a JSON schema for structured output or text to return to plain text."""
+
+
class BetaResponseConfigurationUpdateItem(BaseModel):
- """
- A configuration update that applies to subsequent responses until it is
- replaced by another configuration update.
+ """A configuration update for subsequent responses.
+
+ Each setting stays in effect
+ until a later update replaces that setting.
"""
id: str
@@ -42,3 +64,6 @@ class BetaResponseConfigurationUpdateItem(BaseModel):
reasoning: Optional[Reasoning] = None
"""The reasoning configuration applied by this update."""
+
+ text: Optional[Text] = None
+ """The text output configuration for subsequent responses."""
diff --git a/src/openai/types/beta/beta_response_configuration_update_item_param.py b/src/openai/types/beta/beta_response_configuration_update_item_param.py
index 36090be07d..2f6e759918 100644
--- a/src/openai/types/beta/beta_response_configuration_update_item_param.py
+++ b/src/openai/types/beta/beta_response_configuration_update_item_param.py
@@ -1,11 +1,13 @@
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
-from typing import Optional
-from typing_extensions import Literal
+from typing import Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .beta_response_format_text_json_schema_config import BetaResponseFormatTextJSONSchemaConfig
-__all__ = ["BetaResponseConfigurationUpdateItemParam", "Agent", "Reasoning"]
+__all__ = ["BetaResponseConfigurationUpdateItemParam", "Agent", "Reasoning", "Text", "TextFormat", "TextFormatText"]
class Agent(BaseModel):
@@ -25,12 +27,30 @@ class Reasoning(BaseModel):
"""
+class TextFormatText(BaseModel):
+ """Default response format. Used to generate text responses."""
+
+ type: Literal["text"]
+ """The type of response format being defined. Always `text`."""
+
+
+TextFormat: TypeAlias = Annotated[
+ Union[TextFormatText, BetaResponseFormatTextJSONSchemaConfig], PropertyInfo(discriminator="type")
+]
+
+
+class Text(BaseModel):
+ """The text output configuration for subsequent responses."""
+
+ format: TextFormat
+ """Use a JSON schema for structured output or text to return to plain text."""
+
+
class BetaResponseConfigurationUpdateItemParam(BaseModel):
"""An update to the conversation's response configuration.
- The configuration
- remains in effect for subsequent responses until it is replaced by another
- configuration update.
+ Supply reasoning,
+ text, or both. Each setting stays in effect until a later update replaces it.
"""
type: Literal["configuration_update"]
@@ -44,3 +64,6 @@ class BetaResponseConfigurationUpdateItemParam(BaseModel):
reasoning: Optional[Reasoning] = None
"""Updates to reasoning configuration. Only effort is supported."""
+
+ text: Optional[Text] = None
+ """The text output configuration for subsequent responses."""
diff --git a/src/openai/types/beta/beta_response_configuration_update_item_param_param.py b/src/openai/types/beta/beta_response_configuration_update_item_param_param.py
index 1c9b060f83..4b233c90c6 100644
--- a/src/openai/types/beta/beta_response_configuration_update_item_param_param.py
+++ b/src/openai/types/beta/beta_response_configuration_update_item_param_param.py
@@ -2,10 +2,19 @@
from __future__ import annotations
-from typing import Optional
-from typing_extensions import Literal, Required, TypedDict
+from typing import Union, Optional
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
-__all__ = ["BetaResponseConfigurationUpdateItemParamParam", "Agent", "Reasoning"]
+from .beta_response_format_text_json_schema_config_param import BetaResponseFormatTextJSONSchemaConfigParam
+
+__all__ = [
+ "BetaResponseConfigurationUpdateItemParamParam",
+ "Agent",
+ "Reasoning",
+ "Text",
+ "TextFormat",
+ "TextFormatText",
+]
class Agent(TypedDict, total=False):
@@ -25,12 +34,28 @@ class Reasoning(TypedDict, total=False):
"""
+class TextFormatText(TypedDict, total=False):
+ """Default response format. Used to generate text responses."""
+
+ type: Required[Literal["text"]]
+ """The type of response format being defined. Always `text`."""
+
+
+TextFormat: TypeAlias = Union[TextFormatText, BetaResponseFormatTextJSONSchemaConfigParam]
+
+
+class Text(TypedDict, total=False):
+ """The text output configuration for subsequent responses."""
+
+ format: Required[TextFormat]
+ """Use a JSON schema for structured output or text to return to plain text."""
+
+
class BetaResponseConfigurationUpdateItemParamParam(TypedDict, total=False):
"""An update to the conversation's response configuration.
- The configuration
- remains in effect for subsequent responses until it is replaced by another
- configuration update.
+ Supply reasoning,
+ text, or both. Each setting stays in effect until a later update replaces it.
"""
type: Required[Literal["configuration_update"]]
@@ -44,3 +69,6 @@ class BetaResponseConfigurationUpdateItemParamParam(TypedDict, total=False):
reasoning: Reasoning
"""Updates to reasoning configuration. Only effort is supported."""
+
+ text: Text
+ """The text output configuration for subsequent responses."""
diff --git a/src/openai/types/responses/response_configuration_update_item.py b/src/openai/types/responses/response_configuration_update_item.py
index 5d14e6091d..9224cb3000 100644
--- a/src/openai/types/responses/response_configuration_update_item.py
+++ b/src/openai/types/responses/response_configuration_update_item.py
@@ -1,12 +1,15 @@
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
-from typing import Optional
-from typing_extensions import Literal
+from typing import Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+from ..._utils import PropertyInfo
from ..._models import BaseModel
from ..shared.reasoning_effort import ReasoningEffort
+from ..shared.response_format_text import ResponseFormatText
+from .response_format_text_json_schema_config import ResponseFormatTextJSONSchemaConfig
-__all__ = ["ResponseConfigurationUpdateItem", "Reasoning"]
+__all__ = ["ResponseConfigurationUpdateItem", "Reasoning", "Text", "TextFormat"]
class Reasoning(BaseModel):
@@ -19,10 +22,23 @@ class Reasoning(BaseModel):
"""
+TextFormat: TypeAlias = Annotated[
+ Union[ResponseFormatText, ResponseFormatTextJSONSchemaConfig], PropertyInfo(discriminator="type")
+]
+
+
+class Text(BaseModel):
+ """The text output configuration for subsequent responses."""
+
+ format: TextFormat
+ """Use a JSON schema for structured output or text to return to plain text."""
+
+
class ResponseConfigurationUpdateItem(BaseModel):
- """
- A configuration update that applies to subsequent responses until it is
- replaced by another configuration update.
+ """A configuration update for subsequent responses.
+
+ Each setting stays in effect
+ until a later update replaces that setting.
"""
id: str
@@ -33,3 +49,6 @@ class ResponseConfigurationUpdateItem(BaseModel):
reasoning: Optional[Reasoning] = None
"""The reasoning configuration applied by this update."""
+
+ text: Optional[Text] = None
+ """The text output configuration for subsequent responses."""
diff --git a/src/openai/types/responses/response_configuration_update_item_param.py b/src/openai/types/responses/response_configuration_update_item_param.py
index 7a18c2a51d..157dcc31be 100644
--- a/src/openai/types/responses/response_configuration_update_item_param.py
+++ b/src/openai/types/responses/response_configuration_update_item_param.py
@@ -1,12 +1,15 @@
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
-from typing import Optional
-from typing_extensions import Literal
+from typing import Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+from ..._utils import PropertyInfo
from ..._models import BaseModel
from ..shared.reasoning_effort import ReasoningEffort
+from ..shared.response_format_text import ResponseFormatText
+from .response_format_text_json_schema_config import ResponseFormatTextJSONSchemaConfig
-__all__ = ["ResponseConfigurationUpdateItemParam", "Reasoning"]
+__all__ = ["ResponseConfigurationUpdateItemParam", "Reasoning", "Text", "TextFormat"]
class Reasoning(BaseModel):
@@ -19,12 +22,23 @@ class Reasoning(BaseModel):
"""
+TextFormat: TypeAlias = Annotated[
+ Union[ResponseFormatText, ResponseFormatTextJSONSchemaConfig], PropertyInfo(discriminator="type")
+]
+
+
+class Text(BaseModel):
+ """The text output configuration for subsequent responses."""
+
+ format: TextFormat
+ """Use a JSON schema for structured output or text to return to plain text."""
+
+
class ResponseConfigurationUpdateItemParam(BaseModel):
"""An update to the conversation's response configuration.
- The configuration
- remains in effect for subsequent responses until it is replaced by another
- configuration update.
+ Supply reasoning,
+ text, or both. Each setting stays in effect until a later update replaces it.
"""
type: Literal["configuration_update"]
@@ -35,3 +49,6 @@ class ResponseConfigurationUpdateItemParam(BaseModel):
reasoning: Optional[Reasoning] = None
"""Updates to reasoning configuration. Only effort is supported."""
+
+ text: Optional[Text] = None
+ """The text output configuration for subsequent responses."""
diff --git a/src/openai/types/responses/response_configuration_update_item_param_param.py b/src/openai/types/responses/response_configuration_update_item_param_param.py
index ed7e5949ec..f14f79b344 100644
--- a/src/openai/types/responses/response_configuration_update_item_param_param.py
+++ b/src/openai/types/responses/response_configuration_update_item_param_param.py
@@ -2,12 +2,14 @@
from __future__ import annotations
-from typing import Optional
-from typing_extensions import Literal, Required, TypedDict
+from typing import Union, Optional
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
from ..shared.reasoning_effort import ReasoningEffort
+from ..shared_params.response_format_text import ResponseFormatText
+from .response_format_text_json_schema_config_param import ResponseFormatTextJSONSchemaConfigParam
-__all__ = ["ResponseConfigurationUpdateItemParamParam", "Reasoning"]
+__all__ = ["ResponseConfigurationUpdateItemParamParam", "Reasoning", "Text", "TextFormat"]
class Reasoning(TypedDict, total=False):
@@ -20,12 +22,21 @@ class Reasoning(TypedDict, total=False):
"""
+TextFormat: TypeAlias = Union[ResponseFormatText, ResponseFormatTextJSONSchemaConfigParam]
+
+
+class Text(TypedDict, total=False):
+ """The text output configuration for subsequent responses."""
+
+ format: Required[TextFormat]
+ """Use a JSON schema for structured output or text to return to plain text."""
+
+
class ResponseConfigurationUpdateItemParamParam(TypedDict, total=False):
"""An update to the conversation's response configuration.
- The configuration
- remains in effect for subsequent responses until it is replaced by another
- configuration update.
+ Supply reasoning,
+ text, or both. Each setting stays in effect until a later update replaces it.
"""
type: Required[Literal["configuration_update"]]
@@ -36,3 +47,6 @@ class ResponseConfigurationUpdateItemParamParam(TypedDict, total=False):
reasoning: Reasoning
"""Updates to reasoning configuration. Only effort is supported."""
+
+ text: Text
+ """The text output configuration for subsequent responses."""
diff --git a/tests/lib/responses/test_parse_as.py b/tests/lib/responses/test_parse_as.py
new file mode 100644
index 0000000000..dd3bba194e
--- /dev/null
+++ b/tests/lib/responses/test_parse_as.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import json
+from typing_extensions import assert_type
+
+import pytest
+from pydantic import BaseModel
+
+from openai import OpenAI, AsyncOpenAI
+from tests.respx2 import MockRouter
+from openai.types.responses import ResponseConfigurationUpdateItem, ResponseConfigurationUpdateItemParamParam
+from openai.lib._parsing._responses import type_to_text_format_param
+
+from ...conftest import base_url
+
+
+class ShortAnswer(BaseModel):
+ answer: str
+
+
+class DetailedAnswer(ShortAnswer):
+ evidence: str
+
+
+def _detailed_update() -> ResponseConfigurationUpdateItemParamParam:
+ fmt = type_to_text_format_param(DetailedAnswer)
+ assert fmt["type"] == "json_schema"
+ return {"type": "configuration_update", "text": {"format": fmt}}
+
+
+@pytest.mark.respx2(base_url=base_url)
+@pytest.mark.parametrize("client,async_client", [(False, False)], indirect=True)
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_parse_as_on_continuation(
+ client: OpenAI, async_client: AsyncOpenAI, respx2_mock: MockRouter, sync: bool
+) -> None:
+ text = '{"answer":"yes","evidence":"the receipt"}'
+ route = respx2_mock.post("/responses").respond(
+ json={"output": [{"type": "message", "content": [{"type": "output_text", "text": text}]}]}
+ )
+ if sync:
+ response = client.responses.parse(
+ previous_response_id="resp_previous", input="Explain.", text_format=ShortAnswer, parse_as=DetailedAnswer
+ )
+ else:
+ response = await async_client.responses.parse(
+ previous_response_id="resp_previous", input="Explain.", text_format=ShortAnswer, parse_as=DetailedAnswer
+ )
+
+ assert_type(response.output_parsed, DetailedAnswer | None)
+ assert response.output_parsed == DetailedAnswer(answer="yes", evidence="the receipt")
+ body = json.loads(route.calls.last.request.content)
+ assert body["text"]["format"] == type_to_text_format_param(ShortAnswer)
+ assert body["previous_response_id"] == "resp_previous"
+ assert "parse_as" not in body
+
+
+@pytest.mark.respx2(base_url=base_url)
+@pytest.mark.parametrize("client", [False], indirect=True)
+def test_parse_as_does_not_send_a_top_level_schema(client: OpenAI, respx2_mock: MockRouter) -> None:
+ update = _detailed_update()
+ text = '{"answer":"yes","evidence":"the receipt"}'
+ route = respx2_mock.post("/responses").respond(
+ json={"output": [{"type": "message", "content": [{"type": "output_text", "text": text}]}]}
+ )
+ response = client.responses.parse(input=[update], parse_as=DetailedAnswer)
+
+ assert response.output_parsed == DetailedAnswer(answer="yes", evidence="the receipt")
+ assert json.loads(route.calls.last.request.content) == {"input": [update]}
+
+
+@pytest.mark.respx2(base_url=base_url)
+@pytest.mark.parametrize("client", [False], indirect=True)
+def test_visible_format_updates(client: OpenAI, respx2_mock: MockRouter) -> None:
+ detailed = _detailed_update()
+ reasoning: ResponseConfigurationUpdateItemParamParam = {
+ "type": "configuration_update",
+ "reasoning": {"effort": "high"},
+ }
+ reset: ResponseConfigurationUpdateItemParamParam = {
+ "type": "configuration_update",
+ "text": {"format": {"type": "text"}},
+ }
+ route = respx2_mock.post("/responses").respond(
+ json={"output": [{"type": "message", "content": [{"type": "output_text", "text": "plain text"}]}]}
+ )
+ with pytest.raises(TypeError, match="Pass parse_as"):
+ client.responses.parse(text_format=ShortAnswer, input=[detailed, reasoning])
+ assert not route.called
+
+ response = client.responses.parse(text_format=ShortAnswer, input=[detailed, reset, reasoning])
+ assert response.output_text == "plain text"
+ assert response.output_parsed is None
+
+ with pytest.raises(TypeError, match="plain-text format update"):
+ client.responses.parse(text_format=ShortAnswer, parse_as=DetailedAnswer, input=[reset])
+ assert route.call_count == 1
+
+ response_text = client.responses.parse(previous_response_id="resp_reset", text_format=ShortAnswer, parse_as=None)
+ assert_type(response_text.output_parsed, None)
+ assert response_text.output_parsed is None
+
+
+@pytest.mark.respx2(base_url=base_url)
+def test_list_input_items_preserves_format_updates(client: OpenAI, respx2_mock: MockRouter) -> None:
+ items = [
+ {"id": "cnfu_schema", **_detailed_update()},
+ {
+ "id": "cnfu_reset",
+ "type": "configuration_update",
+ "reasoning": {"effort": "high"},
+ "text": {"format": {"type": "text"}},
+ },
+ {"id": "cnfu_reason", "type": "configuration_update", "reasoning": {"effort": "low"}},
+ ]
+ respx2_mock.get("/responses/resp_test/input_items").respond(
+ json={"object": "list", "data": items, "first_id": "cnfu_schema", "last_id": "cnfu_reason", "has_more": False}
+ )
+
+ detailed, reset, reasoning = client.responses.input_items.list("resp_test").data
+ assert isinstance(detailed, ResponseConfigurationUpdateItem) and detailed.text is not None
+ assert detailed.text.format.type == "json_schema"
+ assert detailed.text.format.name == "DetailedAnswer"
+ assert isinstance(reset, ResponseConfigurationUpdateItem) and reset.text is not None
+ assert reset.text.format.type == "text"
+ assert isinstance(reasoning, ResponseConfigurationUpdateItem) and reasoning.text is None
+ assert [item.to_dict() for item in (detailed, reset, reasoning)] == items
diff --git a/tests/lib/test_responses_tool_runner.py b/tests/lib/test_responses_tool_runner.py
index 336d2b66e4..d1593f26ef 100644
--- a/tests/lib/test_responses_tool_runner.py
+++ b/tests/lib/test_responses_tool_runner.py
@@ -2022,6 +2022,34 @@ async def test_replay_reasoning_requires_native_fields(lane: str, mode: str, fie
assert requests == []
+@pytest.mark.parametrize("lane", ["sync", "async"])
+@pytest.mark.parametrize("mode", ["ledger", "budget"])
+async def test_replay_preserves_configuration_updates(lane: str, mode: str) -> None:
+ history: ResponseInputParam = [
+ {
+ "type": "configuration_update",
+ "text": {"format": {"type": "json_schema", "name": "answer", "schema": {"type": "object"}}},
+ },
+ {"role": "user", "content": "Continue"},
+ {"type": "configuration_update", "text": {"format": {"type": "text"}}},
+ ]
+ requests: list[dict[str, Any]] = []
+ records: list[ResponsesToolCheckpoint] = []
+ await run(
+ lane,
+ None,
+ [resp([fn("record", "one", text="once")], "r"), FINAL],
+ requests,
+ params={**PARAMS, "input": history},
+ save=records.append if mode == "ledger" else None,
+ context_budget=ContextBudget(max_input_bytes=10000, max_input_images=0) if mode == "budget" else None,
+ )
+ assert requests[0]["input"] == history
+ assert requests[1]["input"][: len(history)] == history
+ if mode == "ledger":
+ assert records[0]["history"] == history
+
+
@pytest.mark.parametrize("lane", ["sync", "async"])
async def test_replay_reasoning_preserves_native_fields(lane: str) -> None:
reasoning = {