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.
|
- 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.
|
- 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
|
## Current product architecture
|
||||||
|
|
||||||
Three distinct routes, not a single page:
|
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) + "...");
|
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;
|
let _chatSupported = null;
|
||||||
const reconstructionJsonSchema = z.toJSONSchema(reconstructionV2Schema);
|
const reconstructionJsonSchema = z.toJSONSchema(reconstructionV2Schema);
|
||||||
const openAIReconstructionJsonSchema = createOpenAIStrictSchema(
|
const openAIReconstructionJsonSchema = createOpenAIStrictSchema(
|
||||||
@@ -458,9 +484,11 @@ class OpenAIReconstructionProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const outputText = data.output_text;
|
let outputText;
|
||||||
if (typeof outputText !== "string") {
|
try {
|
||||||
const error = new Error("OpenAI Responses API returned no output_text");
|
outputText = extractOpenAIResponseText(data);
|
||||||
|
} catch (cause) {
|
||||||
|
const error = new Error(cause.message);
|
||||||
error.providerApiPath = "/v1/responses";
|
error.providerApiPath = "/v1/responses";
|
||||||
throw error;
|
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 () => {
|
it("surfaces Responses API failures without inventing reconstruction content", async () => {
|
||||||
const provider = createOpenAIReconstructionProvider({
|
const provider = createOpenAIReconstructionProvider({
|
||||||
apiKey: "test-key",
|
apiKey: "test-key",
|
||||||
|
|||||||
Reference in New Issue
Block a user