SIGN IN SIGN UP

fix(openai-codex): complete prompts over the streaming transport (#1243)

* fix(openai-codex): complete prompts over the streaming transport

The Codex subscription endpoint only accepts streaming requests, so
`completePrompt` sending `stream: false` was rejected outright with
HTTP 400 `Stream must be set to true`. That made commit-message
generation, prompt enhancement and condensing unusable on Codex.

Rather than issue its own request, `completePrompt` now runs the
existing streaming path and joins the text chunks. That inherits the
OAuth refresh-and-retry, the SDK-then-SSE fallback and the Luna body
instead of duplicating a second, subtly different request builder.
Reasoning chunks are deliberately dropped: a commit message is written
straight into the Source Control box.

A caller's abort signal also never reached the wire, since both
transports abort through the handler's own controller. It is now linked
to that controller, so stopping a generation actually cancels it.

The spec asserted `stream: false`, pinning the bug in place; it now
asserts the opposite and covers chunk joining, reasoning exclusion,
auth retry and signal propagation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(openai-codex): stop replaying the stream and treat an abort as one

Two problems in the streaming completePrompt path.

The SDK stream and its consumption loop sat inside the same try, so an
error raised part way through the stream was handled as "the SDK could
not be used at all" and the request was replayed over SSE. Whatever the
SDK had already yielded stayed with the caller, so the replay appended a
second generation to the first. The fallback now closes after the first
SDK event, which is the point where the request has been accepted and
its output is already out. It is set before processEvent runs, since
that mutates response state too, so a throw from it must not replay
either. This covers the chat path as well, since both go through
executeRequest.

Both transports also end quietly on abort, breaking out of their loops
rather than throwing, so completePrompt returned whatever partial text
had arrived and callers read a cancelled generation as a finished one.
It now rejects with an AbortError instead, and a cancellation is passed
through rather than reported to telemetry or relabelled as a completion
error, since stopping is the caller's own doing.

The two abort specs asserted that a cancelled call resolves, which held
the bug in place. They now assert the rejection and cover the pre
aborted and mid stream cases, and a new spec covers the mid stream SDK
failure and checks that SSE is never reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(openai-codex): close the last two replay routes into a live request

Follow-ups to the previous commit, both raised by CodeRabbit.

Closing the SSE fallback after the first SDK event left the OAuth retry
loop as a second way back into a request the service had already
accepted. A mid stream error that reads as an auth failure would refresh
the token and send the whole thing again, so completePrompt could
concatenate two generations and the chat path could repeat streamed
effects. The flag moved onto the handler, reset per request alongside
the other response state, and the retry is now skipped once the SDK has
emitted. A refresh before any event still retries as before, which is
the case that loop exists for.

An abort also still reached the fallback, since the SDK rejects when the
caller cancels and that read as a transport failure. It spent a second
request on an already aborted signal and reported the cancellation as a
connection error. The fallback now rethrows instead.

Specs for both, plus a check that the pre event refresh path is
untouched.

* fix(openai-codex): report one abort result however the request ended

Chasing a review comment about the cancellation tests turned up a real
inconsistency behind it. A stream that ended quietly threw an AbortError,
but a transport that rejected on abort had its own error passed straight
through, so what a cancelled completePrompt rejected with depended on how
far the request had got. Callers cannot key off that. An abort is now
restated as an AbortError unless it already is one.

The in flight case, where the caller cancels after the request is away
but before any event, had no coverage. It only works if the caller signal
is genuinely linked to the internal controller, so it is the case that
would catch that link breaking. Added, and it waits for the request to be
in flight before aborting, since the token lookup and the listener are
both async and a synchronous abort fires before anything is listening.

Also tightened the existing abort assertion from a bare rejects.toThrow,
which passed on any error at all, to the same AbortError check the other
cancellation tests use.

* fix(openai-codex): keep refusals out of the completed prompt

completePrompt joined every text chunk, and a refusal is streamed as text so
the chat can show it. The non-streaming request this replaced read output_text,
which never carries refusals, so it returned "". The streaming version instead
handed the prompt enhancer "[Refusal] ..." to paste into the input box as if the
model had answered.

The prefix both refusal branches emit is now a shared constant, and
completePrompt skips the chunks carrying it - any real output_text alongside a
refusal still comes back, matching what output_text held.

Also corrects the comment naming commit-message generation as the caller. The
prompt enhancer is the only one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
R
Rafael Oliveira committed
bd399fa77046727fff853f4a41936984753c9eda
Parent: c747c02
Committed by GitHub <noreply@github.com> on 8/22/2026, 2:45:22 PM