Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b457024
feat(api): abort signal support for openai-codex (completePrompt + cr…
easonliang28 Aug 19, 2026
22da1d1
fix(api): address CodeRabbit review on openai-codex abort handling
easonliang28 Aug 19, 2026
0ca6c23
refactor(api): adopt RequestConfigBuilder in feat/abort-r1-openai-cod…
easonliang28 Aug 20, 2026
ac8e22b
Merge upstream/main into feat/abort-r1-openai-codex (#1290)
easonliang28 Aug 23, 2026
8b1e81d
Merge branch 'main' into feat/abort-r1-openai-codex
easonLiangWorldedtech Aug 30, 2026
df95e76
Merge branch 'main' into feat/abort-r1-openai-codex
easonLiangWorldedtech Sep 2, 2026
ee5d8ab
Merge branch 'main' into feat/abort-r1-openai-codex
edelauna Sep 3, 2026
dcaa64b
Merge branch 'main' into feat/abort-r1-openai-codex
easonliang28 Sep 5, 2026
714754c
fix(api): address CodeRabbit abort-signal findings in openai-codex
easonliang28 Sep 5, 2026
1aa5fcd
fix(api): close the openai-codex abort mutation gaps
easonliang28 Sep 5, 2026
0183b94
fix(api): import mergeAbortSignalAndTimeout directly; drop builder st…
easonliang28 Sep 15, 2026
73b2b23
fix(api): fast-fail and guard the openai-codex completePrompt stream …
easonliang28 Sep 15, 2026
6414002
test(api): structural kill test for the completePrompt top-of-loop ab…
easonliang28 Sep 16, 2026
a27305b
fix(api): observe cancellation in openai-codex OAuth setup and streaming
easonliang28 Sep 23, 2026
2ce9e64
test(api): cover openai-codex cancellation races and guarded yields
easonliang28 Sep 23, 2026
bf39b3c
test(api): pin the remaining openai-codex abort guards to killable mu…
easonliang28 Sep 23, 2026
9a14884
Merge feat/abort-r1-openai-codex (main-first parent shape for the CI …
easonliang28 Sep 23, 2026
d2f3d94
Merge branch 'main' into feat/abort-r1-openai-codex
easonLiangWorldedtech Sep 27, 2026
2aafd62
Merge branch 'main' into feat/abort-r1-openai-codex
easonLiangWorldedtech Sep 28, 2026
642443c
fix(api): race the auth retry refresh against the caller's abort signal
Sep 28, 2026
b4227f1
Merge branch 'main' into feat/abort-r1-openai-codex
easonLiangWorldedtech Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,4 +526,113 @@ describe("OpenAiCodexHandler native tool calls", () => {
}),
)
})

describe("createMessage abort signal", () => {
it("should bridge the external abortSignal into the internal AbortController", async () => {
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")

// The mock transport pauses mid-flight until the request-local signal aborts
const mockCreate = vi.fn().mockImplementation(async (_body: unknown, init?: { signal?: AbortSignal }) => {
return {
async *[Symbol.asyncIterator]() {
yield { type: "response.text.delta", delta: "test" }
await new Promise<void>((resolve) => {
const signal = init?.signal
if (!signal || signal.aborted) {
resolve()
return
}
signal.addEventListener("abort", () => resolve(), { once: true })
})
yield {
type: "response.completed",
response: {
id: "resp_1",
status: "completed",
output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }],
usage: { input_tokens: 1, output_tokens: 1 },
},
}
// Arrives after the abort resolves the pause: the loop guard must break before
// processing the completed event and this delta.
yield { type: "response.text.delta", delta: "post-abort" }
},
}
})
Object.assign(handler, {
client: {
responses: { create: mockCreate },
},
})

const controller = new AbortController()
const stream = handler.createMessage("system", [{ role: "user", content: "hello" }], {
taskId: "t",
abortSignal: controller.signal,
})

// Consume the stream (the mock transport pauses mid-flight)
const collected = collectStream(stream)

// Wait until the request has started; the bridge listener is registered before
// the SDK call, so aborting now lands mid-flight
await vi.waitFor(() => expect(mockCreate).toHaveBeenCalled())

// Abort the external signal mid-flight; the bridge must abort the request-local controller
controller.abort()

const chunks = await collected
// Exactly the pre-abort delta: the completed event and the post-abort delta only arrive
// once the abort resolves the transport's pause, so the loop guard must break before
// processing either.
expect(chunks).toEqual([{ type: "text", text: "test" }])

expect(mockCreate).toHaveBeenCalled()
const createCallArgs = mockCreate.mock.calls[0][1] as { signal?: AbortSignal }
// The captured (request-local) signal passed to the SDK must now be aborted
expect(createCallArgs.signal).toBeDefined()
expect(createCallArgs.signal).toBeInstanceOf(AbortSignal)
expect(createCallArgs.signal?.aborted).toBe(true)
})

it("should immediately abort when the external signal is already aborted", async () => {
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")

const mockCreate = vi.fn().mockResolvedValue({
async *[Symbol.asyncIterator]() {
yield { type: "response.text.delta", delta: "test" }
yield {
type: "response.completed",
response: {
id: "resp_1",
status: "completed",
output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }],
usage: { input_tokens: 1, output_tokens: 1 },
},
}
},
})
Object.assign(handler, {
client: {
responses: { create: mockCreate },
},
})

const controller = new AbortController()
controller.abort() // Pre-abort

const stream = handler.createMessage("system", [{ role: "user", content: "hello" }], {
taskId: "t",
abortSignal: controller.signal,
})

// A pre-aborted signal settles in the OAuth race before the SDK is reached: the abort
// contract must win over the request setup, so the stream rejects and the SDK is never
// called with a signal that is already aborted.
await expect(collectStream(stream)).rejects.toMatchObject({ name: "AbortError" })
expect(mockCreate).not.toHaveBeenCalled()
})
})
})
Loading
Loading