fix(confidence-engine): extract raw openai response text

This commit is contained in:
2026-09-06 11:45:24 +01:00
parent 0bb2f01100
commit e8d401e506
3 changed files with 95 additions and 3 deletions
+5
View File
@@ -151,6 +151,11 @@
- An experiment-only reconstruction-only seam now returns after initial reconstruction and graph construction, before `determineGraphBackedQuestion`. Normal production/default `startCase()` behavior remains unchanged; explicit OpenAI helper selection guarantees one selected reconstruction provider call and zero downstream question-generation calls.
- Zero live calls occurred while implementing and verifying this seam. Next boundary: exactly one live Terra reconstruction with no retry.
## OpenAI Responses text extraction
- The first reconstruction-only Terra request reached inference successfully, but native-fetch parsing relied on SDK-only `output_text` and could not extract the raw response. The provider now reads documented `output[].content[].output_text` parts in response order while retaining the convenience-property path.
- No tool-call assumption was introduced; canonical schema, prompt, transport compatibility, and reasoning remain unchanged. Zero live calls occurred during this correction; next boundary is exactly one live Terra reconstruction with no retry and no Ollama call.
## Current product architecture
Three distinct routes, not a single page:
+31 -3
View File
@@ -69,6 +69,32 @@ function recoverJson(raw) {
throw new SyntaxError("Model output could not be parsed as JSON: " + result.slice(0, 300) + "...");
}
function extractOpenAIResponseText(data) {
if (typeof data.output_text === "string" && data.output_text.length > 0) {
return data.output_text;
}
const outputText = (data.output ?? []).flatMap((item) =>
item?.type === "message"
? (item.content ?? []).flatMap((part) =>
part?.type === "output_text" && typeof part.text === "string"
? [part.text]
: [],
)
: [],
);
if (outputText.length > 0) return outputText.join("");
const outputTypes = (data.output ?? []).map((item) => item?.type ?? "unknown");
const contentTypes = (data.output ?? []).flatMap((item) =>
(item?.content ?? []).map((part) => part?.type ?? "unknown"),
);
const refusal = contentTypes.includes("refusal");
throw new Error(
`OpenAI Responses API returned no output_text (output types: ${outputTypes.join(",") || "none"}; content types: ${contentTypes.join(",") || "none"}; refusal: ${refusal})`,
);
}
let _chatSupported = null;
const reconstructionJsonSchema = z.toJSONSchema(reconstructionV2Schema);
const openAIReconstructionJsonSchema = createOpenAIStrictSchema(
@@ -458,9 +484,11 @@ class OpenAIReconstructionProvider {
}
const data = await response.json();
const outputText = data.output_text;
if (typeof outputText !== "string") {
const error = new Error("OpenAI Responses API returned no output_text");
let outputText;
try {
outputText = extractOpenAIResponseText(data);
} catch (cause) {
const error = new Error(cause.message);
error.providerApiPath = "/v1/responses";
throw error;
}
+59
View File
@@ -239,6 +239,65 @@ describe("OpenAI reconstruction provider experiment seam", () => {
});
});
it("extracts ordered output_text parts from raw Responses output", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",
fetchImpl: vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
output: [
{ type: "reasoning", content: [] },
{
type: "message",
role: "assistant",
content: [
{ type: "output_text", text: '{"reconstruction":' },
{ type: "output_text", text: '"result"}' },
],
},
],
}),
}),
});
await expect(provider.generateReconstruction("prompt")).resolves.toEqual({
response: { reconstruction: "result" },
providerApiPath: "/v1/responses",
});
});
it("preserves output_text convenience responses", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",
fetchImpl: vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ output_text: '{"reconstruction":"result"}' }),
}),
});
await expect(provider.generateReconstruction("prompt")).resolves.toEqual({
response: { reconstruction: "result" },
providerApiPath: "/v1/responses",
});
});
it("fails safely when a raw Responses result has no output text", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",
fetchImpl: vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
output: [{ type: "message", content: [{ type: "refusal" }] }],
}),
}),
});
await expect(provider.generateReconstruction("prompt")).rejects.toMatchObject({
providerApiPath: "/v1/responses",
message: expect.stringContaining("refusal: true"),
});
});
it("surfaces Responses API failures without inventing reconstruction content", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",