fix(confidence-engine): handle propertyless openai object schemas
This commit is contained in:
@@ -36,7 +36,8 @@ If YES, the next live experiment is one timed/costed OpenAI UI investigation mea
|
||||
## OpenAI alternate structured output
|
||||
|
||||
- The OpenAI provider now honors a caller-supplied structured-output schema, applying its existing strict-schema transport projection; absent an alternate schema, initial reconstruction retains its existing strict schema and transport normalization.
|
||||
- This deterministically fixes the focused-deconstruction 502 root cause: its six-field schema now reaches OpenAI rather than being replaced by the initial-reconstruction schema. Zero live calls occurred.
|
||||
- The next live run exposed incomplete recursive strict projection: OpenAI rejected `relationships.items` because it lacked `additionalProperties: false`. The projector now recognizes every `type: "object"` node, including property-less objects in array items, and recursively enforces strict object schemas while preserving initial-reconstruction optionality/nullability behavior.
|
||||
- This deterministically fixes the focused-deconstruction transport failures. Provider and focused-route suites pass with zero live calls.
|
||||
- Next boundary: retry one fresh real Playwright-driven OpenAI/Terra investigation; do not reuse the failed focused-deconstruction attempt.
|
||||
|
||||
## Repository checkpoint
|
||||
|
||||
+19
-6
@@ -112,7 +112,7 @@ const openAIReconstructionJsonSchema = createOpenAIStrictSchema(
|
||||
/** @internal OpenAI Structured Outputs requires every object property. */
|
||||
export function createOpenAIStrictSchema(schema, zodSchema = reconstructionV2Schema) {
|
||||
const projected = structuredClone(schema);
|
||||
projectOpenAIStrictSchema(projected, zodSchema, projected);
|
||||
projectOpenAIStrictSchema(projected, zodSchema, projected, new WeakSet());
|
||||
return projected;
|
||||
}
|
||||
|
||||
@@ -157,10 +157,12 @@ function zodAcceptsNull(schema) {
|
||||
return schema?.isNullable?.() === true;
|
||||
}
|
||||
|
||||
function projectOpenAIStrictSchema(schema, zodSchema, rootSchema) {
|
||||
function projectOpenAIStrictSchema(schema, zodSchema, rootSchema, visited) {
|
||||
const resolved = resolveSchema(schema, rootSchema);
|
||||
if (!resolved || typeof resolved !== "object" || visited.has(resolved)) return;
|
||||
visited.add(resolved);
|
||||
const shape = zodObjectShape(zodSchema);
|
||||
if (resolved?.properties) {
|
||||
if (resolved?.type === "object" || resolved?.properties) {
|
||||
if (shape) {
|
||||
for (const [key, property] of Object.entries(resolved.properties)) {
|
||||
const propertyZodSchema = shape[key];
|
||||
@@ -169,17 +171,28 @@ function projectOpenAIStrictSchema(schema, zodSchema, rootSchema) {
|
||||
}
|
||||
}
|
||||
}
|
||||
resolved.required = Object.keys(resolved.properties);
|
||||
if (resolved.properties) resolved.required = Object.keys(resolved.properties);
|
||||
resolved.additionalProperties = false;
|
||||
}
|
||||
|
||||
if (resolved?.items) {
|
||||
projectOpenAIStrictSchema(resolved.items, zodArrayItem(zodSchema), rootSchema);
|
||||
projectOpenAIStrictSchema(resolved.items, zodArrayItem(zodSchema), rootSchema, visited);
|
||||
}
|
||||
if (resolved?.properties) {
|
||||
for (const [key, property] of Object.entries(resolved.properties)) {
|
||||
projectOpenAIStrictSchema(property, shape?.[key], rootSchema);
|
||||
projectOpenAIStrictSchema(property, shape?.[key], rootSchema, visited);
|
||||
}
|
||||
}
|
||||
for (const branch of [
|
||||
...(resolved.anyOf ?? []),
|
||||
...(resolved.oneOf ?? []),
|
||||
...(resolved.allOf ?? []),
|
||||
]) {
|
||||
projectOpenAIStrictSchema(branch, null, rootSchema, visited);
|
||||
}
|
||||
for (const definition of Object.values(resolved.$defs ?? resolved.definitions ?? {})) {
|
||||
projectOpenAIStrictSchema(definition, null, rootSchema, visited);
|
||||
}
|
||||
}
|
||||
|
||||
function schemaAllowsNull(schema, rootSchema) {
|
||||
|
||||
@@ -220,6 +220,7 @@ describe("OpenAI reconstruction provider experiment seam", () => {
|
||||
const resolved = resolveLocalRef(schema, root);
|
||||
if (resolved?.properties) {
|
||||
expect(resolved.required).toEqual(Object.keys(resolved.properties));
|
||||
expect(resolved.additionalProperties).toBe(false);
|
||||
Object.values(resolved.properties).forEach((property) => assertAllPropertiesRequired(property, root));
|
||||
}
|
||||
(resolved?.anyOf ?? []).forEach((branch) => assertAllPropertiesRequired(branch, root));
|
||||
@@ -337,7 +338,9 @@ describe("OpenAI reconstruction provider experiment seam", () => {
|
||||
"possibleFollowUpQuestions",
|
||||
]);
|
||||
expect(Object.keys(schema.properties)).toEqual(schema.required);
|
||||
expect(schema.additionalProperties).toBe(false);
|
||||
expect(schema.properties).toHaveProperty("targetNodeId");
|
||||
expect(schema.properties.relationships.items.additionalProperties).toBe(false);
|
||||
expect(schema.properties).not.toHaveProperty("reconstruction");
|
||||
expect(schema.properties).not.toHaveProperty("inputClassification");
|
||||
expect(result.response).toEqual({
|
||||
@@ -350,6 +353,46 @@ describe("OpenAI reconstruction provider experiment seam", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("recursively projects arbitrary nested object schemas as strict", () => {
|
||||
const alternateSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
groups: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
details: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const projected = createOpenAIStrictSchema(alternateSchema, null);
|
||||
|
||||
expect(projected.additionalProperties).toBe(false);
|
||||
expect(projected.properties.groups.items.additionalProperties).toBe(false);
|
||||
expect(projected.properties.groups.items.properties.details.additionalProperties).toBe(false);
|
||||
});
|
||||
|
||||
it("projects property-less objects nested in arrays as strict", () => {
|
||||
const projected = createOpenAIStrictSchema({
|
||||
type: "object",
|
||||
properties: {
|
||||
relationships: { type: "array", items: { type: "object" } },
|
||||
},
|
||||
}, null);
|
||||
|
||||
expect(projected.properties.relationships.items).toEqual({
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("extracts ordered output_text parts from raw Responses output", async () => {
|
||||
const provider = createOpenAIReconstructionProvider({
|
||||
apiKey: "test-key",
|
||||
|
||||
Reference in New Issue
Block a user