Skip to content

Commit 0a86942

Browse files
committed
feat(api): pass abortSignal to streaming API calls for all providers
- Add AbortController signal support across all OpenAI-compatible providers (openai, openrouter, lm-studio, deepseek, fireworks, vercel-ai-gateway, sambanova, unbound, zai, requesty, opencode-go, qwen-code, mimo, lite-llm) - Add abort signal support to base-openai-compatible-provider - Pass abort signal through Task class for cancellation propagation - Add comprehensive abort signal tests for all providers - Remove ripgrep diagnostic feature (Show Ripgrep Diagnostic command) - Clean up ripgrep service files and related tests - Update list-files to check cwd before invoking rg - Remove unused dependencies from package.json
1 parent 991130a commit 0a86942

62 files changed

Lines changed: 933 additions & 712 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

knip.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"@types/node-cache",
2222
"@types/vscode",
2323
"@vscode/codicons",
24-
"@vscode/ripgrep",
2524
"esbuild-wasm",
2625
"sambanova-ai-provider",
2726
"tree-sitter-wasms",

packages/types/src/vscode.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ export const commandIds = [
4646
"acceptInput",
4747
"focusPanel",
4848
"toggleAutoApprove",
49-
50-
"showRipgrepDiagnostic",
5149
] as const
5250

5351
export type CommandId = (typeof commandIds)[number]

src/activate/__tests__/registerCommands.spec.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ vi.mock("../../i18n", () => ({
8181
t: (key: string) => key,
8282
}))
8383

84-
vi.mock("../../services/ripgrep/diagnostic", () => ({
85-
registerRipgrepDiagnosticCommand: vi.fn().mockReturnValue({ dispose: vi.fn() }),
86-
}))
87-
8884
describe("getVisibleProviderOrLog", () => {
8985
let mockOutputChannel: vscode.OutputChannel
9086

@@ -176,14 +172,6 @@ describe("registerCommands handlers", () => {
176172
setPanel(undefined, "tab")
177173
})
178174

179-
it("registers the ripgrep diagnostic command and stores its disposable in context.subscriptions", async () => {
180-
const { registerRipgrepDiagnosticCommand } = await import("../../services/ripgrep/diagnostic")
181-
const mock = vi.mocked(registerRipgrepDiagnosticCommand)
182-
const disposable = mock.mock.results[0]?.value
183-
expect(mock).toHaveBeenCalled()
184-
expect(mockContext.subscriptions).toContain(disposable)
185-
})
186-
187175
it("settingsButtonClicked posts both settingsButtonClicked and didBecomeVisible actions", () => {
188176
handlers["zoo-code.settingsButtonClicked"]()
189177

src/activate/registerCommands.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { handleNewTask } from "./handleTask"
1313
import { CodeIndexManager } from "../services/code-index/manager"
1414
import { importSettingsWithFeedback } from "../core/config/importExport"
1515
import { MdmService } from "../services/mdm/MdmService"
16-
import { registerRipgrepDiagnosticCommand } from "../services/ripgrep/diagnostic"
1716
import { t } from "../i18n"
1817

1918
/**
@@ -69,27 +68,9 @@ export const registerCommands = (options: RegisterCommandOptions) => {
6968
const command = getCommand(id as CommandId)
7069
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
7170
}
72-
73-
context.subscriptions.push(registerRipgrepDiagnosticCommand())
7471
}
7572

76-
// `showRipgrepDiagnostic` is registered separately by
77-
// `registerRipgrepDiagnosticCommand` (above), which owns the OutputChannel
78-
// lifecycle alongside the command registration, so it's intentionally
79-
// excluded from this map.
80-
//
81-
// Callback shape mirrors VS Code's own `commands.registerCommand` signature
82-
// (`(...args: any[]) => any`), with the return narrowed to `unknown` so
83-
// callers must inspect before using. `any[]` for args is unavoidable: the
84-
// callbacks here are heterogeneous (`importSettings` takes an optional
85-
// `filePath?: string`, others take none) and VS Code dispatches positional
86-
// args dynamically.
87-
type CommandCallback = (...args: any[]) => unknown
88-
const getCommandsMap = ({
89-
context,
90-
outputChannel,
91-
provider,
92-
}: RegisterCommandOptions): Record<Exclude<CommandId, "showRipgrepDiagnostic">, CommandCallback> => ({
73+
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions): Record<CommandId, any> => ({
9374
activationCompleted: () => {},
9475
plusButtonClicked: async () => {
9576
const visibleProvider = getVisibleProviderOrLog(outputChannel)

src/api/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ export interface ApiHandlerCreateMessageMetadata {
9090
* Only applies to providers that support function calling restrictions (e.g., Gemini).
9191
*/
9292
allowedFunctionNames?: string[]
93+
/**
94+
* Abort signal for cancelling the HTTP request mid-stream.
95+
* Passed through to AI SDK's streamText() so the underlying HTTP request is aborted
96+
* when the user clicks stop, preventing wasted API tokens/compute on the provider side.
97+
*/
98+
abortSignal?: AbortSignal
9399
}
94100

95101
export interface ApiHandler {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ describe("BaseOpenAiCompatibleProvider", () => {
354354
stream: true,
355355
stream_options: { include_usage: true },
356356
}),
357-
undefined,
357+
expect.any(Object),
358358
)
359359
})
360360

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__/fireworks.spec.ts

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,46 @@ describe("FireworksHandler", () => {
9595
})
9696

9797
it.each([
98-
{ modelId: "accounts/fireworks/models/glm-5p1" as const, contextWindow: 202752, inputPrice: 1.4, outputPrice: 4.4, cacheReadsPrice: 0.26 },
99-
{ modelId: "accounts/fireworks/models/kimi-k2p6" as const, contextWindow: 262144, inputPrice: 0.95, outputPrice: 4.0, cacheReadsPrice: 0.16 },
100-
{ modelId: "accounts/fireworks/models/deepseek-v4-pro" as const, contextWindow: 1048576, inputPrice: 1.74, outputPrice: 3.48, cacheReadsPrice: 0.14 },
101-
])("should expose newly added model $modelId", ({ modelId, contextWindow, inputPrice, outputPrice, cacheReadsPrice }) => {
102-
expect(fireworksModels[modelId]).toBeDefined()
103-
const info = fireworksModels[modelId]
104-
expect(info.maxTokens).toBeGreaterThan(0)
105-
expect(info.contextWindow).toBe(contextWindow)
106-
expect(info.inputPrice).toBe(inputPrice)
107-
expect(info.outputPrice).toBe(outputPrice)
108-
expect(info.cacheReadsPrice).toBe(cacheReadsPrice)
109-
expect(info.description).toBeTruthy()
110-
111-
const handlerWithModel = new FireworksHandler({
112-
apiModelId: modelId,
113-
fireworksApiKey: "test-fireworks-api-key",
114-
})
115-
expect(handlerWithModel.getModel().id).toBe(modelId)
116-
})
98+
{
99+
modelId: "accounts/fireworks/models/glm-5p1" as const,
100+
contextWindow: 202752,
101+
inputPrice: 1.4,
102+
outputPrice: 4.4,
103+
cacheReadsPrice: 0.26,
104+
},
105+
{
106+
modelId: "accounts/fireworks/models/kimi-k2p6" as const,
107+
contextWindow: 262144,
108+
inputPrice: 0.95,
109+
outputPrice: 4.0,
110+
cacheReadsPrice: 0.16,
111+
},
112+
{
113+
modelId: "accounts/fireworks/models/deepseek-v4-pro" as const,
114+
contextWindow: 1048576,
115+
inputPrice: 1.74,
116+
outputPrice: 3.48,
117+
cacheReadsPrice: 0.14,
118+
},
119+
])(
120+
"should expose newly added model $modelId",
121+
({ modelId, contextWindow, inputPrice, outputPrice, cacheReadsPrice }) => {
122+
expect(fireworksModels[modelId]).toBeDefined()
123+
const info = fireworksModels[modelId]
124+
expect(info.maxTokens).toBeGreaterThan(0)
125+
expect(info.contextWindow).toBe(contextWindow)
126+
expect(info.inputPrice).toBe(inputPrice)
127+
expect(info.outputPrice).toBe(outputPrice)
128+
expect(info.cacheReadsPrice).toBe(cacheReadsPrice)
129+
expect(info.description).toBeTruthy()
130+
131+
const handlerWithModel = new FireworksHandler({
132+
apiModelId: modelId,
133+
fireworksApiKey: "test-fireworks-api-key",
134+
})
135+
expect(handlerWithModel.getModel().id).toBe(modelId)
136+
},
137+
)
117138

118139
it("should return Kimi K2 Instruct model with correct configuration", () => {
119140
const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct"
@@ -465,7 +486,7 @@ describe("FireworksHandler", () => {
465486
stream: true,
466487
stream_options: { include_usage: true },
467488
}),
468-
undefined,
489+
expect.any(Object),
469490
)
470491
})
471492

@@ -491,7 +512,7 @@ describe("FireworksHandler", () => {
491512
expect.objectContaining({
492513
temperature: 0.5,
493514
}),
494-
undefined,
515+
expect.any(Object),
495516
)
496517
})
497518

@@ -518,7 +539,7 @@ describe("FireworksHandler", () => {
518539
expect.objectContaining({
519540
temperature: 1.0,
520541
}),
521-
undefined,
542+
expect.any(Object),
522543
)
523544
})
524545

@@ -546,7 +567,7 @@ describe("FireworksHandler", () => {
546567
expect.objectContaining({
547568
temperature: 0.7,
548569
}),
549-
undefined,
570+
expect.any(Object),
550571
)
551572
})
552573

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ describe("LmStudioHandler Native Tools", () => {
8181
}),
8282
]),
8383
}),
84+
expect.any(Object),
8485
)
8586
// parallel_tool_calls should be true by default when not explicitly set
8687
const callArgs = mockCreate.mock.calls[0][0]
@@ -107,6 +108,7 @@ describe("LmStudioHandler Native Tools", () => {
107108
expect.objectContaining({
108109
tool_choice: "auto",
109110
}),
111+
expect.any(Object),
110112
)
111113
})
112114

@@ -219,6 +221,7 @@ describe("LmStudioHandler Native Tools", () => {
219221
expect.objectContaining({
220222
parallel_tool_calls: true,
221223
}),
224+
expect.any(Object),
222225
)
223226
})
224227

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ describe("MimoHandler", () => {
374374
expect.objectContaining({
375375
extra_body: { thinking: { type: "enabled" } },
376376
}),
377+
expect.any(Object),
377378
)
378379
})
379380

0 commit comments

Comments
 (0)