Skip to content

Commit a6583be

Browse files
committed
feat(api): pass through abortSignal to 19 provider implementations
1 parent e72f4d8 commit a6583be

40 files changed

Lines changed: 1165 additions & 77 deletions

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,74 @@ describe("VertexHandler", () => {
538538
expect(usageChunks[0]).toHaveProperty("cacheWriteTokens", 5)
539539
expect(usageChunks[0]).toHaveProperty("cacheReadTokens", 3)
540540
})
541+
542+
describe("abort signal", () => {
543+
const systemPrompt = "You are a helpful assistant"
544+
const mockMessages: Anthropic.Messages.MessageParam[] = [
545+
{
546+
role: "user",
547+
content: "Hello",
548+
},
549+
]
550+
551+
it("should pass abort signal through to client in createMessage", async () => {
552+
handler = new AnthropicVertexHandler({
553+
apiModelId: "claude-3-5-sonnet-v2@20241022",
554+
vertexProjectId: "test-project",
555+
vertexRegion: "us-central1",
556+
})
557+
558+
const controller = new AbortController()
559+
560+
const mockCreate = vitest.fn().mockImplementation(async () => ({
561+
async *[Symbol.asyncIterator]() {
562+
yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } }
563+
},
564+
}))
565+
;(handler["client"].messages as any).create = mockCreate
566+
567+
const stream = handler.createMessage(systemPrompt, mockMessages, {
568+
taskId: "test-task",
569+
abortSignal: controller.signal as any,
570+
})
571+
for await (const _ of stream) {
572+
// consume stream
573+
}
574+
575+
expect(mockCreate).toHaveBeenCalledWith(
576+
expect.any(Object),
577+
expect.objectContaining({ signal: controller.signal }),
578+
)
579+
})
580+
581+
it("should pass the exact same signal reference (reference identity)", async () => {
582+
handler = new AnthropicVertexHandler({
583+
apiModelId: "claude-3-5-sonnet-v2@20241022",
584+
vertexProjectId: "test-project",
585+
vertexRegion: "us-central1",
586+
})
587+
588+
const controller = new AbortController()
589+
590+
const mockCreate = vitest.fn().mockImplementation(async () => ({
591+
async *[Symbol.asyncIterator]() {
592+
yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } }
593+
},
594+
}))
595+
;(handler["client"].messages as any).create = mockCreate
596+
597+
const stream = handler.createMessage(systemPrompt, mockMessages, {
598+
taskId: "test-task",
599+
abortSignal: controller.signal as any,
600+
})
601+
for await (const _ of stream) {
602+
// consume stream
603+
}
604+
605+
const callOptions = mockCreate.mock.calls[0][1]
606+
expect(callOptions?.signal).toBe(controller.signal)
607+
})
608+
})
541609
})
542610

543611
describe("thinking functionality", () => {

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,42 @@ describe("AnthropicHandler", () => {
425425
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
426426
expect(requestBody?.thinking).toEqual({ type: "adaptive" })
427427
expect(requestBody?.temperature).toBeUndefined()
428-
expect(requestBody?.max_tokens).toBe(32768)
429-
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
428+
})
429+
430+
describe("abort signal", () => {
431+
it("should pass abort signal through to client in createMessage", async () => {
432+
const controller = new AbortController()
433+
const testHandler = new AnthropicHandler(mockOptions)
434+
435+
const stream = testHandler.createMessage(systemPrompt, [], {
436+
taskId: "test-task",
437+
abortSignal: controller.signal as any,
438+
})
439+
for await (const _ of stream) {
440+
// consume stream
441+
}
442+
443+
expect(mockCreate).toHaveBeenCalledWith(
444+
expect.any(Object),
445+
expect.objectContaining({ signal: controller.signal }),
446+
)
447+
})
448+
449+
it("should pass the exact same signal reference (reference identity)", async () => {
450+
const controller = new AbortController()
451+
const testHandler = new AnthropicHandler(mockOptions)
452+
453+
const stream = testHandler.createMessage(systemPrompt, [], {
454+
taskId: "test-task",
455+
abortSignal: controller.signal as any,
456+
})
457+
for await (const _ of stream) {
458+
// consume stream
459+
}
460+
461+
const callOptions = mockCreate.mock.calls[0][1]
462+
expect(callOptions?.signal).toBe(controller.signal)
463+
})
430464
})
431465
})
432466

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,56 @@ describe("BaseOpenAiCompatibleProvider", () => {
454454
})
455455
})
456456

457+
describe("abort signal", () => {
458+
it("should pass abort signal through to client in createMessage", async () => {
459+
const controller = new AbortController()
460+
461+
mockCreate.mockImplementationOnce(() => ({
462+
[Symbol.asyncIterator]: () => ({
463+
async next() {
464+
return { done: true }
465+
},
466+
}),
467+
}))
468+
469+
const stream = handler.createMessage("system prompt", [], {
470+
taskId: "test-task",
471+
abortSignal: controller.signal as any,
472+
})
473+
for await (const _ of stream) {
474+
// consume stream
475+
}
476+
477+
expect(mockCreate).toHaveBeenCalledWith(
478+
expect.any(Object),
479+
expect.objectContaining({ signal: controller.signal }),
480+
)
481+
})
482+
483+
it("should pass the exact same signal reference (reference identity)", async () => {
484+
const controller = new AbortController()
485+
486+
mockCreate.mockImplementationOnce(() => ({
487+
[Symbol.asyncIterator]: () => ({
488+
async next() {
489+
return { done: true }
490+
},
491+
}),
492+
}))
493+
494+
const stream = handler.createMessage("system prompt", [], {
495+
taskId: "test-task",
496+
abortSignal: controller.signal as any,
497+
})
498+
for await (const _ of stream) {
499+
// consume stream
500+
}
501+
502+
const callOptions = mockCreate.mock.calls[0][1]
503+
expect(callOptions?.signal).toBe(controller.signal)
504+
})
505+
})
506+
457507
describe("Tool call handling", () => {
458508
it("should yield tool_call_end events when finish_reason is tool_calls", async () => {
459509
mockCreate.mockImplementationOnce(() => {

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,42 @@ describe("DeepSeekHandler", () => {
455455
const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
456456
expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }])
457457
})
458+
459+
describe("abort signal", () => {
460+
it("should pass abort signal through to client in createMessage", async () => {
461+
const controller = new AbortController()
462+
const testHandler = new DeepSeekHandler(mockOptions)
463+
464+
const stream = testHandler.createMessage(systemPrompt, messages, {
465+
taskId: "test-task",
466+
abortSignal: controller.signal as any,
467+
})
468+
for await (const _ of stream) {
469+
// consume stream
470+
}
471+
472+
expect(mockCreate).toHaveBeenCalledWith(
473+
expect.any(Object),
474+
expect.objectContaining({ signal: controller.signal }),
475+
)
476+
})
477+
478+
it("should pass the exact same signal reference (reference identity)", async () => {
479+
const controller = new AbortController()
480+
const testHandler = new DeepSeekHandler(mockOptions)
481+
482+
const stream = testHandler.createMessage(systemPrompt, messages, {
483+
taskId: "test-task",
484+
abortSignal: controller.signal as any,
485+
})
486+
for await (const _ of stream) {
487+
// consume stream
488+
}
489+
490+
const callOptions = mockCreate.mock.calls[0][1]
491+
expect(callOptions?.signal).toBe(controller.signal)
492+
})
493+
})
458494
})
459495

460496
describe("processUsageMetrics", () => {
@@ -563,7 +599,7 @@ describe("DeepSeekHandler", () => {
563599
expect.objectContaining({
564600
thinking: { type: "enabled" },
565601
}),
566-
{}, // Empty path options for non-Azure URLs
602+
undefined,
567603
)
568604
const callArgs = mockCreate.mock.calls[0][0]
569605
expect(callArgs.reasoning_effort).toBeUndefined()
@@ -586,7 +622,7 @@ describe("DeepSeekHandler", () => {
586622
reasoning_effort: "high",
587623
max_completion_tokens: 200_000,
588624
}),
589-
{},
625+
undefined,
590626
)
591627
})
592628

@@ -623,7 +659,7 @@ describe("DeepSeekHandler", () => {
623659
thinking: { type: "enabled" },
624660
reasoning_effort: "max",
625661
}),
626-
{},
662+
undefined,
627663
)
628664
})
629665

src/api/providers/__tests__/lite-llm.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,62 @@ describe("LiteLLMHandler", () => {
922922
})
923923
})
924924

925+
describe("abort signal", () => {
926+
it("should pass abort signal through to client in createMessage", async () => {
927+
const mockStream = {
928+
async *[Symbol.asyncIterator]() {
929+
yield {
930+
choices: [{ delta: { content: "response" } }],
931+
usage: null,
932+
}
933+
},
934+
}
935+
936+
mockCreate.mockReturnValue({
937+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
938+
})
939+
940+
const controller = new AbortController()
941+
const stream = handler.createMessage("system", [{ role: "user", content: "Test" }], {
942+
taskId: "test-task",
943+
abortSignal: controller.signal as any,
944+
})
945+
for await (const _ of stream) {
946+
}
947+
948+
expect(mockCreate).toHaveBeenCalledWith(
949+
expect.any(Object),
950+
expect.objectContaining({ signal: controller.signal }),
951+
)
952+
})
953+
954+
it("should pass the exact same signal reference (reference identity)", async () => {
955+
const mockStream = {
956+
async *[Symbol.asyncIterator]() {
957+
yield {
958+
choices: [{ delta: { content: "response" } }],
959+
usage: null,
960+
}
961+
},
962+
}
963+
964+
mockCreate.mockReturnValue({
965+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
966+
})
967+
968+
const controller = new AbortController()
969+
const stream = handler.createMessage("system", [{ role: "user", content: "Test" }], {
970+
taskId: "test-task",
971+
abortSignal: controller.signal as any,
972+
})
973+
for await (const _ of stream) {
974+
}
975+
976+
const callOptions = mockCreate.mock.calls[0][1]
977+
expect(callOptions?.signal).toBe(controller.signal)
978+
})
979+
})
980+
925981
describe("tool ID normalization", () => {
926982
it("should truncate tool IDs longer than 64 characters", async () => {
927983
const optionsWithBedrock: ApiHandlerOptions = {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ describe("LmStudioHandler Native Tools", () => {
8383
}),
8484
]),
8585
}),
86+
undefined,
8687
)
8788
// parallel_tool_calls should be true by default when not explicitly set
8889
const callArgs = mockCreate.mock.calls[0][0]
@@ -109,6 +110,7 @@ describe("LmStudioHandler Native Tools", () => {
109110
expect.objectContaining({
110111
tool_choice: "auto",
111112
}),
113+
undefined,
112114
)
113115
})
114116

@@ -221,6 +223,7 @@ describe("LmStudioHandler Native Tools", () => {
221223
expect.objectContaining({
222224
parallel_tool_calls: true,
223225
}),
226+
undefined,
224227
)
225228
})
226229

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,42 @@ describe("LmStudioHandler", () => {
127127
}
128128
}).rejects.toThrow("Please check the LM Studio developer logs to debug what went wrong")
129129
})
130+
131+
describe("abort signal", () => {
132+
it("should pass abort signal through to client in createMessage", async () => {
133+
const controller = new AbortController()
134+
const testHandler = new LmStudioHandler(mockOptions)
135+
136+
const stream = testHandler.createMessage(systemPrompt, messages, {
137+
taskId: "test-task",
138+
abortSignal: controller.signal as any,
139+
})
140+
for await (const _ of stream) {
141+
// consume stream
142+
}
143+
144+
expect(mockCreate).toHaveBeenCalledWith(
145+
expect.any(Object),
146+
expect.objectContaining({ signal: controller.signal }),
147+
)
148+
})
149+
150+
it("should pass the exact same signal reference (reference identity)", async () => {
151+
const controller = new AbortController()
152+
const testHandler = new LmStudioHandler(mockOptions)
153+
154+
const stream = testHandler.createMessage(systemPrompt, messages, {
155+
taskId: "test-task",
156+
abortSignal: controller.signal as any,
157+
})
158+
for await (const _ of stream) {
159+
// consume stream
160+
}
161+
162+
const callOptions = mockCreate.mock.calls[0][1]
163+
expect(callOptions?.signal).toBe(controller.signal)
164+
})
165+
})
130166
})
131167

132168
describe("completePrompt", () => {

0 commit comments

Comments
 (0)