Skip to content

Commit c2b1757

Browse files
feat(api): support response format updates and local output types
Castiron-Internal-PR: openai/openai-python-internal#365 Castiron-Source-SHA: 76f8408a2d73fd095ef2e6e849072c6033eb93ba Castiron-Public-Base-SHA: 10f9dd0
1 parent 10f9dd0 commit c2b1757

16 files changed

Lines changed: 671 additions & 72 deletions

‎.castiron.stats.yml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
schema_version: 1
2-
generation_id: edefdf16-46c7-4f93-94a6-10028a087b3e
3-
openapi_spec_hash: 0c25fc15ac5b26e2054196d579cc67a0
4-
openapi_transformed_spec_hash: 651cea16581547a33af56bd631894a03
2+
generation_id: 5dfacb1b-1abf-4d69-9083-e6586471a6f6
3+
openapi_spec_hash: 53e44788ffb3382947f27b9cf1d14cfe
4+
openapi_transformed_spec_hash: 1385724889e077e80e65b0e8da123eca
55
config_hash: 8a684aec06907ff8ced7809753e16225
6-
codegen_sha: a1dd47dd01ce07be1ecbc39e2f66f2156d89c6f3
7-
codegen_hash: 2c7827fd0ccd38edcd80a97839ae5c10cf89f8da20ea468d4b20c41468607b81
6+
codegen_sha: 11cd26ec903a4168e95565cd46fb7f33f657afca
7+
codegen_hash: 5a9715f0fcf2bbdc00a4d9ad8b7c749a51e4d0531ee776d1ae7707881e0adf2a

‎api_reference/openapi.transformed.yml‎

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64749,8 +64749,8 @@ components:
6474964749
ResponseConfigurationUpdate:
6475064750
type: object
6475164751
description: |
64752-
A configuration update that applies to subsequent responses until it is
64753-
replaced by another configuration update.
64752+
A configuration update for subsequent responses. Each setting stays in effect
64753+
until a later update replaces that setting.
6475464754
properties:
6475564755
id:
6475664756
type: string
@@ -64772,15 +64772,27 @@ components:
6477264772
description: |
6477364773
The reasoning effort used for subsequent responses until another
6477464774
configuration update replaces it.
64775+
text:
64776+
type: object
64777+
description: The text output configuration for subsequent responses.
64778+
properties:
64779+
format:
64780+
description: Use a JSON schema for structured output or text to return to plain text.
64781+
discriminator:
64782+
propertyName: type
64783+
anyOf:
64784+
- $ref: '#/components/schemas/ResponseFormatText'
64785+
- $ref: '#/components/schemas/TextResponseFormatJsonSchema'
64786+
required:
64787+
- format
6477564788
required:
6477664789
- id
6477764790
- type
6477864791
ResponseConfigurationUpdateItemParam:
6477964792
type: object
6478064793
description: |
64781-
An update to the conversation's response configuration. The configuration
64782-
remains in effect for subsequent responses until it is replaced by another
64783-
configuration update.
64794+
An update to the conversation's response configuration. Supply reasoning,
64795+
text, or both. Each setting stays in effect until a later update replaces it.
6478464796
properties:
6478564797
id:
6478664798
anyOf:
@@ -64804,6 +64816,19 @@ components:
6480464816
description: |
6480564817
The reasoning effort to use for subsequent responses until another
6480664818
configuration update replaces it.
64819+
text:
64820+
type: object
64821+
description: The text output configuration for subsequent responses.
64822+
properties:
64823+
format:
64824+
description: Use a JSON schema for structured output or text to return to plain text.
64825+
discriminator:
64826+
propertyName: type
64827+
anyOf:
64828+
- $ref: '#/components/schemas/ResponseFormatText'
64829+
- $ref: '#/components/schemas/TextResponseFormatJsonSchema'
64830+
required:
64831+
- format
6480764832
required:
6480864833
- type
6480964834
ResponseContentPartAddedEvent:
@@ -70390,13 +70415,13 @@ components:
7039070415
Setting to `{ "type": "json_object" }` enables the older JSON mode, which
7039170416
ensures the message the model generates is valid JSON. Using `json_schema`
7039270417
is preferred for models that support it.
70418+
discriminator:
70419+
propertyName: type
7039370420
anyOf:
7039470421
- $ref: '#/components/schemas/ResponseFormatText'
7039570422
- $ref: '#/components/schemas/TextResponseFormatJsonSchema'
7039670423
x-openai-ruby-json-schema-input: true
7039770424
- $ref: '#/components/schemas/ResponseFormatJsonObject'
70398-
discriminator:
70399-
propertyName: type
7040070425
TextResponseFormatJsonSchema:
7040170426
x-openai-structured-output: json_schema_default_strict
7040270427
type: object
@@ -98392,12 +98417,12 @@ components:
9839298417
Setting to `{ "type": "json_object" }` enables the older JSON mode, which
9839398418
ensures the message the model generates is valid JSON. Using `json_schema`
9839498419
is preferred for models that support it.
98420+
discriminator:
98421+
propertyName: type
9839598422
anyOf:
9839698423
- $ref: '#/components/schemas/BetaResponseFormatText'
9839798424
- $ref: '#/components/schemas/BetaTextResponseFormatJsonSchema'
9839898425
- $ref: '#/components/schemas/BetaResponseFormatJsonObject'
98399-
discriminator:
98400-
propertyName: type
9840198426
BetaResponseFormatJsonObject:
9840298427
type: object
9840398428
title: JSON object
@@ -101460,9 +101485,8 @@ components:
101460101485
BetaResponseConfigurationUpdateItemParam:
101461101486
type: object
101462101487
description: |
101463-
An update to the conversation's response configuration. The configuration
101464-
remains in effect for subsequent responses until it is replaced by another
101465-
configuration update.
101488+
An update to the conversation's response configuration. Supply reasoning,
101489+
text, or both. Each setting stays in effect until a later update replaces it.
101466101490
properties:
101467101491
agent:
101468101492
anyOf:
@@ -101491,6 +101515,19 @@ components:
101491101515
description: |
101492101516
The reasoning effort to use for subsequent responses until another
101493101517
configuration update replaces it.
101518+
text:
101519+
type: object
101520+
description: The text output configuration for subsequent responses.
101521+
properties:
101522+
format:
101523+
description: Use a JSON schema for structured output or text to return to plain text.
101524+
discriminator:
101525+
propertyName: type
101526+
anyOf:
101527+
- $ref: '#/components/schemas/BetaResponseFormatText'
101528+
- $ref: '#/components/schemas/BetaTextResponseFormatJsonSchema'
101529+
required:
101530+
- format
101494101531
required:
101495101532
- type
101496101533
BetaAdditionalToolsItemParam:
@@ -104217,8 +104254,8 @@ components:
104217104254
BetaResponseConfigurationUpdate:
104218104255
type: object
104219104256
description: |
104220-
A configuration update that applies to subsequent responses until it is
104221-
replaced by another configuration update.
104257+
A configuration update for subsequent responses. Each setting stays in effect
104258+
until a later update replaces that setting.
104222104259
properties:
104223104260
agent:
104224104261
$ref: '#/components/schemas/BetaAgentTag'
@@ -104243,6 +104280,19 @@ components:
104243104280
description: |
104244104281
The reasoning effort used for subsequent responses until another
104245104282
configuration update replaces it.
104283+
text:
104284+
type: object
104285+
description: The text output configuration for subsequent responses.
104286+
properties:
104287+
format:
104288+
description: Use a JSON schema for structured output or text to return to plain text.
104289+
discriminator:
104290+
propertyName: type
104291+
anyOf:
104292+
- $ref: '#/components/schemas/BetaResponseFormatText'
104293+
- $ref: '#/components/schemas/BetaTextResponseFormatJsonSchema'
104294+
required:
104295+
- format
104246104296
required:
104247104297
- id
104248104298
- type

‎helpers.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,41 @@ output into a Pydantic model. The same parsing rules apply to
6363
Use `output_parsed` for the structured result and retain `output` when replaying
6464
messages so their phases are preserved.
6565

66+
When the output schema changes during a conversation, pass `parse_as` to
67+
`client.responses.parse()` to select the local output type. For example, if a
68+
previous turn switched to `DetailedAnswer`, continue with this call.
69+
70+
```py
71+
response = client.responses.parse(
72+
model=model,
73+
previous_response_id=previous.id,
74+
input="Explain the evidence.",
75+
parse_as=DetailedAnswer,
76+
)
77+
```
78+
79+
The SDK parses the answer as `DetailedAnswer`. The request inherits its output
80+
format from the previous turn. If you also pass `text_format=ShortAnswer`,
81+
the request sends that class's schema as the top-level format. Pass
82+
`parse_as=None` to read unparsed text when the previous turn reset the format.
83+
You can also pass a format update directly in `input`. For example, given a
84+
JSON schema format named `detailed_format`:
85+
86+
```py
87+
response = client.responses.parse(
88+
model=model,
89+
input=[
90+
{"type": "configuration_update", "text": {"format": detailed_format}},
91+
{"role": "user", "content": "Explain the evidence."},
92+
],
93+
parse_as=DetailedAnswer,
94+
)
95+
```
96+
97+
To switch back to plain text, send `{"type": "text"}` as the update's format.
98+
A reasoning-only update keeps the active format. Retrieved configuration items
99+
include `item.text.format` when the input supplied it.
100+
66101
## Auto-parsing function tool calls
67102

68103
The `.parse()` method will also automatically parse `function` tool calls if:

‎scripts/castiron/generated-files.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@
619619
},
620620
"src/openai/resources/responses/api.md": {
621621
"mode": "100644",
622-
"sha256": "e7815b1278aa2780bb885ac5a249fe00859dff67d3899b708d25d2968e734785"
622+
"sha256": "b635f37005fb7126cc69a7b69e3f14a9df135d9523271cc4407b1ffcb9bd5c8c"
623623
},
624624
"src/openai/resources/responses/input_items.py": {
625625
"mode": "100644",
@@ -631,7 +631,7 @@
631631
},
632632
"src/openai/resources/responses/responses.py": {
633633
"mode": "100644",
634-
"sha256": "b9a2ef1b61d33450318babc49e2b25f604628983804e7ebf4291f8802f372aee"
634+
"sha256": "e74696203a015c8a87a6e1117d5e73449a1dd4b591c57d4c68d1cfda2641934a"
635635
},
636636
"src/openai/resources/safety/__init__.py": {
637637
"mode": "100644",
@@ -2523,15 +2523,15 @@
25232523
},
25242524
"src/openai/types/beta/beta_response_configuration_update_item.py": {
25252525
"mode": "100644",
2526-
"sha256": "4bd97e32e710edbd688bf8c6eac0dd8f24d1d0a64da60efda522ffefa0790046"
2526+
"sha256": "eebab46ab26a7650a94cf353d3b98c86fc12378284a9431cd4379808515ba6b0"
25272527
},
25282528
"src/openai/types/beta/beta_response_configuration_update_item_param.py": {
25292529
"mode": "100644",
2530-
"sha256": "eb06f107b27ef1fa3a9266bfaf8921b68384faf7c0dd0d300e554767bb7aed30"
2530+
"sha256": "c10511a06c2bd67530ce2f6ab2e94b63e49602e4e29df8cfb64e4bb73359e3a9"
25312531
},
25322532
"src/openai/types/beta/beta_response_configuration_update_item_param_param.py": {
25332533
"mode": "100644",
2534-
"sha256": "3c9bcd2d7e730b102b0674424e56cbd35e6bed0664a58acc1830466eea28d2be"
2534+
"sha256": "269015eac1b4dd3a8bfbd1053218e64c9a2fd3cac4b50db961089f3813c6bbd0"
25352535
},
25362536
"src/openai/types/beta/beta_response_container_reference.py": {
25372537
"mode": "100644",
@@ -6167,15 +6167,15 @@
61676167
},
61686168
"src/openai/types/responses/response_configuration_update_item.py": {
61696169
"mode": "100644",
6170-
"sha256": "4d07aca0f5dbb6f87f1db31fc16dbba2e4a49d0670edbd46256907066605a703"
6170+
"sha256": "89a39d84322be94f3805145e1ca70bae0c2b0500f89102ea93918700ecc65a02"
61716171
},
61726172
"src/openai/types/responses/response_configuration_update_item_param.py": {
61736173
"mode": "100644",
6174-
"sha256": "c8eb1b18f489c4d820bc17c10e505e97a4da8059592e903882aae5ed3ca919ad"
6174+
"sha256": "fb7a2cb3b72d2a5dd73fc78ad6194bce51eb4e1b805d9ccc85c2725e3c0bab3f"
61756175
},
61766176
"src/openai/types/responses/response_configuration_update_item_param_param.py": {
61776177
"mode": "100644",
6178-
"sha256": "8e177dbfa309934a8aedbc5bd278b48bddfa3365a42cfb661ead8677aa924a9b"
6178+
"sha256": "d7e43648f155a90a35d2f57d31e8f8481c463ada28805dd810d17db00304ce23"
61796179
},
61806180
"src/openai/types/responses/response_container_reference.py": {
61816181
"mode": "100644",

‎src/openai/lib/_response_resources.py‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import httpx2
99

1010
from openai._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit
11-
from openai._utils import is_given, maybe_transform
11+
from openai._utils import is_dict, is_list, is_given, maybe_transform
1212
from openai._streaming import Stream
1313
from openai.lib._tools import PydanticFunctionTool, ResponsesPydanticFunctionTool
1414
from openai._base_client import make_request_options
@@ -36,10 +36,41 @@
3636
from openai.resources.responses.responses import Responses, AsyncResponses
3737

3838

39+
def _output_text_parser(
40+
*,
41+
input: object,
42+
text: ResponseTextConfigParam | Omit,
43+
text_format: type[TextFormatT] | Omit,
44+
parse_as: type[TextFormatT] | None | Omit,
45+
) -> type[TextFormatT] | Omit:
46+
if parse_as is None:
47+
return omit
48+
49+
if is_list(input):
50+
for item in reversed(input):
51+
if not is_dict(item) or item.get("type") != "configuration_update":
52+
continue
53+
update = item.get("text")
54+
if not is_dict(update) or not is_dict(fmt := update.get("format")):
55+
continue
56+
if fmt.get("type") == "text":
57+
if is_given(parse_as):
58+
raise TypeError("Cannot use parse_as with a plain-text format update")
59+
return omit
60+
if is_given(text_format) and not is_given(parse_as) and (not is_given(text) or fmt != text.get("format")):
61+
raise TypeError(
62+
"Pass parse_as for the updated output schema, or parse_as=None to read plain output_text"
63+
)
64+
break
65+
66+
return parse_as if is_given(parse_as) else text_format
67+
68+
3969
def parse(
4070
resource: Responses,
4171
*,
4272
text_format: type[TextFormatT] | Omit = omit,
73+
parse_as: type[TextFormatT] | None | Omit = omit,
4374
access_programs: response_create_params.AccessPrograms | Omit = omit,
4475
background: Optional[bool] | Omit = omit,
4576
context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
@@ -89,12 +120,18 @@ def parse(
89120
text = copy(text)
90121
text["format"] = _type_to_text_format_param(text_format)
91122

123+
output_format = _output_text_parser(
124+
input=extra_body.get("input", input) if is_dict(extra_body) else input,
125+
text=text,
126+
text_format=text_format,
127+
parse_as=parse_as,
128+
)
92129
tools = _make_tools(tools)
93130

94131
def parser(raw_response: Response) -> ParsedResponse[TextFormatT]:
95132
return parse_response(
96133
input_tools=tools,
97-
text_format=text_format,
134+
text_format=output_format,
98135
response=raw_response,
99136
)
100137

@@ -156,6 +193,7 @@ async def async_parse(
156193
resource: AsyncResponses,
157194
*,
158195
text_format: type[TextFormatT] | Omit = omit,
196+
parse_as: type[TextFormatT] | None | Omit = omit,
159197
access_programs: response_create_params.AccessPrograms | Omit = omit,
160198
background: Optional[bool] | Omit = omit,
161199
context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit,
@@ -205,12 +243,18 @@ async def async_parse(
205243
text = copy(text)
206244
text["format"] = _type_to_text_format_param(text_format)
207245

246+
output_format = _output_text_parser(
247+
input=extra_body.get("input", input) if is_dict(extra_body) else input,
248+
text=text,
249+
text_format=text_format,
250+
parse_as=parse_as,
251+
)
208252
tools = _make_tools(tools)
209253

210254
def parser(raw_response: Response) -> ParsedResponse[TextFormatT]:
211255
return parse_response(
212256
input_tools=tools,
213-
text_format=text_format,
257+
text_format=output_format,
214258
response=raw_response,
215259
)
216260

‎src/openai/lib/responses_tool_runner.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from ..types.responses.tool_param import ToolParam, FunctionToolParam
5050
from .streaming.responses._events import ResponseStreamEvent
5151
from .streaming.responses._responses import ResponseStream, AsyncResponseStream
52+
from ..types.responses.response_input import ResponseInput
5253
from ..types.responses.response_input_param import ComputerCallOutput, FunctionCallOutput, ResponseInputParam
5354
from ..types.responses.response_output_item import McpCall, McpApprovalRequest, ResponseOutputItem
5455
from ..types.responses.response_create_params import ResponseCreateParamsStreaming, ResponseCreateParamsNonStreaming
@@ -506,8 +507,9 @@ def _history_calls(history: ResponseInputParam, tools: list[ToolParam], *, budge
506507
# Modern computer calls can omit the legacy checklist; validate these with
507508
# the same action parser as dispatch, without changing the saved input.
508509
try:
510+
# The input models alias wire fields such as "schema" that are reserved by Pydantic v1.
509511
validate_type(
510-
type_=ResponseInputParam,
512+
type_=ResponseInput,
511513
value=[item for item in history if item.get("type") not in ("computer_call", "computer_call_output")],
512514
)
513515
except pydantic.ValidationError:
@@ -522,6 +524,9 @@ def _history_calls(history: ResponseInputParam, tools: list[ToolParam], *, budge
522524
answered: set[str] = set()
523525
for index, item in enumerate(cast(list[dict[str, object]], history)):
524526
kind = item.get("type", "message")
527+
if kind == "configuration_update":
528+
# Already schema-validated above. There is no tool call to pair.
529+
continue
525530
if budget_only:
526531
if kind == "compaction_trigger":
527532
if index != len(history) - 1:

0 commit comments

Comments
 (0)