mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): process unterminated Codex SSE terminal events
Flush the SSE decoder at EOF and treat the residual buffer as a complete frame. fixes #9047
This commit is contained in:
parent
55adba4f24
commit
64eeb82a46
3 changed files with 37 additions and 2 deletions
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
- Removed the unnecessary Chord dependency from pi-ai by defining its exported `JsonValue` type directly.
|
||||
- Fixed GitHub Copilot Claude Fable 5 requests to use the Anthropic Messages adapter so selected reasoning levels are sent ([#8961](https://github.com/earendil-works/pi/issues/8961)).
|
||||
- Fixed OpenAI Codex SSE parsing to process terminal events that are not followed by a blank line ([#9047](https://github.com/earendil-works/pi/issues/9047)).
|
||||
|
||||
## [0.84.4] - 2026-08-28
|
||||
|
||||
|
|
|
|||
|
|
@ -779,8 +779,9 @@ async function* parseSSE(response: Response, signal?: AbortSignal): AsyncGenerat
|
|||
if (signal?.aborted) {
|
||||
throw new Error("Request was aborted");
|
||||
}
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
||||
// Treat EOF as terminating the residual SSE frame.
|
||||
if (done && buffer.trim()) buffer += "\n\n";
|
||||
|
||||
let idx = buffer.indexOf("\n\n");
|
||||
while (idx !== -1) {
|
||||
|
|
@ -806,6 +807,8 @@ async function* parseSSE(response: Response, signal?: AbortSignal): AsyncGenerat
|
|||
}
|
||||
idx = buffer.indexOf("\n\n");
|
||||
}
|
||||
|
||||
if (done) break;
|
||||
}
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
|
|
|
|||
|
|
@ -209,6 +209,37 @@ describe("openai-codex streaming", () => {
|
|||
expect(sawDone).toBe(true);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/earendil-works/pi/issues/9047
|
||||
it("processes a terminal SSE event without a trailing blank line", async () => {
|
||||
const token = mockToken();
|
||||
const sse = buildSSEPayload({ status: "completed" }).trimEnd();
|
||||
const model: Model<"openai-codex-responses"> = {
|
||||
id: "gpt-5.1-codex",
|
||||
name: "GPT-5.1 Codex",
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400000,
|
||||
maxTokens: 128000,
|
||||
};
|
||||
const context: Context = {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
||||
};
|
||||
const resultStream = streamOpenAICodexResponses(model, context, {
|
||||
apiKey: token,
|
||||
transport: "sse",
|
||||
fetch: async () => new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
|
||||
});
|
||||
const result = await resultStream.result();
|
||||
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello");
|
||||
});
|
||||
|
||||
it("completes after response.completed even when the SSE body stays open", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
||||
process.env.PI_CODING_AGENT_DIR = tempDir;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue