mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
fix(codex): keep output schema fields named like schema keywords (#5684)
The Codex output schema normalizer walked every key as if it held a schema, so a field named "properties" (or a literal default, enum, or const value shaped like an object schema) was rewritten with invented required and additionalProperties entries. Codex then rejected the turn with invalid_json_schema. Walk only the keywords that hold schemas. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2373,6 +2373,71 @@ describe("Codex app-server provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps an output field named properties as a field", () => {
|
||||
const input = {
|
||||
type: "object",
|
||||
properties: { properties: { type: "string" } },
|
||||
required: ["properties"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
expect(normalizeCodexOutputSchema(input)).toEqual(input);
|
||||
});
|
||||
|
||||
test("normalizes schemas under keywords and leaves literal values alone", () => {
|
||||
const literal = { type: "object", properties: { type: "string" } };
|
||||
const input = {
|
||||
type: "object",
|
||||
properties: {
|
||||
result: { $ref: "#/$defs/result" },
|
||||
choice: { anyOf: [{ type: "object", properties: { id: { type: "string" } } }] },
|
||||
},
|
||||
$defs: {
|
||||
properties: { type: "string" },
|
||||
result: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
default: literal,
|
||||
examples: [literal],
|
||||
enum: [literal],
|
||||
const: literal,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeCodexOutputSchema(input)).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
result: { $ref: "#/$defs/result" },
|
||||
choice: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ["result", "choice"],
|
||||
additionalProperties: false,
|
||||
$defs: {
|
||||
properties: { type: "string" },
|
||||
result: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
default: literal,
|
||||
examples: [literal],
|
||||
enum: [literal],
|
||||
const: literal,
|
||||
required: ["value"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("passes a normalized output schema to turn/start", async () => {
|
||||
const session = createSession();
|
||||
const request = vi.fn(async (method: string) => {
|
||||
|
||||
@@ -365,19 +365,67 @@ function isObjectSchemaNode(schema: Record<string, unknown>): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCodexOutputSchemaNode(schema: unknown, schemaPath: string): unknown {
|
||||
// Keywords whose value maps names to schemas.
|
||||
const SCHEMA_MAP_KEYWORDS = [
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
] as const;
|
||||
|
||||
// Keywords whose value is a schema or an array of schemas.
|
||||
const SUBSCHEMA_KEYWORDS = [
|
||||
"items",
|
||||
"prefixItems",
|
||||
"additionalItems",
|
||||
"contains",
|
||||
"additionalProperties",
|
||||
"unevaluatedProperties",
|
||||
"unevaluatedItems",
|
||||
"propertyNames",
|
||||
"allOf",
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
"not",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
] as const;
|
||||
|
||||
function normalizeCodexOutputSubschema(schema: unknown, schemaPath: string): unknown {
|
||||
if (Array.isArray(schema)) {
|
||||
return schema.map((entry, index) =>
|
||||
normalizeCodexOutputSchemaNode(entry, `${schemaPath}[${index}]`),
|
||||
);
|
||||
}
|
||||
return normalizeCodexOutputSchemaNode(schema, schemaPath);
|
||||
}
|
||||
|
||||
function normalizeCodexOutputSchemaNode(schema: unknown, schemaPath: string): unknown {
|
||||
if (!isSchemaRecord(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
normalized[key] = normalizeCodexOutputSchemaNode(value, `${schemaPath}.${key}`);
|
||||
const normalized: Record<string, unknown> = { ...schema };
|
||||
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
||||
const schemas = schema[keyword];
|
||||
if (isSchemaRecord(schemas)) {
|
||||
normalized[keyword] = Object.fromEntries(
|
||||
Object.entries(schemas).map(([name, child]) => [
|
||||
name,
|
||||
normalizeCodexOutputSchemaNode(child, `${schemaPath}.${keyword}.${name}`),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const keyword of SUBSCHEMA_KEYWORDS) {
|
||||
if (keyword in schema) {
|
||||
normalized[keyword] = normalizeCodexOutputSubschema(
|
||||
schema[keyword],
|
||||
`${schemaPath}.${keyword}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isObjectSchemaNode(normalized)) {
|
||||
|
||||
Reference in New Issue
Block a user