Skip to content

feat(api): add abort signal support to all completePrompt#780

Open
easonLiangWorldedtech wants to merge 9 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-signal-core/complete-prompt-abort-pr
Open

feat(api): add abort signal support to all completePrompt#780
easonLiangWorldedtech wants to merge 9 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-signal-core/complete-prompt-abort-pr

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Completes #615 — Core plumbing for abort signal in completePrompt path. Adds CompletePromptOptions interface with abortSignal and timeoutMs, updates SingleCompletionHandler.completePrompt() to accept optional metadata, and wires it through all 30+ provider implementations.

Closes #615 (part of #404)
Depends on: #674 (already merged ✅)

Changes at a Glance

Metric Value
Files changed 62 files (+2,645 / -180 lines)
Test files added/modified 32 spec files
Provider impls updated 27 providers + core interfaces
New test file complete-prompt-options.spec.ts

Acceptance Criteria (#615)

#615 Criterion Status Notes
abortSignal in metadata (ApiHandlerCreateMessageMetadata) Already merged via #674
SingleCompletionHandler.completePrompt accepts optional metadata New CompletePromptOptions interface
Task.ts wires currentRequestAbortController.signal into metadata Already merged via #674
single-completion-handler.ts forwards options to provider Added options?: CompletePromptOptions param

Files Changed

Core Interface Changes (3 files)

  • src/api/index.ts — New CompletePromptOptions interface, completePrompt(prompt, metadata?) signature
  • src/utils/single-completion-handler.ts — Accepts and forwards options to provider
  • src/core/task/__tests__/Task.spec.ts — Strengthened assertions: signal identity checks instead of toBeInstanceOf(AbortSignal), new test for fresh AbortController per request

Provider Implementations (27 files)

All providers updated in completePrompt() path to forward options.abortSignal:

Direct pass-through (assign requestOptions.signal = options.abortSignal):
anthropic, anthropic-vertex, deepseek, lite-llm, lm-studio, minimax, mistral, openai, openrouter, poe, qwen-code, requesty, unbound, vercel-ai-gateway, xai, zoo-gateway

SDK-specific patterns:

  • bedrock: AbortSignal.any([localController.signal, options.abortSignal]) — merges with internal controller
  • openai-codex: AbortSignal.any([localAbortController.signal, options.abortSignal]) + local controller fallback
  • openai-native: Merged signal linked to abort controller lifecycle
  • gemini: Sets config.abortSignal (not httpOptions.signal) for AI SDK compatibility
  • native-ollama: Accepts param but notes Ollama doesn't support abort at transport level

Base class:

  • base-openai-compatible-provider: Forward to requestOptions.signal

Test Files (32 files)

Each provider test file includes:

  • Signal abort — verifies abort signal is correctly passed through
  • Backward compatible — works without options (no breaking changes)

New test file complete-prompt-options.spec.ts covers the interface contract.

Review Guide

Quick Checklist for Reviewers

  1. Provider signal forwarding patterns consistent?
  2. Backward compatibility verified (all providers work without options)?
  3. SDK-specific quirks handled correctly (gemini config.abortSignal, bedrock merge)?
  4. Task.spec.ts assertions correct (signal identity vs instanceof)?

Focus Areas (highest impact)

  • 🔍 src/api/index.ts — New interface definition, review for completeness
  • 🔍 bedrock.ts, openai-codex.ts, openai-native.ts — Signal merge logic is most complex
  • 🔍 Task.spec.ts — Strengthened assertions from toBeInstanceOf(AbortSignal) to signal identity checks

What to Skip (low risk)

  • Boilerplate changes in providers with minimal diffs (1-2 lines each, standard pattern)
  • Test file additions follow consistent pattern across all providers

Summary by CodeRabbit

  • New Features
    • Prompt completion now supports an optional second argument (CompletePromptOptions) with per-call abortSignal and timeoutMs, enabling cancellation and timeouts across supported providers.
  • Bug Fixes
    • Improved consistency of how providers forward these options (including correct timeoutMs = 0 handling and backward compatibility when options are omitted).
  • Tests
    • Expanded and updated completion tests across multiple providers (and core task behavior) to verify signal/timeout propagation, call signatures, and edge cases.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5540359e-366f-4ee0-bf27-77ca0f768b92

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab7156 and 490b502.

📒 Files selected for processing (1)
  • src/api/providers/__tests__/requesty.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api/providers/tests/requesty.spec.ts

📝 Walkthrough

Walkthrough

This PR adds CompletePromptOptions with abortSignal and timeoutMs, updates the shared completion contract and helper forwarding, and threads those options through provider implementations and tests. Several providers now pass cancellation and timeout controls to their underlying SDK calls.

Changes

CompletePromptOptions feature

Layer / File(s) Summary
Core contract and helper wiring
src/api/index.ts, src/utils/single-completion-handler.ts, src/utils/__tests__/enhance-prompt.spec.ts, src/core/task/__tests__/Task.spec.ts, src/api/providers/__tests__/complete-prompt-options.spec.ts
Adds CompletePromptOptions, updates the shared completePrompt signature, forwards options through the helper, and tightens abort-signal assertions.
OpenAI-family handlers and tests
src/api/providers/openai.ts, src/api/providers/openai-native.ts, src/api/providers/openai-codex.ts, src/api/providers/openai-compatible.ts, src/api/providers/base-openai-compatible-provider.ts, src/api/providers/xai.ts, src/api/providers/__tests__/openai*.spec.ts, src/api/providers/__tests__/openai-native.spec.ts, src/api/providers/__tests__/openai-codex*.spec.ts, src/api/providers/__tests__/openai-compatible.spec.ts, src/api/providers/__tests__/xai.spec.ts, src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
Threads abort and timeout controls into OpenAI-style request paths and adds coverage for signal merging, timeout handling, and response parsing.
Provider-specific cancellation paths
src/api/providers/anthropic.ts, src/api/providers/anthropic-vertex.ts, src/api/providers/minimax.ts, src/api/providers/gemini.ts, src/api/providers/bedrock.ts, src/api/providers/poe.ts, src/api/providers/vscode-lm.ts, src/api/providers/native-ollama.ts, src/api/providers/opencode-go.ts, src/api/providers/__tests__/anthropic*.spec.ts, src/api/providers/__tests__/gemini*.spec.ts, src/api/providers/__tests__/vertex.spec.ts, src/api/providers/__tests__/bedrock.spec.ts, src/api/providers/__tests__/poe.spec.ts, src/api/providers/__tests__/vscode-lm.spec.ts, src/api/providers/__tests__/native-ollama.spec.ts, src/api/providers/__tests__/opencode-go.spec.ts
Forwards cancellation and timeout controls into provider-specific request and cleanup APIs, including merge behavior and abort handling.
SDK-backed third-party providers
src/api/providers/lite-llm.ts, src/api/providers/lm-studio.ts, src/api/providers/mistral.ts, src/api/providers/unbound.ts, src/api/providers/requesty.ts, src/api/providers/openrouter.ts, src/api/providers/vercel-ai-gateway.ts, src/api/providers/qwen-code.ts, src/api/providers/zoo-gateway.ts, src/api/providers/fake-ai.ts, src/api/providers/sambanova.ts, src/api/providers/deepseek.ts, src/api/providers/fireworks.ts, src/api/providers/mimo.ts, src/api/providers/moonshot.ts, src/api/providers/zai.ts, src/api/providers/__tests__/*
Extends the remaining SDK-backed providers and lightweight handlers to accept CompletePromptOptions and forward abort/timeout settings, with provider-specific tests.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: taltas, hannesrudolph, navedmerchant, edelauna, JamesRobert20

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the repo template and omits required sections like Test Procedure and the checklist. Add the template sections: Related GitHub Issue, Description, Test Procedure, Pre-Submission Checklist, Documentation Updates, Additional Notes, and Get in Touch.
Out of Scope Changes check ⚠️ Warning Large parts of the PR add timeoutMs plumbing and broad provider/test changes, which are beyond #615's abort-signal-only scope. Split timeoutMs and wider provider updates into a separate PR or link the issue that explicitly requests them.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the main change: adding abort-signal support to completePrompt.
Linked Issues check ✅ Passed The PR covers the core plumbing required by #615 and adds the requested signal-identity and fresh-controller tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/providers/zoo-gateway.ts (1)

279-306: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unnecessary as any casts and inconsistent timeoutMs handling vs. sibling providers.

options is already typed as CompletePromptOptions (with timeoutMs?: number), so the as any casts at Line 302-303 are unnecessary and bypass type checking. More importantly, the added > 0 guard means timeoutMs: 0 is silently dropped here, whereas lite-llm.ts and opencode-go.ts forward timeoutMs unconditionally whenever it's not undefined. This is a hidden behavioral divergence across providers implementing the same CompletePromptOptions contract.

🔧 Suggested fix to align with sibling providers
 			// Build request options with abortSignal and/or timeout
 			const createOptions: OpenAI.RequestOptions = {}
 			if (options?.abortSignal) {
 				createOptions.signal = options.abortSignal
 			}
-			if ((options as any)?.timeoutMs !== undefined && (options as any).timeoutMs > 0) {
-				createOptions.timeout = (options as any).timeoutMs
+			if (options?.timeoutMs !== undefined) {
+				createOptions.timeout = options.timeoutMs
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/zoo-gateway.ts` around lines 279 - 306, The
`completePrompt` implementation in `ZooGatewayProvider` is using unnecessary `as
any` casts for `options.timeoutMs` and also diverges from sibling providers by
skipping `timeoutMs: 0`. Update the `createOptions` building logic to use the
typed `CompletePromptOptions` directly, read `timeoutMs` without casts, and
forward it whenever it is not `undefined`, matching the behavior in
`lite-llm.ts` and `opencode-go.ts`.
🧹 Nitpick comments (12)
src/api/providers/__tests__/gemini-handler.spec.ts (1)

58-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test title says "via httpOptions" but asserts config.abortSignal.

The test name is misleading — it doesn't verify anything about httpOptions; it verifies abortSignal is passed on config.abortSignal directly.

✏️ Suggested title fix
-	it("completePrompt should pass abort signal through to client via httpOptions", async () => {
+	it("completePrompt should pass abort signal through to client via config.abortSignal", async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/gemini-handler.spec.ts` around lines 58 - 79,
Update the GeminiHandler test name to match what it actually asserts: it
currently checks that completePrompt forwards the abort signal to
client.models.generateContent via config.abortSignal, not through httpOptions.
Rename the test in gemini-handler.spec.ts to reflect abortSignal propagation on
config, and keep the assertion aligned with the existing
GeminiHandler.completePrompt call path.
src/api/providers/gemini.ts (1)

587-593: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

httpOpts typed as Record<string, any> loses type safety.

Unlike the httpOptions literal used in createMessage (Line 300), this is built as a loosely-typed Record<string, any>, which won't catch typos in property names (e.g. timeout vs timeouts) at compile time.

♻️ Suggested typed alternative
-			const httpOpts: Record<string, any> = {}
+			const httpOpts: NonNullable<GenerateContentConfig["httpOptions"]> = {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/gemini.ts` around lines 587 - 593, `httpOpts` in the Gemini
provider is too loosely typed as `Record<string, any>`, so property-name
mistakes won’t be caught. Update the `httpOpts` construction in
`createMessage`/the Gemini request setup to use a typed object shape consistent
with the `httpOptions` literal pattern used elsewhere, and keep the `timeout`
and `baseUrl` assignments type-checked. Use the `httpOpts` variable and the
surrounding `options?.timeoutMs` / `this.options.googleGeminiBaseUrl` logic to
locate and replace the weak typing.
src/api/providers/__tests__/vscode-lm.spec.ts (2)

757-780: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant test with weaker assertion than the existing abort-signal test.

This duplicates "should cancel token when signal is already aborted" (Lines 651-675) but only asserts sendRequest was called instead of verifying cancel() on the CancellationTokenSource, providing no additional coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/vscode-lm.spec.ts` around lines 757 - 780, This
test in vscode-lm.spec.ts duplicates the existing “should cancel token when
signal is already aborted” case without adding coverage. Either remove the
redundant “should cancel token immediately when signal is already aborted” test,
or update it to assert the CancellationTokenSource cancel() behavior through
completePrompt and the handler’s abort handling instead of only checking
mockLanguageModelChat.sendRequest. Use the existing abort-related tests around
completePrompt and sendRequest as the reference point for locating the
duplicated logic.

699-744: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Timeout/combined-signal tests only assert sendRequest was called, not actual cancellation behavior.

Both "should handle timeoutMs by creating a timeout-based cancellation" and "should handle both signal and timeoutMs together" only check that sendRequest was invoked — they don't verify the CancellationTokenSource is actually wired to the timeout, or that cancellation propagates correctly (unlike the earlier dispose()/cancel() assertions at Lines 646-649 and 673-675). As per coding guidelines, spec files should validate state transitions, not just that a call happened.

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js} should "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/vscode-lm.spec.ts` around lines 699 - 744, The
timeout-related specs in completePrompt only verify that sendRequest was called,
so they do not prove cancellation wiring works. Update the two tests to assert
the CancellationTokenSource behavior created for timeoutMs and combined
abortSignal/timeoutMs cases, mirroring the earlier dispose/cancel assertions;
specifically verify the token source is created, disposed, and that cancellation
propagates when the timeout or external signal is triggered. Keep the checks
focused on state transitions and cancellation effects rather than only call
presence.

Source: Coding guidelines

src/api/providers/__tests__/bedrock.spec.ts (1)

1629-1679: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the timeoutMs: 0 + abortSignal case.

None of these tests exercise completePrompt("...", { abortSignal, timeoutMs: 0 }), which is the combination that triggers the immediate-abort bug flagged in bedrock.ts (Lines 841-875). Also, unlike the analogous test in poe.spec.ts ("should prefer signal over timeoutMs when both are provided"), the merge test here doesn't assert sendOptions.abortSignal !== controller.signal to confirm a genuinely merged signal is produced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/bedrock.spec.ts` around lines 1629 - 1679, Add
regression coverage in AwsBedrockHandler.completePrompt for the timeoutMs: 0
plus abortSignal case, since that combination currently triggers the
immediate-abort path. Update the existing merge test to verify the composed
sendOptions.abortSignal is not the same object as the caller’s
controller.signal, and add a new test that calls completePrompt with both
abortSignal and timeoutMs: 0 to confirm the request still proceeds with a merged
signal. Use AwsBedrockHandler, completePrompt, and the mockSend/sendOptions
assertions to locate the right test block.
src/api/providers/poe.ts (1)

141-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Solid implementation — correctly handles timeoutMs: 0 and cleans up the timer in finally.

Unlike the Bedrock implementation, this correctly guards the merge branch with timeoutMs > 0 (Line 151) so timeoutMs: 0 never triggers an immediate abort, and it hoists timeoutId to the function scope with a finally cleanup (Lines 143, 172-175), avoiding the dangling-timer issue seen elsewhere.

This merge-abort-signal-with-timeout logic is now duplicated nearly verbatim across poe.ts, bedrock.ts, vscode-lm.ts, and other providers in this PR stack, with each reimplementation subtly diverging (e.g. the Bedrock bug noted separately). Consider extracting a shared mergeAbortSignal(abortSignal?, timeoutMs?): AbortSignal | undefined utility so all providers share one correct, tested implementation instead of re-deriving it per file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/poe.ts` around lines 141 - 176, The abort-signal/timeout
merge logic in completePrompt is duplicated across providers and has already
diverged in behavior, so extract it into a shared mergeAbortSignal helper and
use it from PoeProvider.completePrompt (and the other provider implementations).
Keep the current correct behavior in poe.ts, but move the controller/timeout
handling into the shared utility so all callers reuse one tested implementation
instead of maintaining separate copies.
src/api/providers/__tests__/lite-llm.spec.ts (1)

1185-1193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Signal-forwarding assertion doesn't verify identity.

expect.objectContaining({ signal: controller.signal }) inside toHaveBeenCalledWith performs deep/structural equality, not reference equality. Vitest's docs note toEqual-style matching recursively compares every field of an object or element of an array, ignoring object identity. Since a fresh AbortSignal has no distinguishing own enumerable properties, this assertion could pass even if a different (but similarly unaborted) signal were forwarded instead of controller.signal. Given the PR's own acceptance criteria calls for toBe-based identity checks, consider asserting on the captured call args directly.

♻️ Suggested stronger identity assertion
-			await handler.completePrompt("test prompt", { abortSignal: controller.signal })
-			expect(mockCreate).toHaveBeenCalledWith(
-				expect.objectContaining({ model: expect.any(String) }),
-				expect.objectContaining({ signal: controller.signal }),
-			)
+			await handler.completePrompt("test prompt", { abortSignal: controller.signal })
+			const [, callOptions] = mockCreate.mock.calls[0]
+			expect(callOptions.signal).toBe(controller.signal)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/lite-llm.spec.ts` around lines 1185 - 1193, The
abort-signal test in handler.completePrompt is only doing structural matching,
so it may pass without proving the exact AbortSignal instance was forwarded.
Update the assertion in lite-llm.spec.ts to inspect the captured mockCreate call
arguments directly and verify the signal by identity with the controller.signal
reference, using the existing completePrompt and mockCreate test setup.
src/api/providers/opencode-go.ts (1)

549-558: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inconsistent "no options" handling vs. the Anthropic branch above.

createOptions || undefined is dead code — createOptions is always a truthy {} object, so this branch always passes at least {} to create(). This differs from the Anthropic branch (Line 517) just above, which correctly passes undefined when no options were supplied via Object.keys(requestOptions).length > 0 ? requestOptions : undefined. Aligning both branches would make the abort/timeout forwarding behavior consistent within this handler.

♻️ Suggested fix for consistency
 			const createOptions: OpenAI.RequestOptions = {}
 			if (options?.abortSignal) {
 				createOptions.signal = options.abortSignal
 			}
 			if (options?.timeoutMs !== undefined) {
 				createOptions.timeout = options.timeoutMs
 			}

-			const response = await this.client.chat.completions.create(requestOptions, createOptions || undefined)
+			const response = await this.client.chat.completions.create(
+				requestOptions,
+				Object.keys(createOptions).length > 0 ? createOptions : undefined,
+			)

Note: this would require updating the corresponding "backward compatible" assertions in opencode-go.spec.ts (Lines 662-666, 717-721) that currently expect {} instead of undefined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/opencode-go.ts` around lines 549 - 558, The OpenAI path in
opencode-go.ts always passes an empty options object because createOptions is
never undefined, so the `createOptions || undefined` fallback is dead code.
Update the `chat.completions.create` call in the handler to mirror the Anthropic
branch’s “no options” behavior by passing undefined when no
abortSignal/timeoutMs were provided, using the `createOptions` assembly around
the `createOptions` variable and `this.client.chat.completions.create` call.
Also adjust the related backward-compatible expectations in opencode-go.spec.ts
that currently assert an empty object instead of undefined.
src/api/providers/fake-ai.ts (1)

83-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unnecessary as any cast now that FakeAI.completePrompt matches the signature.

FakeAI (line 36) already declares completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string>, so this.ai.completePrompt(prompt, options) should type-check directly. The as any cast bypasses type safety for no remaining benefit.

♻️ Suggested cleanup
 	completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
-		return (this.ai as any).completePrompt(prompt, options)
+		return this.ai.completePrompt(prompt, options)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/fake-ai.ts` around lines 83 - 85, The
`FakeAI.completePrompt` implementation still uses an unnecessary `as any` cast
even though `FakeAI` already matches the `completePrompt(prompt, options)`
signature. Update the `completePrompt` method in `FakeAI` to call
`this.ai.completePrompt(prompt, options)` directly, removing the cast so the
existing type definition is enforced.
src/api/providers/qwen-code.ts (1)

341-353: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type fetchOptions as OpenAI.RequestOptions instead of Record<string, unknown> + as any.

Every sibling provider updated in this cohort (openrouter.ts, vercel-ai-gateway.ts, xai.ts) types this object as OpenAI.RequestOptions and passes it without an as any cast. Here the loose typing plus cast bypasses compile-time checking of signal/timeout field names against the SDK's actual RequestOptions shape.

♻️ Proposed fix
-		const fetchOptions: Record<string, unknown> = {}
+		const fetchOptions: OpenAI.RequestOptions = {}

 		if (options?.abortSignal) {
 			fetchOptions.signal = options.abortSignal
 		}

 		if (options?.timeoutMs !== undefined) {
 			fetchOptions.timeout = options.timeoutMs
 		}

 		const response = await this.callApiWithRetry(() =>
-			client.chat.completions.create(requestOptions, fetchOptions as any),
+			client.chat.completions.create(requestOptions, fetchOptions),
 		)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/qwen-code.ts` around lines 341 - 353, The request options
in qwen-code.ts are too loosely typed and the `as any` cast bypasses SDK
validation. Update the `fetchOptions` object in the
`callApiWithRetry`/`client.chat.completions.create` path to use
`OpenAI.RequestOptions` like the sibling providers, and pass it directly without
casting. Keep the existing `options.abortSignal` and `options.timeoutMs`
mapping, but ensure the property names match the `RequestOptions` shape so
TypeScript checks them.
src/api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts (1)

121-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Weaker assertion than sibling provider tests — use exact signal identity.

This test uses expect.any(AbortSignal) for the signal option instead of asserting identity against controller.signal. Sibling provider tests in this same cohort (anthropic, anthropic-vertex, minimax) assert the exact instance via toBe(controller.signal) / literal object equality, consistent with the PR's stated acceptance criteria of verifying signal identity rather than just type. As written, this test would not catch a regression where the signal is wrapped/cloned before being passed to the client.

🧪 Suggested tightening
 			await handler.completePrompt("test prompt", { abortSignal: controller.signal, timeoutMs: 5000 })
 			expect(mockCreate).toHaveBeenCalledWith(
 				expect.objectContaining({ model: "test-model" }),
-				expect.objectContaining({ signal: expect.any(AbortSignal), timeout: 5000 }),
+				expect.objectContaining({ signal: controller.signal, timeout: 5000 }),
 			)

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js} should "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling," which for request construction should verify exact values, not just types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts`
around lines 121 - 134, The timeout-forwarding test in
TestOpenAiCompatibleProvider is too weak because it only checks that signal is
an AbortSignal type. Tighten the assertion in completePrompt’s client call to
verify the exact abortSignal instance from the local AbortController (matching
the sibling provider tests) while still checking timeout: 5000, so regressions
that clone or wrap the signal are caught.

Source: Coding guidelines

src/api/providers/openai-compatible.ts (1)

200-253: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage included for this merge logic in this batch.

This is the only provider in the current cohort whose completePrompt abort/timeout merge logic (branching on both/either of abortSignal/timeoutMs, plus the finally cleanup) has no accompanying spec update in the reviewed files. Given the branching complexity here (and the edge-case bug flagged separately), dedicated tests for each combination (signal-only, timeout-only, both, negative timeout) would help catch regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/openai-compatible.ts` around lines 200 - 253, Add spec
coverage for the abort/timeout merge logic in completePrompt on
OpenAICompatibleProvider, since this is the only provider without tests for
these branches. Update the relevant test file to cover the completePrompt method
with cases for abortSignal only, timeoutMs only, both together, and
negative/zero timeout behavior, and verify the merged signal path plus the
timeout cleanup in the finally block. Use the unique symbols completePrompt,
generateOptions.abortSignal, and the timeoutId cleanup path to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/providers/__tests__/fireworks.spec.ts`:
- Around line 612-639: Update the fireworks provider tests in the completePrompt
cases so they assert the exact AbortSignal instance is forwarded, not just an
object containing a signal field. In the abort-signal and merged signal/timeout
tests, locate handler.completePrompt and mockCreate, then verify the second
argument’s signal with strict identity against controller.signal using the
recorded mock call instead of expect.objectContaining, while keeping the timeout
assertion unchanged.

In `@src/api/providers/__tests__/lmstudio-native-tools.spec.ts`:
- Around line 396-407: The test in completePrompt is too loose because
objectContaining on the options object can still allow a different AbortSignal
instance. Update the assertion in lmstudio-native-tools.spec.ts to inspect the
second argument passed to mockCreate and verify its signal with strict identity
against controller.signal, using the completePrompt and mockCreate call site as
the reference points.

In `@src/api/providers/__tests__/mistral.spec.ts`:
- Around line 546-554: The test name in `mistral.spec.ts` contradicts what
`handler.completePrompt` is asserting: the case with `timeoutMs: 0` should
verify that zero is forwarded, not omitted. Update the `it(...)` description to
match the existing expectation and the `completePrompt` behavior, keeping the
assertion unchanged so the test clearly reflects that `timeoutMs=0` is passed
through.

In `@src/api/providers/__tests__/openai-native.spec.ts`:
- Around line 248-313: The OpenAI native spec contains duplicate and
near-duplicate test coverage in the same describe block. Consolidate the
repeated “should work without options (backward compatible)” cases and merge the
two signal-forwarding assertions into one test each so the behavior is covered
once per scenario. Update the relevant `handler.completePrompt` and
`mockResponsesCreate` tests in `openai-native.spec.ts` to remove redundancy
while preserving both backward-compatibility and abort-signal coverage.
- Around line 315-330: The completePrompt spec in openai-native.spec.ts only
checks that an AbortSignal exists, so it does not prove timeoutMs is wired into
request construction. Strengthen the assertions around handler.completePrompt
and mockResponsesCreate to verify the signal’s behavior or identity when
timeoutMs is passed, such as confirming it aborts after the timeout or differs
from a signal built from abortSignal alone. Update both affected tests to use
meaningful expectations that would fail if completePrompt ignored timeoutMs.

In `@src/api/providers/__tests__/zai.spec.ts`:
- Around line 430-438: The completePrompt test in zai.spec.ts should assert the
abort signal by identity rather than relying on objectContaining. Update the
expectation around handler.completePrompt and mockCreate so you inspect
mockCreate.mock.calls[0][1].signal and verify it is exactly controller.signal
with toBe, ensuring the signal passed through from completePrompt is the same
AbortSignal instance.

In `@src/api/providers/bedrock.ts`:
- Around line 841-875: Fix the request-signal merge logic in the Bedrock send
path so it matches the rest of the providers: in the `sendOptions` IIFE inside
`completePrompt`, treat `timeoutMs <= 0` as “no timeout” even when
`options.abortSignal` is present, and only create/merge a timeout signal when
`timeoutMs > 0`. Also hoist the timeout handle out of the inline abort listener
and ensure it is cleared on normal completion in the `await
this.client.send(command, sendOptions)` flow, mirroring the cleanup pattern used
in `poe.ts` and `vscode-lm.ts`.

In `@src/api/providers/native-ollama.ts`:
- Around line 350-352: The completePrompt path in native-ollama.ts ignores
caller abort/timeout options, so requests cannot be cancelled or bounded. Update
completePrompt to honor CompletePromptOptions by creating a per-request
cancellation path for the Ollama chat/generate call, since the shared
client.abort() is client-scoped; use a separate client instance or equivalent
request-scoped cancellation handling tied to the options passed into
completePrompt.

In `@src/api/providers/openai-codex.ts`:
- Around line 1157-1189: In completePrompt, the abort wiring misses the case
where options.abortSignal is already aborted before the listener is attached, so
the local controller never gets canceled. Update the request-local abort setup
to check options.abortSignal.aborted immediately (or otherwise propagate from
options.abortSignal directly) before creating the merged AbortSignal.any
listener, and ensure localAbortController is aborted and timeoutId cleared in
that path as well.

In `@src/api/providers/openai-compatible.ts`:
- Around line 235-243: In the OpenAI-compatible provider’s timeout handling
block, negative timeoutMs values are currently skipped when abortSignal is not
provided, leaving generateOptions.abortSignal unset. Update the same logic that
handles options.timeoutMs so any timeoutMs less than or equal to 0 creates an
immediately aborted signal, matching the existing behavior used when both
timeoutMs and abortSignal are supplied; keep the positive case using
AbortSignal.timeout(options.timeoutMs).

In `@src/api/providers/requesty.ts`:
- Around line 219-229: The `RequestyProvider` in `requesty.ts` handles
`timeoutMs` differently from the other provider implementations by rejecting
`0`, which makes the shared `CompletePromptOptions` contract inconsistent.
Update the `createOptions` setup in the completion flow so the `timeout` field
is forwarded whenever `options.timeoutMs` is defined, not only when it is
greater than zero, matching the behavior used in the Mistral and Unbound
provider code paths.

---

Outside diff comments:
In `@src/api/providers/zoo-gateway.ts`:
- Around line 279-306: The `completePrompt` implementation in
`ZooGatewayProvider` is using unnecessary `as any` casts for `options.timeoutMs`
and also diverges from sibling providers by skipping `timeoutMs: 0`. Update the
`createOptions` building logic to use the typed `CompletePromptOptions`
directly, read `timeoutMs` without casts, and forward it whenever it is not
`undefined`, matching the behavior in `lite-llm.ts` and `opencode-go.ts`.

---

Nitpick comments:
In `@src/api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts`:
- Around line 121-134: The timeout-forwarding test in
TestOpenAiCompatibleProvider is too weak because it only checks that signal is
an AbortSignal type. Tighten the assertion in completePrompt’s client call to
verify the exact abortSignal instance from the local AbortController (matching
the sibling provider tests) while still checking timeout: 5000, so regressions
that clone or wrap the signal are caught.

In `@src/api/providers/__tests__/bedrock.spec.ts`:
- Around line 1629-1679: Add regression coverage in
AwsBedrockHandler.completePrompt for the timeoutMs: 0 plus abortSignal case,
since that combination currently triggers the immediate-abort path. Update the
existing merge test to verify the composed sendOptions.abortSignal is not the
same object as the caller’s controller.signal, and add a new test that calls
completePrompt with both abortSignal and timeoutMs: 0 to confirm the request
still proceeds with a merged signal. Use AwsBedrockHandler, completePrompt, and
the mockSend/sendOptions assertions to locate the right test block.

In `@src/api/providers/__tests__/gemini-handler.spec.ts`:
- Around line 58-79: Update the GeminiHandler test name to match what it
actually asserts: it currently checks that completePrompt forwards the abort
signal to client.models.generateContent via config.abortSignal, not through
httpOptions. Rename the test in gemini-handler.spec.ts to reflect abortSignal
propagation on config, and keep the assertion aligned with the existing
GeminiHandler.completePrompt call path.

In `@src/api/providers/__tests__/lite-llm.spec.ts`:
- Around line 1185-1193: The abort-signal test in handler.completePrompt is only
doing structural matching, so it may pass without proving the exact AbortSignal
instance was forwarded. Update the assertion in lite-llm.spec.ts to inspect the
captured mockCreate call arguments directly and verify the signal by identity
with the controller.signal reference, using the existing completePrompt and
mockCreate test setup.

In `@src/api/providers/__tests__/vscode-lm.spec.ts`:
- Around line 757-780: This test in vscode-lm.spec.ts duplicates the existing
“should cancel token when signal is already aborted” case without adding
coverage. Either remove the redundant “should cancel token immediately when
signal is already aborted” test, or update it to assert the
CancellationTokenSource cancel() behavior through completePrompt and the
handler’s abort handling instead of only checking
mockLanguageModelChat.sendRequest. Use the existing abort-related tests around
completePrompt and sendRequest as the reference point for locating the
duplicated logic.
- Around line 699-744: The timeout-related specs in completePrompt only verify
that sendRequest was called, so they do not prove cancellation wiring works.
Update the two tests to assert the CancellationTokenSource behavior created for
timeoutMs and combined abortSignal/timeoutMs cases, mirroring the earlier
dispose/cancel assertions; specifically verify the token source is created,
disposed, and that cancellation propagates when the timeout or external signal
is triggered. Keep the checks focused on state transitions and cancellation
effects rather than only call presence.

In `@src/api/providers/fake-ai.ts`:
- Around line 83-85: The `FakeAI.completePrompt` implementation still uses an
unnecessary `as any` cast even though `FakeAI` already matches the
`completePrompt(prompt, options)` signature. Update the `completePrompt` method
in `FakeAI` to call `this.ai.completePrompt(prompt, options)` directly, removing
the cast so the existing type definition is enforced.

In `@src/api/providers/gemini.ts`:
- Around line 587-593: `httpOpts` in the Gemini provider is too loosely typed as
`Record<string, any>`, so property-name mistakes won’t be caught. Update the
`httpOpts` construction in `createMessage`/the Gemini request setup to use a
typed object shape consistent with the `httpOptions` literal pattern used
elsewhere, and keep the `timeout` and `baseUrl` assignments type-checked. Use
the `httpOpts` variable and the surrounding `options?.timeoutMs` /
`this.options.googleGeminiBaseUrl` logic to locate and replace the weak typing.

In `@src/api/providers/openai-compatible.ts`:
- Around line 200-253: Add spec coverage for the abort/timeout merge logic in
completePrompt on OpenAICompatibleProvider, since this is the only provider
without tests for these branches. Update the relevant test file to cover the
completePrompt method with cases for abortSignal only, timeoutMs only, both
together, and negative/zero timeout behavior, and verify the merged signal path
plus the timeout cleanup in the finally block. Use the unique symbols
completePrompt, generateOptions.abortSignal, and the timeoutId cleanup path to
locate the code.

In `@src/api/providers/opencode-go.ts`:
- Around line 549-558: The OpenAI path in opencode-go.ts always passes an empty
options object because createOptions is never undefined, so the `createOptions
|| undefined` fallback is dead code. Update the `chat.completions.create` call
in the handler to mirror the Anthropic branch’s “no options” behavior by passing
undefined when no abortSignal/timeoutMs were provided, using the `createOptions`
assembly around the `createOptions` variable and
`this.client.chat.completions.create` call. Also adjust the related
backward-compatible expectations in opencode-go.spec.ts that currently assert an
empty object instead of undefined.

In `@src/api/providers/poe.ts`:
- Around line 141-176: The abort-signal/timeout merge logic in completePrompt is
duplicated across providers and has already diverged in behavior, so extract it
into a shared mergeAbortSignal helper and use it from PoeProvider.completePrompt
(and the other provider implementations). Keep the current correct behavior in
poe.ts, but move the controller/timeout handling into the shared utility so all
callers reuse one tested implementation instead of maintaining separate copies.

In `@src/api/providers/qwen-code.ts`:
- Around line 341-353: The request options in qwen-code.ts are too loosely typed
and the `as any` cast bypasses SDK validation. Update the `fetchOptions` object
in the `callApiWithRetry`/`client.chat.completions.create` path to use
`OpenAI.RequestOptions` like the sibling providers, and pass it directly without
casting. Keep the existing `options.abortSignal` and `options.timeoutMs`
mapping, but ensure the property names match the `RequestOptions` shape so
TypeScript checks them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bf76c5e-965a-49a6-9547-478202ddd3e0

📥 Commits

Reviewing files that changed from the base of the PR and between 211d360 and 7d9aa64.

📒 Files selected for processing (62)
  • src/api/index.ts
  • src/api/providers/__tests__/anthropic-vertex.spec.ts
  • src/api/providers/__tests__/anthropic.spec.ts
  • src/api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts
  • src/api/providers/__tests__/bedrock.spec.ts
  • src/api/providers/__tests__/complete-prompt-options.spec.ts
  • src/api/providers/__tests__/deepseek.spec.ts
  • src/api/providers/__tests__/fireworks.spec.ts
  • src/api/providers/__tests__/gemini-handler.spec.ts
  • src/api/providers/__tests__/gemini.spec.ts
  • src/api/providers/__tests__/lite-llm.spec.ts
  • src/api/providers/__tests__/lmstudio-native-tools.spec.ts
  • src/api/providers/__tests__/lmstudio.spec.ts
  • src/api/providers/__tests__/mimo.spec.ts
  • src/api/providers/__tests__/minimax.spec.ts
  • src/api/providers/__tests__/mistral.spec.ts
  • src/api/providers/__tests__/moonshot.spec.ts
  • src/api/providers/__tests__/native-ollama.spec.ts
  • src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
  • src/api/providers/__tests__/openai-native.spec.ts
  • src/api/providers/__tests__/openai.spec.ts
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/openrouter.spec.ts
  • src/api/providers/__tests__/poe.spec.ts
  • src/api/providers/__tests__/qwen-code-native-tools.spec.ts
  • src/api/providers/__tests__/requesty.spec.ts
  • src/api/providers/__tests__/sambanova.spec.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  • src/api/providers/__tests__/vertex.spec.ts
  • src/api/providers/__tests__/vscode-lm.spec.ts
  • src/api/providers/__tests__/xai.spec.ts
  • src/api/providers/__tests__/zai.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/anthropic-vertex.ts
  • src/api/providers/anthropic.ts
  • src/api/providers/base-openai-compatible-provider.ts
  • src/api/providers/bedrock.ts
  • src/api/providers/fake-ai.ts
  • src/api/providers/gemini.ts
  • src/api/providers/lite-llm.ts
  • src/api/providers/lm-studio.ts
  • src/api/providers/minimax.ts
  • src/api/providers/mistral.ts
  • src/api/providers/native-ollama.ts
  • src/api/providers/openai-codex.ts
  • src/api/providers/openai-compatible.ts
  • src/api/providers/openai-native.ts
  • src/api/providers/openai.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/openrouter.ts
  • src/api/providers/poe.ts
  • src/api/providers/qwen-code.ts
  • src/api/providers/requesty.ts
  • src/api/providers/unbound.ts
  • src/api/providers/vercel-ai-gateway.ts
  • src/api/providers/vscode-lm.ts
  • src/api/providers/xai.ts
  • src/api/providers/zoo-gateway.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/utils/__tests__/enhance-prompt.spec.ts
  • src/utils/single-completion-handler.ts

Comment thread src/api/providers/__tests__/fireworks.spec.ts
Comment thread src/api/providers/__tests__/lmstudio-native-tools.spec.ts
Comment thread src/api/providers/__tests__/mistral.spec.ts Outdated
Comment thread src/api/providers/__tests__/openai-native.spec.ts
Comment on lines +315 to +330
it("completePrompt should pass timeoutMs through to client", async () => {
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [{ type: "output_text", text: "response" }],
},
],
})

await handler.completePrompt("Test prompt", { timeoutMs: 5000 })
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.objectContaining({ model: expect.any(String) }),
expect.objectContaining({ signal: expect.any(AbortSignal) }),
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Assertions are too weak to verify timeoutMs is actually used.

Both tests only assert signal: expect.any(AbortSignal), which is true even when timeoutMs is never read by the implementation (as is currently the case — see the companion issue raised on openai-native.ts). Strengthen these to assert on signal identity/behavior (e.g., that the signal aborts after the timeout, or that it differs from a signal produced with abortSignal-only) so a regression like the current bug is actually caught. As per coding guidelines, spec files should validate request construction with meaningful assertions.

Also applies to: 332-348

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/openai-native.spec.ts` around lines 315 - 330,
The completePrompt spec in openai-native.spec.ts only checks that an AbortSignal
exists, so it does not prove timeoutMs is wired into request construction.
Strengthen the assertions around handler.completePrompt and mockResponsesCreate
to verify the signal’s behavior or identity when timeoutMs is passed, such as
confirming it aborts after the timeout or differs from a signal built from
abortSignal alone. Update both affected tests to use meaningful expectations
that would fail if completePrompt ignored timeoutMs.

Source: Coding guidelines

Comment thread src/api/providers/bedrock.ts Outdated
Comment thread src/api/providers/native-ollama.ts Outdated
Comment thread src/api/providers/openai-codex.ts
Comment thread src/api/providers/openai-compatible.ts
Comment thread src/api/providers/requesty.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/api/providers/__tests__/openai-compatible.spec.ts (1)

146-178: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Near-duplicate timeoutMs: 0 tests.

"should pass timeoutMs=0 as valid value" (146-161) and "should handle timeoutMs=0 without abortSignal" (163-178) exercise the identical setup and only differ in that the latter adds a signal check. Consider consolidating into a single test with both assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/openai-compatible.spec.ts` around lines 146 -
178, The two `timeoutMs: 0` cases in `openai-compatible.spec.ts` are
near-duplicates, so consolidate them into one test. Keep the shared setup around
`TestOpenAiCompatibleProvider` and `mockCreate`, then assert both that
`completePrompt("test prompt", { timeoutMs: 0 })` passes `timeout: 0` to
`client.chat.completions.create` and that the second call argument has no
`signal` set. This should replace the separate `should pass timeoutMs=0 as valid
value` and `should handle timeoutMs=0 without abortSignal` tests with a single,
clearer test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/providers/__tests__/openai-codex.spec.ts`:
- Around line 233-297: The abort-signal merge tests in OpenAiCodexHandler are
too weak because they only check that fetchOptions.signal is defined. Strengthen
the tests in openai-codex.spec.ts by exercising the actual merged signal
behavior: after calling completePrompt with an external AbortController and/or
timeoutMs, trigger the external abort and assert the fetch request signal
becomes aborted, proving the AbortSignal.any merge and listener wiring in
OpenAiCodexHandler are working rather than just creating a local controller.

In `@src/api/providers/__tests__/openai-compatible.spec.ts`:
- Around line 50-66: The OpenAI-compatible provider tests are only matching the
signal shape, not proving the exact AbortSignal instance is forwarded. Update
the assertions in the `TestOpenAiCompatibleProvider` completePrompt tests to
capture the arguments passed to `client.chat.completions.create` and verify the
forwarded `signal` by identity with `toBe(controller.signal)`, including the
`signal + timeout` case below, so the test confirms the same AbortController
signal is passed through unchanged.

---

Nitpick comments:
In `@src/api/providers/__tests__/openai-compatible.spec.ts`:
- Around line 146-178: The two `timeoutMs: 0` cases in
`openai-compatible.spec.ts` are near-duplicates, so consolidate them into one
test. Keep the shared setup around `TestOpenAiCompatibleProvider` and
`mockCreate`, then assert both that `completePrompt("test prompt", { timeoutMs:
0 })` passes `timeout: 0` to `client.chat.completions.create` and that the
second call argument has no `signal` set. This should replace the separate
`should pass timeoutMs=0 as valid value` and `should handle timeoutMs=0 without
abortSignal` tests with a single, clearer test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 239454f2-b40a-4a31-b4ef-fe2c5b6bb0ce

📥 Commits

Reviewing files that changed from the base of the PR and between 7d9aa64 and 82c67c5.

📒 Files selected for processing (3)
  • src/api/providers/__tests__/bedrock.spec.ts
  • src/api/providers/__tests__/openai-codex.spec.ts
  • src/api/providers/__tests__/openai-compatible.spec.ts

Comment thread src/api/providers/__tests__/openai-codex.spec.ts
Comment thread src/api/providers/__tests__/openai-compatible.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/providers/openai-codex.ts (1)

1162-1172: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat non-positive timeoutMs as “no timeout”, not “abort now”.

AwsBedrockHandler.completePrompt() in this PR only arms a timeout when timeoutMs > 0, but this branch aborts the Codex request immediately for 0 or negative values. That makes the shared CompletePromptOptions contract provider-specific and breaks callers that use 0 to disable the timeout.

Proposed fix
-		if (options?.timeoutMs !== undefined || options?.abortSignal) {
+		if ((options?.timeoutMs !== undefined && options.timeoutMs > 0) || options?.abortSignal) {
 			localAbortController = new AbortController()
 
 			// Handle timeout first
-			if (options.timeoutMs !== undefined) {
-				if (options.timeoutMs > 0) {
-					timeoutId = setTimeout(() => localAbortController?.abort(), options.timeoutMs)
-				} else {
-					// timeoutMs is 0 or negative, abort immediately
-					localAbortController.abort()
-				}
+			if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
+				timeoutId = setTimeout(() => localAbortController?.abort(), options.timeoutMs)
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/openai-codex.ts` around lines 1162 - 1172, Treat
non-positive timeoutMs as disabling the timeout rather than aborting
immediately. In AwsBedrockHandler.completePrompt() / the timeout handling branch
in openai-codex.ts, keep creating the AbortController for an explicit
abortSignal, but only schedule setTimeout when timeoutMs > 0 and skip any abort
action for 0 or negative values. Make sure the shared CompletePromptOptions
behavior stays consistent across providers and does not call
localAbortController.abort() for non-positive timeoutMs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/providers/bedrock.ts`:
- Around line 843-858: The merged abort listener in bedrock.ts’s request setup
is never removed, so shared caller signals can accumulate closures across
repeated completePrompt() calls. Update the abort-signal merge logic in the
sendOptions block to retain a reference to the listener, and in the request
cleanup/finally path remove that listener in addition to clearing
mergeTimeoutId. Use the unique symbols completePrompt, sendOptions, and
mergeTimeoutId to locate the merge-and-cleanup flow.

In `@src/api/providers/openai-codex.ts`:
- Around line 1181-1189: The per-request timeout and abort listener in
completePrompt() are not fully cleaned up after a successful request, which can
accumulate across reused task signals. Move the timer and listener cleanup into
the function’s finally path so timeoutId is always cleared and the abort handler
registered on options.abortSignal is always removed, including the related abort
wiring in the other completePrompt() flow. Use the existing
localAbortController, timeoutId, and options.abortSignal references to locate
the cleanup points.

---

Outside diff comments:
In `@src/api/providers/openai-codex.ts`:
- Around line 1162-1172: Treat non-positive timeoutMs as disabling the timeout
rather than aborting immediately. In AwsBedrockHandler.completePrompt() / the
timeout handling branch in openai-codex.ts, keep creating the AbortController
for an explicit abortSignal, but only schedule setTimeout when timeoutMs > 0 and
skip any abort action for 0 or negative values. Make sure the shared
CompletePromptOptions behavior stays consistent across providers and does not
call localAbortController.abort() for non-positive timeoutMs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98114667-4764-467b-a4b4-8be2f4d175b1

📥 Commits

Reviewing files that changed from the base of the PR and between 82c67c5 and d46cb9e.

📒 Files selected for processing (11)
  • src/api/providers/__tests__/fireworks.spec.ts
  • src/api/providers/__tests__/lmstudio-native-tools.spec.ts
  • src/api/providers/__tests__/mistral.spec.ts
  • src/api/providers/__tests__/openai-codex.spec.ts
  • src/api/providers/__tests__/openai-compatible.spec.ts
  • src/api/providers/__tests__/openai-native.spec.ts
  • src/api/providers/__tests__/zai.spec.ts
  • src/api/providers/bedrock.ts
  • src/api/providers/openai-codex.ts
  • src/api/providers/openai-compatible.ts
  • src/api/providers/requesty.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/api/providers/tests/zai.spec.ts
  • src/api/providers/tests/lmstudio-native-tools.spec.ts
  • src/api/providers/tests/mistral.spec.ts
  • src/api/providers/tests/fireworks.spec.ts
  • src/api/providers/openai-compatible.ts
  • src/api/providers/requesty.ts
  • src/api/providers/tests/openai-codex.spec.ts
  • src/api/providers/tests/openai-compatible.spec.ts

Comment thread src/api/providers/bedrock.ts Outdated
Comment thread src/api/providers/openai-codex.ts Outdated
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 1, 2026
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the feat/abort-signal-core/complete-prompt-abort-pr branch from 71c9508 to 8d06710 Compare July 1, 2026 07:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/providers/__tests__/native-ollama.spec.ts`:
- Around line 373-420: The abort assertions in native-ollama.spec.ts are too
global and can pass even if cancellation hits the shared client instead of the
request-local one. Update the Ollama constructor mock to return a per-instance
abort spy and keep the chat mock tied to that instance, then assert inside the
completePrompt timeout and abortSignal cases that the instance-specific abort
was called. Use the existing handler.completePrompt, mockChat, and mockAbort
setup as the entry point, but switch the expectations to the fresh client
created for each request.
- Around line 422-443: The test in native-ollama.spec.ts is only verifying the
timeout delay, so it does not prove the finally-block cleanup in completePrompt.
Update the test around handler.completePrompt and the setTimeout/clearTimeout
spies to capture the returned timer handle and assert that clearTimeout is
called with that exact handle after a successful completion. Keep the existing
timeout setup in place, but tighten the assertion so the cleanup behavior is
validated directly.

In `@src/api/providers/native-ollama.ts`:
- Around line 353-367: Use an isolated Ollama client for every cancellation path
in native-ollama.ts, not just when abortSignal is set. Update the client
selection in the request flow around fetchModel()/timeoutMs so timeout handling
also uses a per-request Ollama instance instead of this.ensureClient(). Make
sure the local client is created with the same host and auth headers as
ensureClient() so authenticated Ollama endpoints continue to work, and keep
client.abort() scoped to that request only.
- Around line 371-384: The abort handling in native-ollama’s request flow still
allows execution to continue into `client.chat()` after
`options.abortSignal.aborted` is already true. In the method that sets up the
request and registers the abort listener, throw an abort error immediately when
the signal is pre-aborted, before constructing or sending the chat request, and
keep the listener cleanup/removal in the existing `finally` block by
unregistering the `onAbort` callback there.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a02b2fe-e66c-4603-ba86-33765ad106f3

📥 Commits

Reviewing files that changed from the base of the PR and between d46cb9e and 71c9508.

📒 Files selected for processing (6)
  • src/api/providers/__tests__/native-ollama.spec.ts
  • src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
  • src/api/providers/__tests__/openai-native.spec.ts
  • src/api/providers/bedrock.ts
  • src/api/providers/native-ollama.ts
  • src/api/providers/openai-codex.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/api/providers/tests/openai-codex-native-tool-calls.spec.ts
  • src/api/providers/tests/openai-native.spec.ts
  • src/api/providers/bedrock.ts
  • src/api/providers/openai-codex.ts

Comment thread src/api/providers/__tests__/native-ollama.spec.ts
Comment thread src/api/providers/__tests__/native-ollama.spec.ts
Comment on lines +353 to +367
const hasAbortSignal = options?.abortSignal !== undefined
let localClient: Ollama | undefined
let timeoutId: ReturnType<typeof setTimeout> | undefined

try {
const client = this.ensureClient()
// Use local client if abortSignal is provided (per-request isolation)
const client = hasAbortSignal
? (localClient ??= new Ollama({ host: this.options.ollamaBaseUrl }))
: this.ensureClient()
const { id: modelId } = await this.fetchModel()
const useR1Format = modelId.toLowerCase().includes("deepseek-r1")

// Handle timeoutMs if provided
if (options?.timeoutMs !== undefined && options.timeoutMs > 0) {
timeoutId = setTimeout(() => client.abort(), options.timeoutMs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file structure first
ast-grep outline src/api/providers/native-ollama.ts --view expanded

# Read the relevant section around the reported lines
sed -n '300,430p' src/api/providers/native-ollama.ts | cat -n

# Find the shared client setup
rg -n "ensureClient|ollamaApiKey|ollamaBaseUrl|new Ollama|abortSignal|timeoutMs" src/api/providers/native-ollama.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 6698


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '161,180p' src/api/providers/native-ollama.ts | cat -n
sed -n '350,380p' src/api/providers/native-ollama.ts | cat -n

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2374


Use an isolated Ollama client for all cancellation paths.

timeoutMs still goes through this.ensureClient(), so client.abort() can cancel unrelated requests sharing the provider client. The local client path also needs the same auth headers as ensureClient(), otherwise authenticated Ollama endpoints will fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/native-ollama.ts` around lines 353 - 367, Use an isolated
Ollama client for every cancellation path in native-ollama.ts, not just when
abortSignal is set. Update the client selection in the request flow around
fetchModel()/timeoutMs so timeout handling also uses a per-request Ollama
instance instead of this.ensureClient(). Make sure the local client is created
with the same host and auth headers as ensureClient() so authenticated Ollama
endpoints continue to work, and keep client.abort() scoped to that request only.

Comment thread src/api/providers/native-ollama.ts
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

@easonLiangWorldedtech easonLiangWorldedtech force-pushed the feat/abort-signal-core/complete-prompt-abort-pr branch from ba77fe2 to 8ab7156 Compare July 2, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(api): abort signal core plumbing — interface, Task.ts, single-completion-handler

2 participants