@@ -10,6 +10,7 @@ use codex_protocol::config_types::Verbosity as VerbosityConfig;
10
10
use codex_protocol:: models:: ResponseItem ;
11
11
use futures:: Stream ;
12
12
use serde:: Serialize ;
13
+ use serde_json:: Value ;
13
14
use std:: borrow:: Cow ;
14
15
use std:: ops:: Deref ;
15
16
use std:: pin:: Pin ;
@@ -32,6 +33,9 @@ pub struct Prompt {
32
33
33
34
/// Optional override for the built-in BASE_INSTRUCTIONS.
34
35
pub base_instructions_override : Option < String > ,
36
+
37
+ /// Optional the output schema for the model's response.
38
+ pub output_schema : Option < Value > ,
35
39
}
36
40
37
41
impl Prompt {
@@ -90,14 +94,31 @@ pub(crate) struct Reasoning {
90
94
pub ( crate ) summary : Option < ReasoningSummaryConfig > ,
91
95
}
92
96
97
+ #[ derive( Debug , Serialize , Default , Clone ) ]
98
+ #[ serde( rename_all = "snake_case" ) ]
99
+ pub ( crate ) enum TextFormatType {
100
+ #[ default]
101
+ JsonSchema ,
102
+ }
103
+
104
+ #[ derive( Debug , Serialize , Default , Clone ) ]
105
+ pub ( crate ) struct TextFormat {
106
+ pub ( crate ) r#type : TextFormatType ,
107
+ pub ( crate ) strict : bool ,
108
+ pub ( crate ) schema : Value ,
109
+ pub ( crate ) name : String ,
110
+ }
111
+
93
112
/// Controls under the `text` field in the Responses API for GPT-5.
94
- #[ derive( Debug , Serialize , Default , Clone , Copy ) ]
113
+ #[ derive( Debug , Serialize , Default , Clone ) ]
95
114
pub ( crate ) struct TextControls {
96
115
#[ serde( skip_serializing_if = "Option::is_none" ) ]
97
116
pub ( crate ) verbosity : Option < OpenAiVerbosity > ,
117
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
118
+ pub ( crate ) format : Option < TextFormat > ,
98
119
}
99
120
100
- #[ derive( Debug , Serialize , Default , Clone , Copy ) ]
121
+ #[ derive( Debug , Serialize , Default , Clone ) ]
101
122
#[ serde( rename_all = "lowercase" ) ]
102
123
pub ( crate ) enum OpenAiVerbosity {
103
124
Low ,
@@ -156,9 +177,20 @@ pub(crate) fn create_reasoning_param_for_request(
156
177
157
178
pub ( crate ) fn create_text_param_for_request (
158
179
verbosity : Option < VerbosityConfig > ,
180
+ output_schema : & Option < Value > ,
159
181
) -> Option < TextControls > {
160
- verbosity. map ( |v| TextControls {
161
- verbosity : Some ( v. into ( ) ) ,
182
+ if verbosity. is_none ( ) && output_schema. is_none ( ) {
183
+ return None ;
184
+ }
185
+
186
+ Some ( TextControls {
187
+ verbosity : verbosity. map ( std:: convert:: Into :: into) ,
188
+ format : output_schema. as_ref ( ) . map ( |schema| TextFormat {
189
+ r#type : TextFormatType :: JsonSchema ,
190
+ strict : true ,
191
+ schema : schema. clone ( ) ,
192
+ name : "codex_output_schema" . to_string ( ) ,
193
+ } ) ,
162
194
} )
163
195
}
164
196
@@ -255,6 +287,7 @@ mod tests {
255
287
prompt_cache_key : None ,
256
288
text : Some ( TextControls {
257
289
verbosity : Some ( OpenAiVerbosity :: Low ) ,
290
+ format : None ,
258
291
} ) ,
259
292
} ;
260
293
@@ -267,6 +300,52 @@ mod tests {
267
300
) ;
268
301
}
269
302
303
+ #[ test]
304
+ fn serializes_text_schema_with_strict_format ( ) {
305
+ let input: Vec < ResponseItem > = vec ! [ ] ;
306
+ let tools: Vec < serde_json:: Value > = vec ! [ ] ;
307
+ let schema = serde_json:: json!( {
308
+ "type" : "object" ,
309
+ "properties" : {
310
+ "answer" : { "type" : "string" }
311
+ } ,
312
+ "required" : [ "answer" ] ,
313
+ } ) ;
314
+ let text_controls =
315
+ create_text_param_for_request ( None , & Some ( schema. clone ( ) ) ) . expect ( "text controls" ) ;
316
+
317
+ let req = ResponsesApiRequest {
318
+ model : "gpt-5" ,
319
+ instructions : "i" ,
320
+ input : & input,
321
+ tools : & tools,
322
+ tool_choice : "auto" ,
323
+ parallel_tool_calls : false ,
324
+ reasoning : None ,
325
+ store : false ,
326
+ stream : true ,
327
+ include : vec ! [ ] ,
328
+ prompt_cache_key : None ,
329
+ text : Some ( text_controls) ,
330
+ } ;
331
+
332
+ let v = serde_json:: to_value ( & req) . expect ( "json" ) ;
333
+ let text = v. get ( "text" ) . expect ( "text field" ) ;
334
+ assert ! ( text. get( "verbosity" ) . is_none( ) ) ;
335
+ let format = text. get ( "format" ) . expect ( "format field" ) ;
336
+
337
+ assert_eq ! (
338
+ format. get( "name" ) ,
339
+ Some ( & serde_json:: Value :: String ( "codex_output_schema" . into( ) ) )
340
+ ) ;
341
+ assert_eq ! (
342
+ format. get( "type" ) ,
343
+ Some ( & serde_json:: Value :: String ( "json_schema" . into( ) ) )
344
+ ) ;
345
+ assert_eq ! ( format. get( "strict" ) , Some ( & serde_json:: Value :: Bool ( true ) ) ) ;
346
+ assert_eq ! ( format. get( "schema" ) , Some ( & schema) ) ;
347
+ }
348
+
270
349
#[ test]
271
350
fn omits_text_when_not_set ( ) {
272
351
let input: Vec < ResponseItem > = vec ! [ ] ;
0 commit comments