Skip to content

Commit 589b887

Browse files
committed
feat: add abort singal for pass-through providers
1 parent 7e7348b commit 589b887

38 files changed

Lines changed: 2040 additions & 58 deletions

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

Lines changed: 166 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,13 @@ describe("VertexHandler", () => {
263263
],
264264
stream: true,
265265
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
266-
tools: expect.any(Array),
267-
tool_choice: expect.any(Object),
266+
tools: [],
267+
tool_choice: {
268+
disable_parallel_tool_use: false,
269+
type: "auto",
270+
},
268271
}),
269-
undefined,
272+
{},
270273
)
271274
})
272275

@@ -481,7 +484,7 @@ describe("VertexHandler", () => {
481484
}),
482485
],
483486
}),
484-
undefined,
487+
{},
485488
)
486489
})
487490

@@ -1156,7 +1159,7 @@ describe("VertexHandler", () => {
11561159
}
11571160

11581161
// Verify the API was called without the beta header
1159-
expect(mockCreate).toHaveBeenCalledWith(expect.anything(), undefined)
1162+
expect(mockCreate).toHaveBeenCalledWith(expect.anything(), {})
11601163
})
11611164
})
11621165

@@ -1246,7 +1249,7 @@ describe("VertexHandler", () => {
12461249
thinking: { type: "enabled", budget_tokens: 4096 },
12471250
temperature: 1.0, // Thinking requires temperature 1.0
12481251
}),
1249-
undefined,
1252+
{},
12501253
)
12511254
})
12521255

@@ -1273,7 +1276,7 @@ describe("VertexHandler", () => {
12731276
expect.objectContaining({
12741277
thinking: { type: "adaptive" },
12751278
}),
1276-
undefined,
1279+
{},
12771280
)
12781281

12791282
const request = mockCreate.mock.calls[0][0]
@@ -1302,7 +1305,7 @@ describe("VertexHandler", () => {
13021305
expect.objectContaining({
13031306
thinking: { type: "adaptive" },
13041307
}),
1305-
undefined,
1308+
{},
13061309
)
13071310

13081311
const request = mockCreate.mock.calls[0][0]
@@ -1393,7 +1396,7 @@ describe("VertexHandler", () => {
13931396
]),
13941397
tool_choice: { type: "auto", disable_parallel_tool_use: false },
13951398
}),
1396-
undefined,
1399+
{},
13971400
)
13981401
})
13991402

@@ -1446,7 +1449,7 @@ describe("VertexHandler", () => {
14461449
}),
14471450
]),
14481451
}),
1449-
undefined,
1452+
{},
14501453
)
14511454
})
14521455

@@ -1611,4 +1614,157 @@ describe("VertexHandler", () => {
16111614
})
16121615
})
16131616
})
1617+
1618+
describe("abort signal", () => {
1619+
it("should handle abort signal triggered during request", async () => {
1620+
const controller = new AbortController()
1621+
const handler = new AnthropicVertexHandler({
1622+
apiModelId: "claude-3-sonnet",
1623+
vertexProjectId: "test-project",
1624+
vertexRegion: "us-central1",
1625+
})
1626+
1627+
const mockStream = async function* () {
1628+
await new Promise((resolve) => setTimeout(resolve, 10))
1629+
if (controller.signal.aborted) {
1630+
throw new Error("AbortError: The operation was aborted")
1631+
}
1632+
yield {
1633+
type: "message_start",
1634+
message: { usage: { input_tokens: 10, output_tokens: 0 } },
1635+
}
1636+
}
1637+
1638+
;(handler["client"].messages as any).create = vitest.fn().mockResolvedValue(mockStream())
1639+
1640+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
1641+
taskId: "test",
1642+
tools: [],
1643+
abortSignal: controller.signal,
1644+
})
1645+
1646+
const chunks: any[] = []
1647+
for await (const chunk of stream) {
1648+
chunks.push(chunk)
1649+
}
1650+
1651+
expect(chunks.length).toBeGreaterThan(0)
1652+
})
1653+
1654+
it("should not pass signal when abortSignal is undefined", async () => {
1655+
const handler = new AnthropicVertexHandler({
1656+
apiModelId: "claude-3-sonnet",
1657+
vertexProjectId: "test-project",
1658+
vertexRegion: "us-central1",
1659+
})
1660+
1661+
const mockStream = async function* () {
1662+
yield {
1663+
type: "message_start",
1664+
message: { usage: { input_tokens: 10, output_tokens: 5 } },
1665+
}
1666+
yield {
1667+
type: "content_block_start",
1668+
content_block: { type: "text", text: "" },
1669+
}
1670+
yield {
1671+
type: "content_block_delta",
1672+
delta: { type: "text_delta", text: "response" },
1673+
}
1674+
}
1675+
1676+
;(handler["client"].messages as any).create = vitest.fn().mockResolvedValue(mockStream())
1677+
1678+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }])
1679+
1680+
const chunks: any[] = []
1681+
for await (const chunk of stream) {
1682+
chunks.push(chunk)
1683+
}
1684+
1685+
expect(chunks.length).toBeGreaterThan(0)
1686+
})
1687+
1688+
it("should abort immediately if signal is already aborted", async () => {
1689+
const controller = new AbortController()
1690+
controller.abort()
1691+
1692+
const testHandler = new AnthropicVertexHandler({
1693+
apiModelId: "claude-3-sonnet",
1694+
vertexProjectId: "test-project",
1695+
vertexRegion: "us-central1",
1696+
})
1697+
1698+
testHandler["client"].messages.create = vitest.fn().mockImplementation(async (options, requestOptions) => {
1699+
// Verify that the signal was passed and is already aborted
1700+
expect(requestOptions).toHaveProperty("signal", controller.signal)
1701+
expect(controller.signal.aborted).toBe(true)
1702+
1703+
return {
1704+
[Symbol.asyncIterator]: async function* () {
1705+
if (controller.signal.aborted) {
1706+
throw new Error("AbortError: The operation was aborted")
1707+
}
1708+
yield {
1709+
type: "message_start",
1710+
message: { usage: { input_tokens: 10, output_tokens: 5 } },
1711+
}
1712+
},
1713+
}
1714+
})
1715+
1716+
const stream = testHandler.createMessage("system", [{ role: "user", content: "Hello" }], {
1717+
taskId: "test",
1718+
tools: [],
1719+
abortSignal: controller.signal,
1720+
})
1721+
1722+
await expect(async () => {
1723+
for await (const _chunk of stream) {
1724+
// consume stream
1725+
}
1726+
}).rejects.toThrow(/abort/i)
1727+
})
1728+
1729+
it("should pass signal when provided", async () => {
1730+
const controller = new AbortController()
1731+
let capturedRequestOptions: any
1732+
1733+
const testHandler = new AnthropicVertexHandler({
1734+
apiModelId: "claude-3-sonnet",
1735+
vertexProjectId: "test-project",
1736+
vertexRegion: "us-central1",
1737+
})
1738+
1739+
testHandler["client"].messages.create = vitest.fn().mockImplementation(async (options, requestOptions) => {
1740+
capturedRequestOptions = requestOptions
1741+
return {
1742+
[Symbol.asyncIterator]: async function* () {
1743+
yield {
1744+
type: "message_start",
1745+
message: { usage: { input_tokens: 10, output_tokens: 5 } },
1746+
}
1747+
yield {
1748+
type: "content_block_delta",
1749+
delta: { type: "text_delta", text: "response" },
1750+
}
1751+
},
1752+
}
1753+
})
1754+
1755+
const stream = testHandler.createMessage("system", [{ role: "user", content: "Hello" }], {
1756+
taskId: "test",
1757+
tools: [],
1758+
abortSignal: controller.signal,
1759+
})
1760+
1761+
const chunks: any[] = []
1762+
for await (const chunk of stream) {
1763+
chunks.push(chunk)
1764+
}
1765+
1766+
expect(chunks.length).toBeGreaterThan(0)
1767+
expect(capturedRequestOptions).toHaveProperty("signal", controller.signal)
1768+
})
1769+
})
16141770
})

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,4 +1057,78 @@ describe("AnthropicHandler", () => {
10571057
})
10581058
})
10591059
})
1060+
1061+
describe("abort signal", () => {
1062+
it("should pass abortSignal to the SDK options", async () => {
1063+
const controller = new AbortController()
1064+
1065+
mockCreate.mockImplementation(async (options, requestOptions) => {
1066+
// Verify that the signal was passed
1067+
expect(requestOptions).toHaveProperty("signal", controller.signal)
1068+
return {
1069+
async *[Symbol.asyncIterator]() {
1070+
yield {
1071+
type: "message_start",
1072+
message: { usage: { input_tokens: 10, output_tokens: 5 } },
1073+
}
1074+
yield {
1075+
type: "content_block_delta",
1076+
delta: { type: "text_delta", text: "response" },
1077+
}
1078+
},
1079+
}
1080+
})
1081+
1082+
const handler = new AnthropicHandler(mockOptions)
1083+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
1084+
taskId: "test",
1085+
tools: [],
1086+
abortSignal: controller.signal,
1087+
})
1088+
1089+
const chunks: any[] = []
1090+
for await (const chunk of stream) {
1091+
chunks.push(chunk)
1092+
}
1093+
1094+
expect(chunks.length).toBeGreaterThan(0)
1095+
})
1096+
1097+
it("should work normally without abortSignal", async () => {
1098+
const handler = new AnthropicHandler(mockOptions)
1099+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }])
1100+
1101+
const chunks: any[] = []
1102+
for await (const chunk of stream) {
1103+
chunks.push(chunk)
1104+
}
1105+
1106+
expect(chunks.length).toBeGreaterThan(0)
1107+
})
1108+
1109+
it("should not pass signal when abortSignal is undefined", async () => {
1110+
mockCreate.mockImplementation(async (options, requestOptions) => {
1111+
// When no abortSignal is provided, requestOptions should be undefined or not have signal
1112+
expect(requestOptions).toBeUndefined()
1113+
return {
1114+
async *[Symbol.asyncIterator]() {
1115+
yield {
1116+
type: "message_start",
1117+
message: { usage: { input_tokens: 10, output_tokens: 5 } },
1118+
}
1119+
},
1120+
}
1121+
})
1122+
1123+
const handler = new AnthropicHandler(mockOptions)
1124+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }])
1125+
1126+
const chunks: any[] = []
1127+
for await (const chunk of stream) {
1128+
chunks.push(chunk)
1129+
}
1130+
1131+
expect(chunks.length).toBeGreaterThan(0)
1132+
})
1133+
})
10601134
})

0 commit comments

Comments
 (0)