Skip to content

Commit dcdce75

Browse files
committed
fix(api-providers): fix timeout handling and abort signal issues in completePrompt
1 parent 6c2154f commit dcdce75

6 files changed

Lines changed: 34 additions & 19 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ describe("LmStudioHandler", () => {
182182
)
183183
})
184184

185+
it("should pass timeoutMs=0 through to client", async () => {
186+
mockCreate.mockResolvedValueOnce({
187+
choices: [{ message: { content: "response" } }],
188+
})
189+
await handler.completePrompt("test prompt", { timeoutMs: 0 })
190+
expect(mockCreate).toHaveBeenCalledWith(
191+
expect.objectContaining({ model: expect.any(String) }),
192+
expect.objectContaining({ timeout: 0 }),
193+
)
194+
})
195+
185196
it("should merge signal and timeoutMs together", async () => {
186197
const controller = new AbortController()
187198
mockCreate.mockResolvedValueOnce({

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,12 +392,16 @@ describe("PoeHandler", () => {
392392
const controller = new AbortController()
393393
mockGenerateText.mockResolvedValueOnce({ text: "response" })
394394

395-
await handler.completePrompt("test prompt", { abortSignal: controller.signal, timeoutMs: 5000 })
396-
// The merged abortSignal should be the same as user signal when only signal is provided
395+
const promise = handler.completePrompt("test prompt", { abortSignal: controller.signal, timeoutMs: 5000 })
397396
const callArgs = mockGenerateText.mock.calls[0][0]
398397
expect(callArgs.abortSignal).toBeDefined()
399-
// User didn't abort, so signal should not be aborted yet
400-
expect(controller.signal.aborted).toBe(false)
398+
399+
// Abort the user signal before resolve
400+
controller.abort()
401+
402+
await promise
403+
// Merged signal should be aborted when user signal aborts
404+
expect(callArgs.abortSignal.aborted).toBe(true)
401405
})
402406

403407
it("completePrompt should handle timeoutMs=0 as no timeout", async () => {

src/api/providers/lm-studio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
204204
if (options?.abortSignal) {
205205
createOptions.signal = options.abortSignal
206206
}
207-
if (options?.timeoutMs) {
207+
if (options?.timeoutMs !== undefined) {
208208
createOptions.timeout = options.timeoutMs
209209
}
210210

src/api/providers/opencode-go.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio
495495
if (options?.abortSignal) {
496496
requestOptions.signal = options.abortSignal
497497
}
498-
if (options?.timeoutMs) {
498+
if (options?.timeoutMs !== undefined) {
499499
requestOptions.timeout = options.timeoutMs
500500
}
501501

@@ -551,7 +551,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio
551551
if (options?.abortSignal) {
552552
createOptions.signal = options.abortSignal
553553
}
554-
if (options?.timeoutMs) {
554+
if (options?.timeoutMs !== undefined) {
555555
createOptions.timeout = options.timeoutMs
556556
}
557557

src/api/providers/poe.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler
136136

137137
async completePrompt(prompt: string, options?: import("../index").CompletePromptOptions): Promise<string> {
138138
const { id } = this.getModel()
139+
let timeoutId: ReturnType<typeof setTimeout> | undefined
139140
try {
140141
const generateOptions: Parameters<typeof generateText>[0] & { abortSignal?: AbortSignal } = {
141142
model: this.poe(id),
@@ -145,17 +146,12 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler
145146
// Merge abortSignal and timeoutMs into a single abortSignal
146147
if (options?.abortSignal && options?.timeoutMs && options.timeoutMs > 0) {
147148
const controller = new AbortController()
148-
const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs)
149-
150-
options.abortSignal.addEventListener(
151-
"abort",
152-
() => {
153-
clearTimeout(timeoutId)
154-
controller.abort()
155-
},
156-
{ once: true },
157-
)
158-
149+
if (options.abortSignal.aborted) {
150+
controller.abort()
151+
} else {
152+
timeoutId = setTimeout(() => controller.abort(), options.timeoutMs)
153+
options.abortSignal.addEventListener("abort", () => controller.abort(), { once: true })
154+
}
159155
generateOptions.abortSignal = controller.signal
160156
} else if (options?.abortSignal) {
161157
generateOptions.abortSignal = options.abortSignal
@@ -169,6 +165,10 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler
169165
const errorMessage = error instanceof Error ? error.message : String(error)
170166
TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "completePrompt"))
171167
throw new Error(`Poe completion error: ${errorMessage}`)
168+
} finally {
169+
if (timeoutId) {
170+
clearTimeout(timeoutId)
171+
}
172172
}
173173
}
174174
}

src/api/providers/qwen-code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
344344
fetchOptions.signal = options.abortSignal
345345
}
346346

347-
if (options?.timeoutMs) {
347+
if (options?.timeoutMs !== undefined) {
348348
fetchOptions.timeout = options.timeoutMs
349349
}
350350

0 commit comments

Comments
 (0)