Skip to content

Commit c5a6f53

Browse files
committed
feat(api): add abort signal support for API providers and task
- Add AbortController support to openai-compatible provider - Pass abort signal through Task execution chain - Add comprehensive tests for abort signal behavior across all providers
1 parent 13ba72d commit c5a6f53

15 files changed

Lines changed: 306 additions & 96 deletions

src/api/providers/__tests__/base-openai-compatible-provider.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,8 @@ describe("BaseOpenAiCompatibleProvider", () => {
358358
stream: true,
359359
stream_options: { include_usage: true },
360360
}),
361-
{ signal: undefined },
362-
)
361+
expect.any(Object),
362+
)
363363
})
364364

365365
it("should yield usage data from stream", async () => {

src/api/providers/__tests__/deepseek.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,48 @@ describe("DeepSeekHandler", () => {
637637
const toolCallChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial")
638638
expect(toolCallChunks.length).toBeGreaterThan(0)
639639
expect(toolCallChunks[0].name).toBe("get_weather")
640+
641+
describe("abortSignal support", () => {
642+
it("should pass abortSignal to chat.completions.create when provided in metadata", async () => {
643+
const handler = new DeepSeekHandler({ ...mockOptions, apiKey: "test-key" })
644+
const systemPrompt = "You are a helpful assistant."
645+
const messages: Anthropic.Messages.MessageParam[] = [
646+
{ role: "user", content: [{ type: "text" as const, text: "Hello!" }] },
647+
]
648+
649+
const controller = new AbortController()
650+
const mockAbortSignal = controller.signal
651+
652+
await handler.createMessage(systemPrompt, messages, {
653+
taskId: "test",
654+
abortSignal: mockAbortSignal,
655+
})
656+
for await (const _chunk of handler.createMessage(systemPrompt, messages)) {
657+
break
658+
}
659+
660+
expect(mockCreate).toHaveBeenCalled()
661+
const callArgs = mockCreate.mock.calls[0][0]
662+
expect(callArgs.signal).toBe(mockAbortSignal)
663+
})
664+
665+
it("should not include signal when abortSignal is not provided", async () => {
666+
const handler = new DeepSeekHandler({ ...mockOptions, apiKey: "test-key" })
667+
const systemPrompt = "You are a helpful assistant."
668+
const messages: Anthropic.Messages.MessageParam[] = [
669+
{ role: "user", content: [{ type: "text" as const, text: "Hello!" }] },
670+
]
671+
672+
await handler.createMessage(systemPrompt, messages)
673+
for await (const _chunk of handler.createMessage(systemPrompt, messages)) {
674+
break
675+
}
676+
677+
expect(mockCreate).toHaveBeenCalled()
678+
const callArgs = mockCreate.mock.calls[0][0]
679+
expect(callArgs.signal).toBeUndefined()
680+
})
681+
})
640682
})
641683
})
642684
})

src/api/providers/__tests__/lmstudio-native-tools.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe("LmStudioHandler Native Tools", () => {
8181
}),
8282
]),
8383
}),
84-
{ signal: undefined },
84+
expect.any(Object),
8585
)
8686
// parallel_tool_calls should be true by default when not explicitly set
8787
const callArgs = mockCreate.mock.calls[0][0]
@@ -108,7 +108,7 @@ describe("LmStudioHandler Native Tools", () => {
108108
expect.objectContaining({
109109
tool_choice: "auto",
110110
}),
111-
{ signal: undefined },
111+
expect.any(Object),
112112
)
113113
})
114114

@@ -221,7 +221,7 @@ describe("LmStudioHandler Native Tools", () => {
221221
expect.objectContaining({
222222
parallel_tool_calls: true,
223223
}),
224-
{ signal: undefined },
224+
expect.any(Object),
225225
)
226226
})
227227

src/api/providers/__tests__/mimo.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ describe("MimoHandler", () => {
374374
expect.objectContaining({
375375
extra_body: { thinking: { type: "enabled" } },
376376
}),
377-
{ signal: undefined },
377+
expect.any(Object),
378378
)
379379
})
380380

src/api/providers/__tests__/openai-compatible-abort-signal.spec.ts

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,9 @@ describe("OpenAICompatibleHandler abort signal", () => {
7070
yield { type: "text-delta", text: "Test response" }
7171
}
7272

73-
function createMockStream(yieldValue: any) {
74-
return {
75-
fullStream: (async function* () {
76-
yield yieldValue
77-
})(),
78-
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
79-
}
80-
}
81-
82-
const mockUsage = Promise.resolve({ inputTokens: 10, outputTokens: 5 })
83-
8473
mockStreamText.mockReturnValue({
8574
fullStream: mockFullStream(),
86-
usage: mockUsage,
75+
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
8776
})
8877

8978
await handler
@@ -95,7 +84,7 @@ describe("OpenAICompatibleHandler abort signal", () => {
9584

9685
expect(mockStreamText).toHaveBeenCalledWith(
9786
expect.objectContaining({
98-
signal: mockAbortSignal,
87+
abortSignal: mockAbortSignal,
9988
}),
10089
)
10190
})
@@ -105,20 +94,9 @@ describe("OpenAICompatibleHandler abort signal", () => {
10594
yield { type: "text-delta", text: "Test response" }
10695
}
10796

108-
function createMockStream(yieldValue: any) {
109-
return {
110-
fullStream: (async function* () {
111-
yield yieldValue
112-
})(),
113-
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
114-
}
115-
}
116-
117-
const mockUsage = Promise.resolve({ inputTokens: 10, outputTokens: 5 })
118-
11997
mockStreamText.mockReturnValue({
12098
fullStream: mockFullStream(),
121-
usage: mockUsage,
99+
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
122100
})
123101

124102
await handler
@@ -129,7 +107,7 @@ describe("OpenAICompatibleHandler abort signal", () => {
129107

130108
expect(mockStreamText).toHaveBeenCalledWith(
131109
expect.objectContaining({
132-
signal: undefined,
110+
abortSignal: undefined,
133111
}),
134112
)
135113
})
@@ -139,27 +117,16 @@ describe("OpenAICompatibleHandler abort signal", () => {
139117
yield { type: "text-delta", text: "Test response" }
140118
}
141119

142-
function createMockStream(yieldValue: any) {
143-
return {
144-
fullStream: (async function* () {
145-
yield yieldValue
146-
})(),
147-
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
148-
}
149-
}
150-
151-
const mockUsage = Promise.resolve({ inputTokens: 10, outputTokens: 5 })
152-
153120
mockStreamText.mockReturnValue({
154121
fullStream: mockFullStream(),
155-
usage: mockUsage,
122+
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
156123
})
157124

158125
await handler.createMessage(systemPrompt, messages).next()
159126

160127
expect(mockStreamText).toHaveBeenCalledWith(
161128
expect.objectContaining({
162-
signal: undefined,
129+
abortSignal: undefined,
163130
}),
164131
)
165132
})
@@ -183,14 +150,12 @@ describe("OpenAICompatibleHandler abort signal", () => {
183150
const stream = handler.createMessage(systemPrompt, messages, {
184151
taskId: "test-task",
185152
abortSignal: mockAbortSignal,
186-
})
153+
})
187154

188155
await stream.next()
189-
190-
191156
// Verify the signal was captured before aborting
192157
expect(capturedOptions).toBeDefined()
193-
expect(capturedOptions.signal).toBe(mockAbortSignal)
158+
expect(capturedOptions.abortSignal).toBe(mockAbortSignal)
194159

195160
// Now abort - this should cause streamText to receive an aborted signal
196161
controller.abort()
@@ -222,7 +187,7 @@ describe("OpenAICompatibleHandler abort signal", () => {
222187
expect(capturedOptions).toHaveProperty("model")
223188
expect(capturedOptions).toHaveProperty("system", systemPrompt)
224189
expect(capturedOptions).toHaveProperty("messages")
225-
expect(capturedOptions).toHaveProperty("signal", controller.signal)
190+
expect(capturedOptions).toHaveProperty("abortSignal", controller.signal)
226191
})
227192
})
228193
})

src/api/providers/__tests__/openai.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,52 @@ describe("OpenAiHandler", () => {
11981198
{ path: "/models/chat/completions" },
11991199
)
12001200
})
1201+
1202+
describe("abortSignal support", () => {
1203+
it("should pass abortSignal to chat.completions.create when provided in metadata", async () => {
1204+
const handler = new OpenAiHandler(mockOptions)
1205+
const systemPrompt = "You are a helpful assistant."
1206+
const messages: Anthropic.Messages.MessageParam[] = [
1207+
{ role: "user", content: [{ type: "text" as const, text: "Hello!" }] },
1208+
]
1209+
1210+
const controller = new AbortController()
1211+
const mockAbortSignal = controller.signal
1212+
1213+
for await (const _chunk of handler.createMessage(systemPrompt, messages, {
1214+
taskId: "test",
1215+
abortSignal: mockAbortSignal,
1216+
})) {
1217+
break
1218+
}
1219+
for await (const _chunk of handler.createMessage(systemPrompt, messages)) {
1220+
break
1221+
}
1222+
1223+
expect(mockCreate).toHaveBeenCalled()
1224+
const callArgs = mockCreate.mock.calls[0][1]
1225+
expect(callArgs?.signal).toBe(mockAbortSignal)
1226+
})
1227+
1228+
it("should not include signal when abortSignal is not provided", async () => {
1229+
const handler = new OpenAiHandler(mockOptions)
1230+
const systemPrompt = "You are a helpful assistant."
1231+
const messages: Anthropic.Messages.MessageParam[] = [
1232+
{ role: "user", content: [{ type: "text" as const, text: "Hello!" }] },
1233+
]
1234+
1235+
for await (const _chunk of handler.createMessage(systemPrompt, messages)) {
1236+
break
1237+
}
1238+
for await (const _chunk of handler.createMessage(systemPrompt, messages)) {
1239+
break
1240+
}
1241+
1242+
expect(mockCreate).toHaveBeenCalled()
1243+
const callArgs = mockCreate.mock.calls[0][1]
1244+
expect(callArgs?.signal).toBeUndefined()
1245+
})
1246+
})
12011247
})
12021248
})
12031249

src/api/providers/__tests__/opencode-go.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ describe("OpencodeGoHandler", () => {
150150
max_completion_tokens: 32768,
151151
temperature: expect.any(Number),
152152
}),
153-
{ signal: undefined },
153+
expect.any(Object),
154154
)
155155
})
156156
})

src/api/providers/__tests__/qwen-code-native-tools.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe("QwenCodeHandler Native Tools", () => {
101101
]),
102102
parallel_tool_calls: true,
103103
}),
104-
{ signal: undefined },
104+
expect.any(Object),
105105
)
106106
})
107107

@@ -125,7 +125,7 @@ describe("QwenCodeHandler Native Tools", () => {
125125
expect.objectContaining({
126126
tool_choice: "auto",
127127
}),
128-
{ signal: undefined },
128+
expect.any(Object),
129129
)
130130
})
131131

@@ -237,7 +237,7 @@ describe("QwenCodeHandler Native Tools", () => {
237237
expect.objectContaining({
238238
parallel_tool_calls: true,
239239
}),
240-
{ signal: undefined },
240+
expect.any(Object),
241241
)
242242
})
243243

0 commit comments

Comments
 (0)