fix(confidence-engine): extract raw openai response text
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user