Skip to content

Commit 14f6bb0

Browse files
committed
feat(api): abort signal support for openai-codex (completePrompt + createMessage)
- completePrompt: use a request-local signal built with mergeAbortSignalAndTimeout(options?.abortSignal, options?.timeoutMs) for the fetch call instead of the handler-wide AbortController; re-throw abort errors as-is so cancellation is detectable by the "AbortError" name - createMessage: pass metadata into executeRequest and bridge metadata?.abortSignal into the internal AbortController (Bedrock pattern: pre-aborted guard + { once: true } listener), covering both the OpenAI SDK streaming path and the manual SSE fetch fallback - specs: port the reference completePrompt coverage (request body, timeoutMs=0, abortSignal/timeoutMs merging, error paths) and add pre-aborted and in-flight abort tests rejecting with name === "AbortError"; port the createMessage abort bridge + pre-aborted tests into the native tool calls spec
1 parent 38d5ee0 commit 14f6bb0

3 files changed

Lines changed: 474 additions & 5 deletions

File tree

‎src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,4 +518,93 @@ describe("OpenAiCodexHandler native tool calls", () => {
518518
}),
519519
)
520520
})
521+
522+
describe("createMessage abort signal", () => {
523+
it("should bridge the external abortSignal into the internal AbortController", async () => {
524+
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
525+
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
526+
527+
const mockCreate = vi.fn().mockResolvedValue({
528+
async *[Symbol.asyncIterator]() {
529+
yield { type: "response.text.delta", delta: "test" }
530+
yield {
531+
type: "response.completed",
532+
response: {
533+
id: "resp_1",
534+
status: "completed",
535+
output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }],
536+
usage: { input_tokens: 1, output_tokens: 1 },
537+
},
538+
}
539+
},
540+
})
541+
Object.assign(handler, {
542+
client: {
543+
responses: { create: mockCreate },
544+
},
545+
})
546+
547+
const controller = new AbortController()
548+
const stream = handler.createMessage("system", [{ role: "user", content: "hello" }], {
549+
taskId: "t",
550+
abortSignal: controller.signal,
551+
})
552+
553+
// Consume the stream to trigger the request
554+
await collectStream(stream)
555+
556+
expect(mockCreate).toHaveBeenCalled()
557+
const createCallArgs = mockCreate.mock.calls[0][1] as { signal?: AbortSignal }
558+
expect(createCallArgs.signal).toBeDefined()
559+
expect(createCallArgs.signal).toBeInstanceOf(AbortSignal)
560+
561+
// Verify the signal is not aborted before we abort the external one
562+
expect(createCallArgs.signal?.aborted).toBe(false)
563+
564+
// Abort the external signal; the bridge aborts the internal controller
565+
controller.abort()
566+
expect(controller.signal.aborted).toBe(true)
567+
})
568+
569+
it("should immediately abort when the external signal is already aborted", async () => {
570+
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
571+
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
572+
573+
const mockCreate = vi.fn().mockResolvedValue({
574+
async *[Symbol.asyncIterator]() {
575+
yield { type: "response.text.delta", delta: "test" }
576+
yield {
577+
type: "response.completed",
578+
response: {
579+
id: "resp_1",
580+
status: "completed",
581+
output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }],
582+
usage: { input_tokens: 1, output_tokens: 1 },
583+
},
584+
}
585+
},
586+
})
587+
Object.assign(handler, {
588+
client: {
589+
responses: { create: mockCreate },
590+
},
591+
})
592+
593+
const controller = new AbortController()
594+
controller.abort() // Pre-abort
595+
596+
const stream = handler.createMessage("system", [{ role: "user", content: "hello" }], {
597+
taskId: "t",
598+
abortSignal: controller.signal,
599+
})
600+
601+
// Consume the stream to trigger the request
602+
await collectStream(stream)
603+
604+
expect(mockCreate).toHaveBeenCalled()
605+
const createCallArgs = mockCreate.mock.calls[0][1] as { signal?: AbortSignal }
606+
// The internal signal should already be aborted since the external one was pre-aborted
607+
expect(createCallArgs.signal?.aborted).toBe(true)
608+
})
609+
})
521610
})

0 commit comments

Comments
 (0)