fix(confidence-engine): honor openai alternate output schema
This commit is contained in:
@@ -33,6 +33,12 @@ If YES, the next live experiment is one timed/costed OpenAI UI investigation mea
|
||||
- Browser request contracts and client state remain unchanged. Deterministic coverage includes initial start, normal update, episode reconsideration, focused deconstruction, overview synthesis, and Current Understanding synthesis.
|
||||
- Zero live calls occurred. Next boundary: one real Playwright-driven Terra investigation measuring user-visible latency, actual LLM-call sequence, and OpenAI usage/cost.
|
||||
|
||||
## 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.
|
||||
- Next boundary: retry one fresh real Playwright-driven OpenAI/Terra investigation; do not reuse the failed focused-deconstruction attempt.
|
||||
|
||||
## Repository checkpoint
|
||||
|
||||
- **Branch:** `feature/initial-decomposition-v0.61`
|
||||
|
||||
+13
-6
@@ -160,22 +160,24 @@ function zodAcceptsNull(schema) {
|
||||
function projectOpenAIStrictSchema(schema, zodSchema, rootSchema) {
|
||||
const resolved = resolveSchema(schema, rootSchema);
|
||||
const shape = zodObjectShape(zodSchema);
|
||||
if (resolved?.properties && shape) {
|
||||
if (resolved?.properties) {
|
||||
if (shape) {
|
||||
for (const [key, property] of Object.entries(resolved.properties)) {
|
||||
const propertyZodSchema = shape[key];
|
||||
if (propertyZodSchema?.isOptional?.() && !schemaAllowsNull(property, rootSchema)) {
|
||||
resolved.properties[key] = { anyOf: [property, { type: "null" }] };
|
||||
}
|
||||
}
|
||||
}
|
||||
resolved.required = Object.keys(resolved.properties);
|
||||
}
|
||||
|
||||
if (resolved?.items) {
|
||||
projectOpenAIStrictSchema(resolved.items, zodArrayItem(zodSchema), rootSchema);
|
||||
}
|
||||
if (resolved?.properties && shape) {
|
||||
if (resolved?.properties) {
|
||||
for (const [key, property] of Object.entries(resolved.properties)) {
|
||||
projectOpenAIStrictSchema(property, shape[key], rootSchema);
|
||||
projectOpenAIStrictSchema(property, shape?.[key], rootSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,8 +460,11 @@ class OpenAIReconstructionProvider {
|
||||
this.fetchImpl = fetchImpl;
|
||||
}
|
||||
|
||||
async generateReconstruction(prompt, modelName = "gpt-5.6-terra") {
|
||||
async generateReconstruction(prompt, modelName = "gpt-5.6-terra", outputSchema) {
|
||||
if (!this.apiKey) throw new Error("OPENAI_API_KEY is not set");
|
||||
const structuredOutputSchema = outputSchema
|
||||
? createOpenAIStrictSchema(outputSchema, null)
|
||||
: openAIReconstructionJsonSchema;
|
||||
|
||||
const response = await this.fetchImpl("https://api.openai.com/v1/responses", {
|
||||
method: "POST",
|
||||
@@ -475,7 +480,7 @@ class OpenAIReconstructionProvider {
|
||||
type: "json_schema",
|
||||
name: "reconstruction",
|
||||
strict: true,
|
||||
schema: openAIReconstructionJsonSchema,
|
||||
schema: structuredOutputSchema,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -501,7 +506,9 @@ class OpenAIReconstructionProvider {
|
||||
}
|
||||
|
||||
return {
|
||||
response: normaliseOpenAITransportResponse(recoverJson(outputText)),
|
||||
response: outputSchema
|
||||
? recoverJson(outputText)
|
||||
: normaliseOpenAITransportResponse(recoverJson(outputText)),
|
||||
providerApiPath: "/v1/responses",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
reconstructionJsonSchema,
|
||||
} from "@/lib/llm/provider.js";
|
||||
import { reconstructionV2Schema } from "@/lib/reconstruction/schema.js";
|
||||
import { focusedDeconstructJsonSchema } from "@/lib/graph/focused-investigation.js";
|
||||
import { z } from "zod";
|
||||
|
||||
describe("OllamaLlmProvider chat capability detection", () => {
|
||||
@@ -303,6 +304,52 @@ describe("OpenAI reconstruction provider experiment seam", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("projects a caller-supplied focused schema for OpenAI structured output", async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
output_text: JSON.stringify({
|
||||
targetNodeId: "target",
|
||||
observations: [],
|
||||
uncertainties: [],
|
||||
assumptions: [],
|
||||
relationships: [],
|
||||
possibleFollowUpQuestions: [],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const provider = createOpenAIReconstructionProvider({ apiKey: "test-key", fetchImpl: fetchSpy });
|
||||
|
||||
const result = await provider.generateReconstruction(
|
||||
"focused prompt",
|
||||
"gpt-5.6-terra",
|
||||
focusedDeconstructJsonSchema,
|
||||
);
|
||||
const request = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
||||
const schema = request.text.format.schema;
|
||||
|
||||
expect(schema.required).toEqual([
|
||||
"targetNodeId",
|
||||
"observations",
|
||||
"uncertainties",
|
||||
"assumptions",
|
||||
"relationships",
|
||||
"possibleFollowUpQuestions",
|
||||
]);
|
||||
expect(Object.keys(schema.properties)).toEqual(schema.required);
|
||||
expect(schema.properties).toHaveProperty("targetNodeId");
|
||||
expect(schema.properties).not.toHaveProperty("reconstruction");
|
||||
expect(schema.properties).not.toHaveProperty("inputClassification");
|
||||
expect(result.response).toEqual({
|
||||
targetNodeId: "target",
|
||||
observations: [],
|
||||
uncertainties: [],
|
||||
assumptions: [],
|
||||
relationships: [],
|
||||
possibleFollowUpQuestions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("extracts ordered output_text parts from raw Responses output", async () => {
|
||||
const provider = createOpenAIReconstructionProvider({
|
||||
apiKey: "test-key",
|
||||
|
||||
Reference in New Issue
Block a user