Merge remote-tracking branch 'origin/farm/101aa6fd/codex-search-preserve-sse-error'

This commit is contained in:
can1357
2026-08-01 08:33:48 +02:00
3 changed files with 74 additions and 6 deletions
+3
View File
@@ -18,6 +18,9 @@
### Fixed
- Fixed the live Ask dialog crashing the whole session with a `replaceTabs` TypeError when a question reached `AskDialogComponent` without a string `question` field; questions are now normalized at dialog entry, mirroring the transcript renderer ([#7211](https://github.com/can1357/oh-my-pi/issues/7211)).
### Fixed
- Fixed Codex web search collapsing backend errors to `Codex error (): Unknown error`; the SSE error parser now preserves the backend code and message from top-level, nested `error`, and `response.error` envelopes ([#7200](https://github.com/can1357/oh-my-pi/issues/7200)).
## [17.2.2] - 2026-07-31
@@ -407,6 +407,30 @@ function buildCodexHeaders(
return headers;
}
/**
* Extracts a backend error `{code, message}` from a Codex SSE event, tolerating
* the envelope shapes the ChatGPT Codex backend emits: top-level `{code,message}`,
* a nested `error` object, and a `response.error` object (as in `response.failed`).
* Without this the nested shapes collapse to `Codex error (): Unknown error`,
* discarding the backend diagnostic — e.g. a regional/model-snapshot rejection (#7200).
*/
function extractCodexSseError(rawEvent: Record<string, unknown>): { code: string; message: string } {
const candidates: unknown[] = [
rawEvent,
rawEvent.error,
(rawEvent.response as { error?: unknown } | undefined)?.error,
];
let code = "";
let message = "";
for (const candidate of candidates) {
if (!candidate || typeof candidate !== "object") continue;
const record = candidate as Record<string, unknown>;
if (!code && typeof record.code === "string" && record.code) code = record.code;
if (!message && typeof record.message === "string" && record.message) message = record.message;
}
return { code, message };
}
/**
* Calls the Codex Responses API with web search tool enabled.
* The caller provides the exact model id to send; retry / fallback policy
@@ -558,13 +582,14 @@ async function callCodexSearch(
}
}
} else if (eventType === "error") {
const code = (rawEvent as { code?: string }).code ?? "";
const message = (rawEvent as { message?: string }).message ?? "Unknown error";
throw new SearchProviderError("codex", `Codex error (${code}): ${message}`, 500);
const { code, message } = extractCodexSseError(rawEvent);
throw new SearchProviderError("codex", `Codex error (${code}): ${message || "Unknown error"}`, 500);
} else if (eventType === "response.failed") {
const resp = (rawEvent as { response?: { error?: { message?: string } } }).response;
const errorMessage = resp?.error?.message ?? "Request failed";
throw new SearchProviderError("codex", `Codex request failed: ${errorMessage}`, 500);
const { code, message } = extractCodexSseError(rawEvent);
const detail = code
? `Codex request failed (${code}): ${message || "Request failed"}`
: `Codex request failed: ${message || "Request failed"}`;
throw new SearchProviderError("codex", detail, 500);
}
}
@@ -752,4 +752,44 @@ describe("searchCodex model selection", () => {
expect(result.model).toBe("gpt-5.6-terra");
expect(result.sources).toEqual([{ title: "Example Article", url: "https://example.com/article" }]);
});
it("preserves a nested type:error code and message instead of Unknown error (#7200)", async () => {
delete process.env.PI_CODEX_WEB_SEARCH_MODEL;
const sse = [
`data: ${JSON.stringify({
type: "error",
error: {
code: "unsupported_region",
message: "web_search is not available for this workspace's data residency region.",
},
})}`,
"",
].join("\n");
const fetchMock: FetchImpl = () =>
Promise.resolve(new Response(sse, { status: 200, headers: { "Content-Type": "text/event-stream" } }));
await expect(searchCodex(makeSearchParams("nested error envelope", fetchMock))).rejects.toThrow(
"Codex error (unsupported_region): web_search is not available for this workspace's data residency region.",
);
});
it("preserves a structured response.failed error code and message (#7200)", async () => {
delete process.env.PI_CODEX_WEB_SEARCH_MODEL;
const sse = [
`data: ${JSON.stringify({
type: "response.failed",
response: {
id: "resp_failed",
error: { code: "model_snapshot_unavailable", message: "The requested model snapshot is unavailable." },
},
})}`,
"",
].join("\n");
const fetchMock: FetchImpl = () =>
Promise.resolve(new Response(sse, { status: 200, headers: { "Content-Type": "text/event-stream" } }));
await expect(searchCodex(makeSearchParams("structured failure", fetchMock))).rejects.toThrow(
"Codex request failed (model_snapshot_unavailable): The requested model snapshot is unavailable.",
);
});
});