Skip to content

Commit 615d37a

Browse files
Brant Levinsoncursoragent
andcommitted
fix(codex): normalize output schemas for OpenAI Structured Outputs compliance
OpenAI's Structured Outputs API requires `additionalProperties: false` on every object node and `required` to list ALL property keys. Claude doesn't enforce either, so workflow authors writing provider-agnostic YAML typically omit both — causing 400 errors when switching to the Codex provider. Add `normalizeSchemaForOpenAI()` in the Codex provider that recursively walks the schema tree and fills in the missing constraints before passing to the SDK. This makes all existing workflows work with Codex without any YAML changes. Fixes the `invalid_json_schema` error on `output_format` nodes when using `provider: codex` in workflow definitions. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7bdf931 commit 615d37a

2 files changed

Lines changed: 53 additions & 3 deletions

File tree

packages/providers/src/codex/provider.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,13 @@ describe('CodexProvider', () => {
682682

683683
expect(mockRunStreamed).toHaveBeenCalledWith(
684684
'test prompt',
685-
expect.objectContaining({ outputSchema: schema })
685+
expect.objectContaining({
686+
outputSchema: {
687+
...schema,
688+
additionalProperties: false,
689+
required: ['summary'],
690+
},
691+
})
686692
);
687693
});
688694

packages/providers/src/codex/provider.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,50 @@ function extractUsageFromCodexEvent(event: TurnCompletedEvent): TokenUsage {
267267
};
268268
}
269269

270+
// ─── Schema Normalizer (OpenAI Structured Outputs compliance) ────────────
271+
272+
/**
273+
* Recursively normalize a JSON schema for OpenAI Structured Outputs compliance.
274+
*
275+
* OpenAI requires two things Claude doesn't:
276+
* 1. Every object must have `additionalProperties: false`.
277+
* 2. Every object must have a `required` array listing ALL property keys.
278+
*
279+
* Workflow authors writing provider-agnostic YAML typically omit both.
280+
*/
281+
function normalizeSchemaForOpenAI(schema: Record<string, unknown>): Record<string, unknown> {
282+
const out = { ...schema };
283+
284+
if (out.type === 'object') {
285+
if (!('additionalProperties' in out)) {
286+
out.additionalProperties = false;
287+
}
288+
if (typeof out.properties === 'object' && out.properties !== null) {
289+
const props = out.properties as Record<string, Record<string, unknown>>;
290+
const propKeys = Object.keys(props);
291+
292+
const existingRequired = Array.isArray(out.required) ? (out.required as string[]) : [];
293+
const missingRequired = propKeys.filter(k => !existingRequired.includes(k));
294+
if (missingRequired.length > 0) {
295+
out.required = [...existingRequired, ...missingRequired];
296+
}
297+
298+
const normalized: Record<string, Record<string, unknown>> = {};
299+
for (const [key, value] of Object.entries(props)) {
300+
normalized[key] =
301+
typeof value === 'object' && value !== null ? normalizeSchemaForOpenAI(value) : value;
302+
}
303+
out.properties = normalized;
304+
}
305+
}
306+
307+
if (out.type === 'array' && typeof out.items === 'object' && out.items !== null) {
308+
out.items = normalizeSchemaForOpenAI(out.items as Record<string, unknown>);
309+
}
310+
311+
return out;
312+
}
313+
270314
// ─── Turn Options Builder ────────────────────────────────────────────────
271315

272316
/**
@@ -282,10 +326,10 @@ function buildTurnOptions(requestOptions?: SendQueryOptions): {
282326
requestOptions?.outputFormat ?? requestOptions?.nodeConfig?.output_format
283327
);
284328
if (requestOptions?.outputFormat) {
285-
turnOptions.outputSchema = requestOptions.outputFormat.schema;
329+
turnOptions.outputSchema = normalizeSchemaForOpenAI(requestOptions.outputFormat.schema);
286330
}
287331
if (requestOptions?.nodeConfig?.output_format && !requestOptions?.outputFormat) {
288-
turnOptions.outputSchema = requestOptions.nodeConfig.output_format;
332+
turnOptions.outputSchema = normalizeSchemaForOpenAI(requestOptions.nodeConfig.output_format);
289333
}
290334
if (requestOptions?.abortSignal) {
291335
turnOptions.signal = requestOptions.abortSignal;

0 commit comments

Comments
 (0)