Skip to content

feat(api): pass through abort signal to all providers#730

Draft
easonLiangWorldedtech wants to merge 16 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:origin/feat/abort-signal-core/pass-through-providers
Draft

feat(api): pass through abort signal to all providers#730
easonLiangWorldedtech wants to merge 16 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:origin/feat/abort-signal-core/pass-through-providers

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Passes metadata?.abortSignal through to API calls in 19 provider implementations, so clicking Stop actually cancels the underlying HTTP request.

Closes #616 (part of #404, depends on #615)

What Changed

Provider signal pass-through

Each provider now forwards metadata?.abortSignal in both createMessage (streaming) and completePrompt (non-streaming) paths:

Provider Pattern
openai, anthropic, deepseek, lm-studio, lite-llm, mimo, minimax, mistral, poe, qwen-code, requesty, unbound, vercel-ai-gateway, xai, zai, zoo-gateway, opencode-go, openrouter { signal: metadata?.abortSignal } (OpenAI SDK) or { abortSignal: metadata?.abortSignal } (AI SDK)
native-ollama, openai-compatible Minimal 1-line additions

Supporting changes

  • Unified completePrompt to use metadata.abortSignal consistently across all providers
  • Handle pre-aborted secondary signals in mergeAbortSignals() without unnecessary controller allocation
  • Exported isAuthScopedProvider and writeModels from modelCache for test access
  • Strengthened Task-level abort signal identity assertions with sequential-request tests

Stats

Metric Value
Files changed 72 (+4882 / -250)
Providers modified 19 implementations + base classes
Test files added/modified ~35 spec files

Closes #616

…nfiguration

Body: Implement generic request configuration builder with chainable methods (addAbortSignal, addHeaders, setOption), static factory methods (fromMetadata, mergeAbortSignals), and 40 unit tests.
…ls early-abort

- Fix README TOC: change #how-mergesignals-works to
  #how-mergeabortsignals-works to match the actual heading anchor
- Simplify mergeAbortSignals: return primarySignal directly when it's
  already aborted instead of creating a new AbortController
…onfigBuilder (Zoo-Code-Org#615)

- Add default empty object parameter to addHeaders() so calling with
  undefined no longer throws TypeError from Object.keys(undefined)
- Reorder mergeAbortSignals to check primarySignal.aborted before
  allocating AbortController, preventing unnecessary controller creation
…roviders

- anthropic.spec.ts: timeout trigger test + signal instance verification

- native-ollama.spec.ts: explicit assertion no second arg passed (no signal forwarding)

- openai-codex-native-tool-calls.spec.ts: removed weak toHaveProperty(signal) assertion

- vercel-ai-gateway.spec.ts: temperature test uses correct undefined second arg

fix: add timeoutMs forwarding to completePrompt methods

- vercel-ai-gateway.ts: use Object.keys(createOptions).length > 0 check instead of truthy check

- poe.ts: merge signal and timeoutMs properly with combined abort logic

- config-builder/README.md: update documentation for mergeAbortSignals behavior

test: add timeoutMs coverage for poe, moonshot, minimax, mistral, xai providers

- poe.spec.ts: signal+timeoutMs merge, timeoutMs only, timeoutMs=0 cases

- moonshot.spec.ts: same timeoutMs tests for openai-compatible pattern

- minimax.spec.ts: signal+timeoutMs, timeoutMs only, truthy check behavior

- mistral.spec.ts: same timeoutMs coverage

- xai.spec.ts: signal+timeoutMs, timeoutMs only, truthy check behavior

test: add timeoutMs coverage for anthropic-vertex, base-openai-compatible, bedrock, openai-native

- anthropic-vertex.spec.ts: signal passing test (no timeoutMs support)

- base-openai-compatible-provider-timeout.spec.ts: completePrompt with signal+timeoutMs, timeoutMs only, truthy check behavior

- bedrock.spec.ts: timeoutMs coverage for adaptive thinking path

- openai-native.spec.ts: signal and timeoutMs merging tests

test: add signal+timeoutMs merge tests for fireworks, lite-llm, lmstudio

- fireworks.spec.ts: added merge signal and timeoutMs together test

- lite-llm.spec.ts: added same combined signal+timeoutMs test

- lmstudio.spec.ts: aligned with same abort signal pattern

fix: replace tautological assertion in poe.spec.ts

- poe.spec.ts: completePrompt should prefer signal over timeoutMs test now asserts abortSignal is a distinct AbortSignal from controller.signal instead of always-true instanceof check

test: add missing error catch and timeout cleanup tests

- vscode-lm.spec.ts: added 'should handle errors in completePrompt' test

- poe.spec.ts: added 'completePrompt should clear timeout when user signal aborts' test

- opencode-go.spec.ts: added OpenAI path completePrompt tests (signal, timeoutMs, merged)
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dc68788-eb8f-4ff8-9596-3ccf1d6c0614

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds abort-signal and timeoutMs plumbing to completion APIs, introduces a shared request-config builder, forwards the new options through provider implementations and helper layers, and expands related tests. A separate model-cache helper export and its tests are also updated.

Changes

Abort and timeout request plumbing

Layer / File(s) Summary
Core contract and builder
src/api/index.ts, src/api/providers/config-builder/*, src/api/providers/index.ts, src/api/providers/__tests__/complete-prompt-options.spec.ts, src/api/providers/__tests__/request-config-builder.spec.ts
CompletePromptOptions and RequestConfigBuilder are added, re-exported, documented, and tested.
Completion dispatch wiring
src/utils/single-completion-handler.ts, src/api/providers/fake-ai.ts, src/core/task/__tests__/Task.spec.ts, src/utils/__tests__/enhance-prompt.spec.ts
singleCompletionHandler forwards options to completePrompt, FakeAIHandler matches the new signature, and tests assert signal identity through task metadata and helper calls.
OpenAI request options
src/api/providers/base-openai-compatible-provider.ts, src/api/providers/openai*.ts, src/api/providers/openai-codex.ts, src/api/providers/__tests__/base-openai-compatible-provider*.spec.ts, src/api/providers/__tests__/openai*.spec.ts, src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
Shared OpenAI-style providers build request options from abortSignal and timeoutMs, including merged signal handling in native and Codex paths, and the tests assert the new call shapes.
Non-OpenAI completion paths
src/api/providers/anthropic*.ts, src/api/providers/bedrock.ts, src/api/providers/gemini.ts, src/api/providers/mistral.ts, src/api/providers/minimax.ts, src/api/providers/native-ollama.ts, src/api/providers/poe.ts, src/api/providers/vscode-lm.ts, src/api/providers/__tests__/{anthropic*,anthropic-vertex*,bedrock*,gemini*,mistral*,minimax*,native-ollama*,moonshot*,poe*,sambanova*,vertex*,vscode-lm*}.spec.ts
Anthropic, Vertex, Bedrock, Gemini, Mistral, MiniMax, Ollama, Poe, VS Code LM, Moonshot, SambaNova, and Vertex tests now cover abort and timeout handling.
OpenAI-compatible wrappers
src/api/providers/{deepseek,lite-llm,lm-studio,mimo,opencode-go,openrouter,requesty,unbound,vercel-ai-gateway,xai,zai,zoo-gateway,qwen-code}.ts, src/api/providers/__tests__/{deepseek*,fireworks*,lite-llm*,lmstudio*,lmstudio-native-tools*,mimo*,opencode-go*,openrouter*,requesty*,unbound*,vercel-ai-gateway*,xai*,zai*,zoo-gateway*,qwen-code-native-tools*}.spec.ts
OpenAI-compatible providers and gateway wrappers now pass signal and timeout options through request calls, with tests updated for the new invocation shapes.

Model cache helper

Layer / File(s) Summary
Model cache export and tests
src/api/providers/fetchers/modelCache.ts, src/api/providers/fetchers/__tests__/modelCache.spec.ts
isAuthScopedProvider is exported, and the cache tests cover the updated filesystem mocks, disk-cache lookup, and writeModels behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Task
  participant SingleCompletionHandler as singleCompletionHandler
  participant BaseOpenAiCompatibleProvider
  participant OpenAIClient as client.chat.completions.create
  Task->>SingleCompletionHandler: promptText + metadata.abortSignal
  SingleCompletionHandler->>BaseOpenAiCompatibleProvider: completePrompt(promptText, options)
  BaseOpenAiCompatibleProvider->>OpenAIClient: request options with signal / timeout
  OpenAIClient-->>BaseOpenAiCompatibleProvider: completion text
Loading

Estimated Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#674: Shares the core abort-signal metadata plumbing and Task signal propagation that this PR extends into completion options.
  • Zoo-Code-Org/Zoo-Code#346: Touches the same Zoo Gateway completePrompt surface that now accepts options and forwards abort/timeout request options.
  • Zoo-Code-Org/Zoo-Code#386: Modifies Bedrock completePrompt request shaping on the same code path that now forwards abort signals.

Suggested labels

awaiting-review

Suggested reviewers

  • taltas
  • JamesRobert20
  • navedmerchant
  • hannesrudolph
  • edelauna

Poem

A rabbit hopped through signal grass, 🐇
With timeout moons that softly pass.
“Abort,” said I, and streams replied,
“Your prompt shall run with hops beside.”
🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers the summary and implementation, but it misses several required template sections like Test Procedure and the checklist. Add the missing template sections: Related GitHub Issue heading, Test Procedure, Pre-Submission Checklist, Documentation Updates, Additional Notes, and Get in Touch.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code updates metadata plumbing, forwards abort signals across the listed providers, and adds identity/fresh-request tests.
Out of Scope Changes check ✅ Passed No clear unrelated changes stand out; the README, builder, and extra tests support the abort-signal plumbing.
Title check ✅ Passed The title is concise and accurately captures the main change: abort-signal pass-through across providers.
✨ 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.

@easonLiangWorldedtech easonLiangWorldedtech force-pushed the origin/feat/abort-signal-core/pass-through-providers branch from 71f57b1 to dcdce75 Compare June 26, 2026 22:26

@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: 7

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/vscode-lm.ts (1)

585-610: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The catch is detached — error wrapping is silently dead code.

completePrompt closes at Line 604 (try { … } finally { … } with no catch). Lines 605-610 are therefore parsed as a separate class method named catch, not a catch clause for this try. Consequently any error from client.sendRequest(...) / stream consumption propagates unwrapped (the VSCode LM completion error: … wrapping never runs), and the class gains an unreachable catch() method.

Fold the handler back into the try so it precedes finally:

🐛 Proposed fix
 			return result
+		} catch (error: any) {
+			if (error instanceof Error) {
+				throw new Error(`VSCode LM completion error: ${error.message}`)
+			}
+			throw error
 		} finally {
 			if (timeoutTimeout) {
 				clearTimeout(timeoutTimeout)
 			}
 			tokenSource.dispose()
 		}
 	}
-	catch(error: any) {
-		if (error instanceof Error) {
-			throw new Error(`VSCode LM completion error: ${error.message}`)
-		}
-		throw error
-	}
 }
🤖 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/vscode-lm.ts` around lines 585 - 610, The error handling in
completePrompt is detached from the try/finally block, so the wrapping logic
never runs and the extra catch() becomes a separate class method. Move the catch
handler back onto the completePrompt flow so it is attached to the try before
finally, and keep the Error wrapping that prefixes "VSCode LM completion error:"
for failures from client.sendRequest and stream consumption.
🧹 Nitpick comments (10)
src/api/providers/fetchers/__tests__/modelCache.spec.ts (1)

474-485: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the writeModels assertion.

Since safeWriteJson is mocked, resolving to undefined is nearly guaranteed and doesn't validate the persistence contract. Consider asserting safeWriteJson was called with the expected <router>_models.json filename under the cache dir and with mockModels, which is the behavior this test should protect.

♻️ Suggested stronger assertion
-		await expect(writeModels("openrouter", mockModels)).resolves.toBeUndefined()
+		await expect(writeModels("openrouter", mockModels)).resolves.toBeUndefined()
+		expect(safeWriteJson).toHaveBeenCalledWith(
+			expect.stringContaining("openrouter_models.json"),
+			mockModels,
+		)
🤖 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/fetchers/__tests__/modelCache.spec.ts` around lines 474 -
485, The current `writeModels` test only checks that `writeModels` resolves,
which is too weak because `safeWriteJson` is mocked. Update the `writeModels`
spec in `modelCache.spec.ts` to assert the persistence contract by verifying
`safeWriteJson` is called with the expected `<router>_models.json` path under
the cache directory and the same `mockModels` object. Use the existing
`writeModels` and `safeWriteJson` symbols to locate the behavior being tested.
src/api/providers/__tests__/openai-native.spec.ts (1)

268-280: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate test case. The block at Lines 301-313 is an exact copy of Lines 268-280 (same title and assertions). Remove one, or repurpose the second to cover a distinct scenario.

Also applies to: 301-313

🤖 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 268 - 280,
The test in openai-native.spec.ts is duplicated with the same title and
assertions as another case in the same suite, so remove one copy or change the
second block to cover a different scenario. Update the duplicate test around
handler.completePrompt("Test prompt") and mockResponsesCreate.mockResolvedValue
so the suite keeps only one backward-compatible coverage case and the other test
validates a distinct behavior.
src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts (1)

568-590: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate test title. Both tests are named "completePrompt should work without options (backward compatible)" though they assert different things (POST method vs return value). Rename one for clarity in test output.

Also applies to: 592-611

🤖 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-codex-native-tool-calls.spec.ts` around
lines 568 - 590, There is a duplicate test title in the `completePrompt` specs,
which makes the test output ambiguous because two different cases share the same
name. Rename the test in the `completePrompt` block around
`openai-codex-native-tool-calls.spec.ts` to a distinct, descriptive title that
reflects the behavior it checks, and keep the other `completePrompt` test name
aligned with its specific assertion so the suite output clearly identifies each
case.
src/api/providers/config-builder/request-config-builder.ts (1)

141-148: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace the manual merge with AbortSignal.any(). The current listeners keep the non-aborting signal attached until it aborts, which can retain the merged controller/closure on long-lived parents. Node 20.20.2 already supports AbortSignal.any([primarySignal, secondarySignal]); update the tests that assert toBe(primaryController.signal) since any() returns a fresh 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/config-builder/request-config-builder.ts` around lines 141
- 148, The manual merge logic in request-config-builder’s signal combining path
should be replaced with AbortSignal.any([primarySignal, secondarySignal])
instead of wiring abort listeners on a new AbortController. Update the merge
branch in the signal-handling function to return the combined signal from
AbortSignal.any, and adjust any tests that currently expect the returned value
to be the same object as primaryController.signal since any() produces a new
signal instance.
src/api/providers/fake-ai.ts (1)

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

as any cast is now redundant.

Since the FakeAI interface declares completePrompt(prompt, options?) (Line 31), this.ai is correctly typed and the as any can be dropped for full type safety:

♻️ Optional refactor
-	completePrompt(prompt: string, options?: import("../index").CompletePromptOptions): Promise<string> {
-		return (this.ai as any).completePrompt(prompt, options)
+	completePrompt(prompt: string, options?: CompletePromptOptions): Promise<string> {
+		return this.ai.completePrompt(prompt, options)
 	}

(Add CompletePromptOptions to the existing import from ../index and reuse it on Line 31 as well.)

🤖 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 78 - 79, The
`FakeAI.completePrompt` implementation no longer needs the `as any` cast because
`this.ai` is already typed by the `FakeAI` interface. Remove the cast in
`completePrompt` and call the method directly for type safety, and if you touch
the signature, import and reuse `CompletePromptOptions` from `../index` so the
interface and implementation stay aligned.
src/api/providers/config-builder/README.md (1)

200-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc/implementation branch ordering differs (functionally equivalent).

The documented mergeAbortSignals checks secondarySignal.aborted before primarySignal.aborted, whereas the actual implementation checks the both-aborted and primary-aborted cases first. The resulting behavior is identical, but keeping the snippet byte-for-byte aligned with request-config-builder.ts avoids future confusion when readers diff the two.

🤖 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/config-builder/README.md` around lines 200 - 230, The
documented mergeAbortSignals example is out of order compared with the actual
implementation in request-config-builder.ts, even though the behavior is the
same. Update the README snippet for mergeAbortSignals so its branch ordering
matches the real static method exactly, including the primarySignal and
secondarySignal aborted checks and the both-aborted case, to keep the docs
aligned with the code.
src/api/providers/lm-studio.ts (1)

206-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: createOptions || undefined is a no-op.

createOptions is initialized to {} and only mutated, so it is always truthy; the || undefined fallback can never trigger. Passing createOptions directly is clearer (and matches the existing tests asserting {}).

🤖 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/lm-studio.ts` around lines 206 - 216, Remove the
unnecessary `|| undefined` fallback in `LmStudioProvider`’s chat completion
call: `createOptions` in `this.client.chat.completions.create(...)` is always an
object, so pass it directly. Keep the existing `createOptions` construction in
this method and update the call site to use the object as-is so it matches the
current tests and behavior.
src/api/providers/anthropic.ts (1)

153-184: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant inner switch — default branch (Lines 181-182) is unreachable.

This IIFE only runs inside the outer switch (modelId) case block (Lines 92-107), whose model IDs are identical to the inner case labels (Lines 160-175). So every model reaching here matches a case and pushes the caching beta; the default branch can never execute. You can drop the inner switch entirely and build the options inline.

♻️ Simplify
-					const requestOptions = (() => {
-						// prompt caching: https://x.com/alexalbert__/status/1823751995901272068
-						// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
-						// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
-
-						// Then check for models that support prompt caching
-						switch (modelId) {
-							case "claude-sonnet-4-6":
-							...
-							case "claude-3-haiku-20240307":
-								betas.push("prompt-caching-2024-07-31")
-								return {
-									headers: { "anthropic-beta": betas.join(",") },
-									...(metadata?.abortSignal && { signal: metadata.abortSignal }),
-								}
-							default:
-								return metadata?.abortSignal ? { signal: metadata.abortSignal } : undefined
-						}
-					})()
+					// All models in this branch support prompt caching.
+					betas.push("prompt-caching-2024-07-31")
+					const requestOptions = {
+						headers: { "anthropic-beta": betas.join(",") },
+						...(metadata?.abortSignal && { signal: metadata.abortSignal }),
+					}
🤖 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/anthropic.ts` around lines 153 - 184, The inner switch
inside the requestOptions IIFE in anthropic.ts is redundant because it
duplicates the same model IDs already handled by the outer modelId switch,
making the default branch unreachable. Simplify the requestOptions logic by
removing the nested switch in the requestOptions block and building the options
inline from the already-matched model case, while preserving the prompt-caching
beta header and optional abortSignal handling.
src/api/providers/unbound.ts (1)

219-221: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

createOptions || undefined is a no-op.

createOptions is always a (possibly empty) object, so || undefined never triggers and an empty {} is always passed as the second argument. This is functionally harmless but misleading, and inconsistent with vercel-ai-gateway.ts (Line 150), which guards with Object.keys(createOptions).length > 0 ? createOptions : undefined. Consider aligning the two for clarity. Note the existing test at Line 203 currently asserts {}, so it would need updating if you switch to the guarded form.

🤖 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/unbound.ts` around lines 219 - 221, The `createOptions ||
undefined` fallback in `UnboundProvider`’s chat completion call is misleading
because `createOptions` is always an object, so an empty object is still passed.
Update the `this.client.chat.completions.create(...)` call to match the guarded
pattern used in `vercel-ai-gateway.ts` by only passing `createOptions` when it
has keys, and adjust the existing test around the mocked `create` call in this
provider to expect `undefined` instead of `{}` when no options are set.
src/api/providers/lite-llm.ts (1)

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

createOptions || undefined is dead — {} is always truthy.

createOptions is initialized to {} and never reassigned, so createOptions || undefined always evaluates to the object, even when no signal/timeout was set. This forwards an empty options object rather than undefined. It's harmless for the OpenAI SDK, but it's misleading and inconsistent with xai.ts, which uses Object.keys(...).length > 0 ? ... : undefined. Consider aligning for clarity.

♻️ Optional consistency tweak
-			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,
+			)
🤖 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/lite-llm.ts` at line 350, The `createOptions || undefined`
fallback in `LiteLLMProvider` is unnecessary because `createOptions` is always
an object, so the call always passes an empty options object even when no
`signal` or `timeout` is set. Update the `chat.completions.create` call in
`lite-llm.ts` to match the pattern used in `xai.ts` by only passing
`createOptions` when it has keys, otherwise pass `undefined`, keeping the
behavior explicit and consistent.
🤖 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__/gemini.spec.ts`:
- Around line 188-200: The test in `gemini.spec.ts` is misleading because it
claims to verify `timeoutMs` but only asserts the `abortSignal` is passed
through `httpOptions`. Update the `completePrompt` expectation in the `should
pass timeoutMs through to client via httpOptions` test to assert the timeout
value is forwarded as well, or rename the test to match what it actually checks.
Use the `handler.completePrompt` and `client.models.generateContent` assertions
to locate the expectation.

In `@src/api/providers/gemini.ts`:
- Around line 588-597: `CompletePromptOptions.timeoutMs` is not being forwarded
into Gemini request settings, so `httpOptions` only carries `abortSignal` and
`googleGeminiBaseUrl`. Update the `generateContent` path in
`src/api/providers/gemini.ts` to map `timeoutMs` onto `httpOpts.timeout`
alongside `signal` and `baseUrl`, and keep the `GenerateContentConfig` wiring
unchanged. Also update the `gemini.spec.ts` expectation for the Gemini request
options so it asserts the `timeout` value is passed through, not just `signal`.

In `@src/api/providers/mistral.ts`:
- Around line 200-216: The request options passed from the Mistral provider are
using the wrong shape, so abort and timeout settings are not applied. Update the
`chat.complete` call in `mistral.ts` to build options using the Mistral SDK’s
`RequestOptions` format, specifically placing the abort signal under
`fetchOptions.signal` and using `timeoutMs` for the timeout. Keep the change
localized to the request-building logic around `options`, `requestOptions`, and
`this.client.chat.complete` so the SDK receives the expected fields.

In `@src/api/providers/openai-compatible.ts`:
- Around line 212-231: In openai-compatible.ts, the merged abort handling in the
generateOptions setup must handle an already-aborted options.abortSignal
immediately and must clean up the timeout. Update the branch that combines
options.abortSignal with timeoutMs so it checks abortSignal.aborted before
adding the listener and aborts the merged controller right away, then ensure the
timeoutId is cleared after generateText completes or aborts by wrapping the call
path in try/finally. Refer to the generateOptions.abortSignal merge logic and
the generateText invocation when making the fix.

In `@src/api/providers/openai.ts`:
- Around line 321-327: In completePrompt within the OpenAI provider, the
createOptions timeout assignment is using a truthy check that drops an explicit
timeoutMs of 0, causing it to diverge from the base OpenAI-compatible behavior.
Update the conditional around options.timeoutMs so the OpenAI.RequestOptions
timeout is set whenever timeoutMs is provided, including 0, while keeping the
existing abortSignal handling unchanged.

In `@src/api/providers/openrouter.ts`:
- Around line 335-343: The requestOptions logic in openrouter’s provider path is
applying the Anthropic beta header whenever abortSignal is present, which causes
the header to leak onto non-Anthropic models. Update the conditional that builds
requestOptions so the x-anthropic-beta header is added only when modelId starts
with "anthropic/", and keep the abortSignal-only case separate so non-Anthropic
requests receive only the signal. Make the gating consistent with completePrompt
so both paths use the same Anthropic-only header rule.

In `@src/api/providers/poe.ts`:
- Around line 147-167: The combined abortSignal and timeoutMs handling in poe.ts
leaves the timeout timer pending when generateText completes normally. Update
the generateText flow so the timeoutId created alongside the AbortController is
always cleared after the request finishes, ideally in a finally block around the
generateText(generateOptions) call, while keeping the existing abort listener
behavior intact.

---

Outside diff comments:
In `@src/api/providers/vscode-lm.ts`:
- Around line 585-610: The error handling in completePrompt is detached from the
try/finally block, so the wrapping logic never runs and the extra catch()
becomes a separate class method. Move the catch handler back onto the
completePrompt flow so it is attached to the try before finally, and keep the
Error wrapping that prefixes "VSCode LM completion error:" for failures from
client.sendRequest and stream consumption.

---

Nitpick comments:
In `@src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts`:
- Around line 568-590: There is a duplicate test title in the `completePrompt`
specs, which makes the test output ambiguous because two different cases share
the same name. Rename the test in the `completePrompt` block around
`openai-codex-native-tool-calls.spec.ts` to a distinct, descriptive title that
reflects the behavior it checks, and keep the other `completePrompt` test name
aligned with its specific assertion so the suite output clearly identifies each
case.

In `@src/api/providers/__tests__/openai-native.spec.ts`:
- Around line 268-280: The test in openai-native.spec.ts is duplicated with the
same title and assertions as another case in the same suite, so remove one copy
or change the second block to cover a different scenario. Update the duplicate
test around handler.completePrompt("Test prompt") and
mockResponsesCreate.mockResolvedValue so the suite keeps only one
backward-compatible coverage case and the other test validates a distinct
behavior.

In `@src/api/providers/anthropic.ts`:
- Around line 153-184: The inner switch inside the requestOptions IIFE in
anthropic.ts is redundant because it duplicates the same model IDs already
handled by the outer modelId switch, making the default branch unreachable.
Simplify the requestOptions logic by removing the nested switch in the
requestOptions block and building the options inline from the already-matched
model case, while preserving the prompt-caching beta header and optional
abortSignal handling.

In `@src/api/providers/config-builder/README.md`:
- Around line 200-230: The documented mergeAbortSignals example is out of order
compared with the actual implementation in request-config-builder.ts, even
though the behavior is the same. Update the README snippet for mergeAbortSignals
so its branch ordering matches the real static method exactly, including the
primarySignal and secondarySignal aborted checks and the both-aborted case, to
keep the docs aligned with the code.

In `@src/api/providers/config-builder/request-config-builder.ts`:
- Around line 141-148: The manual merge logic in request-config-builder’s signal
combining path should be replaced with AbortSignal.any([primarySignal,
secondarySignal]) instead of wiring abort listeners on a new AbortController.
Update the merge branch in the signal-handling function to return the combined
signal from AbortSignal.any, and adjust any tests that currently expect the
returned value to be the same object as primaryController.signal since any()
produces a new signal instance.

In `@src/api/providers/fake-ai.ts`:
- Around line 78-79: The `FakeAI.completePrompt` implementation no longer needs
the `as any` cast because `this.ai` is already typed by the `FakeAI` interface.
Remove the cast in `completePrompt` and call the method directly for type
safety, and if you touch the signature, import and reuse `CompletePromptOptions`
from `../index` so the interface and implementation stay aligned.

In `@src/api/providers/fetchers/__tests__/modelCache.spec.ts`:
- Around line 474-485: The current `writeModels` test only checks that
`writeModels` resolves, which is too weak because `safeWriteJson` is mocked.
Update the `writeModels` spec in `modelCache.spec.ts` to assert the persistence
contract by verifying `safeWriteJson` is called with the expected
`<router>_models.json` path under the cache directory and the same `mockModels`
object. Use the existing `writeModels` and `safeWriteJson` symbols to locate the
behavior being tested.

In `@src/api/providers/lite-llm.ts`:
- Line 350: The `createOptions || undefined` fallback in `LiteLLMProvider` is
unnecessary because `createOptions` is always an object, so the call always
passes an empty options object even when no `signal` or `timeout` is set. Update
the `chat.completions.create` call in `lite-llm.ts` to match the pattern used in
`xai.ts` by only passing `createOptions` when it has keys, otherwise pass
`undefined`, keeping the behavior explicit and consistent.

In `@src/api/providers/lm-studio.ts`:
- Around line 206-216: Remove the unnecessary `|| undefined` fallback in
`LmStudioProvider`’s chat completion call: `createOptions` in
`this.client.chat.completions.create(...)` is always an object, so pass it
directly. Keep the existing `createOptions` construction in this method and
update the call site to use the object as-is so it matches the current tests and
behavior.

In `@src/api/providers/unbound.ts`:
- Around line 219-221: The `createOptions || undefined` fallback in
`UnboundProvider`’s chat completion call is misleading because `createOptions`
is always an object, so an empty object is still passed. Update the
`this.client.chat.completions.create(...)` call to match the guarded pattern
used in `vercel-ai-gateway.ts` by only passing `createOptions` when it has keys,
and adjust the existing test around the mocked `create` call in this provider to
expect `undefined` instead of `{}` when no options are set.
🪄 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: 2a422b91-add0-4756-b3bf-6d04315d0915

📥 Commits

Reviewing files that changed from the base of the PR and between 6705e67 and 71f57b1.

📒 Files selected for processing (72)
  • 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__/base-openai-compatible-provider.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__/request-config-builder.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/config-builder/README.md
  • src/api/providers/config-builder/request-config-builder.ts
  • src/api/providers/deepseek.ts
  • src/api/providers/fake-ai.ts
  • src/api/providers/fetchers/__tests__/modelCache.spec.ts
  • src/api/providers/fetchers/modelCache.ts
  • src/api/providers/gemini.ts
  • src/api/providers/index.ts
  • src/api/providers/lite-llm.ts
  • src/api/providers/lm-studio.ts
  • src/api/providers/mimo.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/zai.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__/gemini.spec.ts Outdated
Comment thread src/api/providers/gemini.ts
Comment thread src/api/providers/mistral.ts Outdated
Comment thread src/api/providers/openai-compatible.ts Outdated
Comment thread src/api/providers/openai.ts
Comment on lines +335 to +343
const requestOptions: OpenAI.RequestOptions | undefined =
modelId.startsWith("anthropic/") || metadata?.abortSignal
? {
headers: { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" },
...(metadata?.abortSignal && { signal: metadata.abortSignal }),
}
: metadata?.abortSignal
? { signal: metadata.abortSignal }
: undefined

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Anthropic beta header leaks onto non-Anthropic models when abortSignal is set.

The ternary's first branch fires when either modelId.startsWith("anthropic/") or metadata?.abortSignal is truthy. So for a non-Anthropic model with an abort signal, the request gets x-anthropic-beta: fine-grained-tool-streaming-... even though it isn't an Anthropic model. The intended signal-only branch (metadata?.abortSignal ? { signal }) is unreachable in that case. Note completePrompt (Line 605-612) already gates the header on anthropic/ only, so the two paths now disagree.

🐛 Proposed fix to gate the header on Anthropic models only
-		const requestOptions: OpenAI.RequestOptions | undefined =
-			modelId.startsWith("anthropic/") || metadata?.abortSignal
-				? {
-						headers: { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" },
-						...(metadata?.abortSignal && { signal: metadata.abortSignal }),
-					}
-				: metadata?.abortSignal
-					? { signal: metadata.abortSignal }
-					: undefined
+		const requestOptions: OpenAI.RequestOptions | undefined = modelId.startsWith("anthropic/")
+			? {
+					headers: { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" },
+					...(metadata?.abortSignal && { signal: metadata.abortSignal }),
+				}
+			: metadata?.abortSignal
+				? { signal: metadata.abortSignal }
+				: undefined
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requestOptions: OpenAI.RequestOptions | undefined =
modelId.startsWith("anthropic/") || metadata?.abortSignal
? {
headers: { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" },
...(metadata?.abortSignal && { signal: metadata.abortSignal }),
}
: metadata?.abortSignal
? { signal: metadata.abortSignal }
: undefined
const requestOptions: OpenAI.RequestOptions | undefined = modelId.startsWith("anthropic/")
? {
headers: { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" },
...(metadata?.abortSignal && { signal: metadata.abortSignal }),
}
: metadata?.abortSignal
? { signal: metadata.abortSignal }
: 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/openrouter.ts` around lines 335 - 343, The requestOptions
logic in openrouter’s provider path is applying the Anthropic beta header
whenever abortSignal is present, which causes the header to leak onto
non-Anthropic models. Update the conditional that builds requestOptions so the
x-anthropic-beta header is added only when modelId starts with "anthropic/", and
keep the abortSignal-only case separate so non-Anthropic requests receive only
the signal. Make the gating consistent with completePrompt so both paths use the
same Anthropic-only header rule.

Comment thread src/api/providers/poe.ts
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the origin/feat/abort-signal-core/pass-through-providers branch from 551036e to f728333 Compare June 26, 2026 22:44
@easonLiangWorldedtech easonLiangWorldedtech changed the title feat(api): pass through abort signal to all providers + RequestConfigBuilder feat(api): pass through abort signal to all providers Jun 26, 2026
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the origin/feat/abort-signal-core/pass-through-providers branch from f728333 to 75adc30 Compare June 26, 2026 22:52
@easonLiangWorldedtech easonLiangWorldedtech marked this pull request as draft June 27, 2026 00:03
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the origin/feat/abort-signal-core/pass-through-providers branch from 75adc30 to 4c0dd5e Compare June 27, 2026 00:23
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the origin/feat/abort-signal-core/pass-through-providers branch from 4c0dd5e to 5055fe7 Compare June 27, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(api): abort signal pass-through for simple providers

2 participants