@@ -43,12 +43,41 @@ func NewCodexProvider(binPath string) *CodexProvider {
4343// computation and file writing; the provider only needs the paths so it can
4444// point codex's native --output-schema / --output-last-message flags at them.
4545//
46+ // An empty schemaPath with a non-empty outputPath means "last-message only":
47+ // the runner determined the schema cannot be expressed in OpenAI strict mode
48+ // (codexSchemaStrictExpressible, schema.go), so --output-schema must not be
49+ // sent — the server would reject it with invalid_json_schema — but codex still
50+ // persists its final JSON answer to outputPath for local validation.
51+ //
4652// This implements the schemaAware interface the runner detects (see runner.go).
4753func (p * CodexProvider ) SetSchema (schemaPath , outputPath string ) {
4854 p .schemaPath = schemaPath
4955 p .outputPath = outputPath
5056}
5157
58+ // isOutputSchemaRejection reports whether CLI output carries the server-side
59+ // strict-schema validator's 400 for --output-schema. The error event embeds
60+ // `"code": "invalid_json_schema"` and `Invalid schema for response_format
61+ // 'codex_output_schema'` (both observed live on codex-cli 0.144.1).
62+ func isOutputSchemaRejection (output string ) bool {
63+ lower := strings .ToLower (output )
64+ return strings .Contains (lower , "invalid_json_schema" ) ||
65+ strings .Contains (lower , "invalid schema for response_format" )
66+ }
67+
68+ // withoutFlagValue returns cmd with one `flag value` pair removed.
69+ func withoutFlagValue (cmd []string , flag string ) []string {
70+ out := make ([]string , 0 , len (cmd ))
71+ for i := 0 ; i < len (cmd ); i ++ {
72+ if cmd [i ] == flag && i + 1 < len (cmd ) {
73+ i ++
74+ continue
75+ }
76+ out = append (out , cmd [i ])
77+ }
78+ return out
79+ }
80+
5281func (p * CodexProvider ) Execute (ctx context.Context , prompt string , options Options ) (* RawResult , error ) {
5382 // --skip-git-repo-check lets the harness run in arbitrary working dirs
5483 // (temp dirs, non-repo project roots); codex exec otherwise refuses to
@@ -90,14 +119,20 @@ func (p *CodexProvider) Execute(ctx context.Context, prompt string, options Opti
90119 }
91120
92121 // Native structured output: when the runner has set a schema, point codex at
93- // the strict schema file and the last-message output file (patch lines
94- // 176-178). codex writes its final message to the last-message file, which
95- // the runner reads back.
122+ // the strict schema file (patch lines 176-178). Kept independent of the
123+ // last-message flag below: the runner passes an empty schemaPath when the
124+ // schema is not strict-expressible (see SetSchema), and the reactive
125+ // fallback after execution needs the answer file even when the server
126+ // rejects the schema flag.
127+ usedOutputSchema := false
96128 if p .schemaPath != "" && fileExists (p .schemaPath ) {
97129 cmd = append (cmd , "--output-schema" , p .schemaPath )
98- if p .outputPath != "" {
99- cmd = append (cmd , "--output-last-message" , p .outputPath )
100- }
130+ usedOutputSchema = true
131+ }
132+ // codex writes its final message to the last-message file, which the
133+ // runner reads back.
134+ if p .outputPath != "" {
135+ cmd = append (cmd , "--output-last-message" , p .outputPath )
101136 }
102137
103138 env := make (map [string ]string )
@@ -117,6 +152,21 @@ func (p *CodexProvider) Execute(ctx context.Context, prompt string, options Opti
117152 // prompt from stdin, and delivering it there keeps large prompts off the
118153 // argv and out of process listings.
119154 cliResult , err := runCLI (ctx , cmd , env , cwd , options .timeout (), []byte (prompt ))
155+
156+ // Reactive fallback: if the server's strict-schema validator refused the
157+ // schema we sent (invalid_json_schema 400 — the validator's rules can
158+ // tighten upstream at any time), rerun once WITHOUT --output-schema. The
159+ // prompt suffix still pins the JSON contract and --output-last-message
160+ // still captures the final answer, so the runner's local validation takes
161+ // over exactly as in the not-strict-expressible path.
162+ if err == nil && usedOutputSchema && cliResult .ReturnCode != 0 &&
163+ isOutputSchemaRejection (cliResult .Stdout + cliResult .Stderr ) {
164+ retryCmd := withoutFlagValue (cmd , "--output-schema" )
165+ if retryResult , retryErr := runCLI (ctx , retryCmd , env , cwd , options .timeout (), []byte (prompt )); retryErr == nil {
166+ cliResult = retryResult
167+ }
168+ }
169+
120170 apiMS := int (time .Since (startAPI ).Milliseconds ())
121171
122172 if err != nil {
0 commit comments