Skip to content

feat(api): add CompletePromptOptions parameter to completePrompt method - #901

Merged
edelauna merged 5 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-signal-core/complete-prompt
Jul 18, 2026
Merged

edelauna merged 5 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-signal-core/complete-prompt

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #615 — Core plumbing for abort signal in completePrompt path.

This PR adds an optional second parameter (CompletePromptOptions) to all provider completePrompt methods, enabling future abort signal and timeout support without breaking existing callers.

Changes

New: CompletePromptOptions interface

export interface CompletePromptOptions extends Pick<ApiHandlerCreateMessageMetadata, "abortSignal"> {
    /** Optional timeout override (ms) — falls back to provider default if omitted */
    timeoutMs?: number
}

Design note: Rather than passing the full ApiHandlerCreateMessageMetadata as issue #615 described, this PR introduces a minimal interface that only exposes abortSignal and timeoutMs. This keeps the API surface clean while still enabling abort signal pass-through. The metadata pattern used in the stream path (via ApiHandlerCreateMessageMetadata) is already handled separately.

Updated files

  • src/api/index.ts — Added CompletePromptOptions, updated SingleCompletionHandler.completePrompt signature
  • 25+ provider implementations — All now accept options?: CompletePromptOptions (backward compatible, optional)
  • src/utils/single-completion-handler.ts — Forwards options to the handler

Tests (Task.spec.ts)

  • Updated existing assertions from toBeInstanceOf(AbortSignal) → toBe(task.currentRequestAbortController!.signal) for signal identity verification
  • Added new test: "should create a fresh AbortController for each sequential request" confirming controllers are not reused across requests

Acceptance criteria (from #615)

Related

Summary by CodeRabbit

  • New Features

    • Added optional structured controls for non-streaming prompt completion via a shared options parameter, including abortSignal and optional timeoutMs.
    • Updated completePrompt across providers and the single-completion flow to accept and forward these options.
  • Tests

    • Strengthened cancellation assertions to require the exact abortSignal instance and verified retries use a fresh, initially un-aborted signal.
    • Updated prompt enhancement tests to expect completePrompt to be called with an explicit undefined options argument when omitted.
    • Added coverage ensuring the fake provider delegates completePrompt with the correct prompt/options.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

CompletePromptOptions adds optional abort-signal and timeout metadata to completePrompt. The shared completion utility and provider handlers accept the options, while fake-handler forwarding and abort-signal tests are updated.

Changes

Complete prompt options

Layer / File(s) Summary
Completion contract and forwarding
src/api/index.ts, src/utils/single-completion-handler.ts, src/api/providers/fake-ai.ts, src/api/providers/__tests__/fake-ai.spec.ts
Defines CompletePromptOptions and forwards optional options through completion and fake handlers, with delegation tests.
Provider completion signatures
src/api/providers/*.ts
Updates provider imports and completePrompt signatures to accept optional CompletePromptOptions while preserving existing request behavior.
Abort-signal validation
src/core/task/__tests__/Task.spec.ts, src/utils/__tests__/enhance-prompt.spec.ts
Checks exact abort-signal identity, fresh controllers across sequential requests, and the updated completion call shape.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • Issue 616 — Covers related completePrompt abort-signal metadata propagation.
  • Issue 617 — Addresses abort propagation through the same provider completion APIs.

Possibly related PRs

Suggested reviewers: hannesrudolph

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main API change: adding CompletePromptOptions to completePrompt.
Description check ✅ Passed The description links #615, explains the new options type and forwarding, and includes test and acceptance details.
Linked Issues check ✅ Passed The PR covers #615 by adding an optional second completePrompt argument, forwarding it, and adding the requested abort-signal tests.
Out of Scope Changes check ✅ Passed The changes stay centered on completePrompt plumbing and related tests, with no unrelated feature work evident.
✨ 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.

@codecov

codecov Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

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/base-openai-compatible-provider.ts (1)

215-229: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Thread CompletePromptOptions into these completion requests.

abortSignal and timeoutMs are defined on CompletePromptOptions, but every completePrompt here drops options, so caller cancellation and per-call timeouts never reach the SDK request.

  • src/api/providers/base-openai-compatible-provider.ts#L215-L229
  • src/api/providers/opencode-go.ts#L488-L537
  • src/api/providers/openrouter.ts#L577-L606
  • src/api/providers/poe.ts#L141-L147
  • src/api/providers/qwen-code.ts#L330-L341
  • src/api/providers/requesty.ts#L207-L221
  • src/api/providers/unbound.ts#L195-L209
  • src/api/providers/vercel-ai-gateway.ts#L120-L136
🤖 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/base-openai-compatible-provider.ts` around lines 215 - 229,
Thread CompletePromptOptions through each completePrompt implementation so
abortSignal and timeoutMs reach the underlying SDK completion request. Update
the request construction or invocation in
src/api/providers/base-openai-compatible-provider.ts lines 215-229,
src/api/providers/opencode-go.ts lines 488-537, src/api/providers/openrouter.ts
lines 577-606, src/api/providers/poe.ts lines 141-147,
src/api/providers/qwen-code.ts lines 330-341, src/api/providers/requesty.ts
lines 207-221, src/api/providers/unbound.ts lines 195-209, and
src/api/providers/vercel-ai-gateway.ts lines 120-136, reusing each provider’s
existing request options and preserving current completion behavior.
🧹 Nitpick comments (1)
src/core/task/__tests__/Task.spec.ts (1)

2235-2252: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify the mock stream factory.

The custom mock implementation manually overrides the async iterator protocol, but for await loops rely exclusively on the [Symbol.asyncIterator]() method. Since that method returns a native generator, your custom next(), return(), and throw() methods are bypassed as dead code, and callCount is never actually incremented here.

You can safely simplify this to a standard async generator function, which naturally fulfills the AsyncGenerator contract and correctly increments your counter as intended:

♻️ Proposed refactor
-				const mockStreamFactory = () => {
-					return {
-						async *[Symbol.asyncIterator]() {
-							yield { type: "text", text: `response ${callCount}` }
-						},
-						async next() {
-							callCount++
-							return { done: true, value: { type: "text", text: `response ${callCount - 1}` } }
-						},
-						async return() {
-							return { done: true, value: undefined }
-						},
-						async throw(e: any) {
-							throw e
-						},
-						[Symbol.asyncDispose]: async () => {},
-					} as AsyncGenerator<ApiStreamChunk>
-				}
+				const mockStreamFactory = async function* (): AsyncGenerator<ApiStreamChunk> {
+					yield { type: "text", text: `response ${callCount}` }
+					callCount++
+				}
🤖 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/core/task/__tests__/Task.spec.ts` around lines 2235 - 2252, Replace the
custom object returned by mockStreamFactory with a standard async generator
function. Move the callCount increment into the generator execution before
yielding the response, remove the manually defined next, return, throw, and
async iterator members, and preserve the existing response text and
AsyncGenerator<ApiStreamChunk> contract.
🤖 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/gemini.ts`:
- Line 579: All completePrompt implementations accept CompletePromptOptions but
do not propagate cancellation or timeout settings to their requests. Update
src/api/providers/gemini.ts:579 in completePrompt to pass abortSignal to
generateContent; src/api/providers/minimax.ts:292-295 in completePrompt to pass
the signal to client.messages.create; src/api/providers/mistral.ts:195 in
completePrompt to pass abortSignal and timeoutMs to client.chat.complete;
src/api/providers/native-ollama.ts:553-579 in completePrompt to pass abortSignal
to client.chat; src/api/providers/openai-codex.ts:1157 in completePrompt to use
options?.abortSignal for fetch, preserving the existing controller as fallback;
src/api/providers/openai-compatible.ts:200 in completePrompt to pass abortSignal
to generateText; and src/api/providers/openai-native.ts:1487-1585 in
completePrompt to use options?.abortSignal for responses.create instead of
relying solely on the shared controller.

---

Outside diff comments:
In `@src/api/providers/base-openai-compatible-provider.ts`:
- Around line 215-229: Thread CompletePromptOptions through each completePrompt
implementation so abortSignal and timeoutMs reach the underlying SDK completion
request. Update the request construction or invocation in
src/api/providers/base-openai-compatible-provider.ts lines 215-229,
src/api/providers/opencode-go.ts lines 488-537, src/api/providers/openrouter.ts
lines 577-606, src/api/providers/poe.ts lines 141-147,
src/api/providers/qwen-code.ts lines 330-341, src/api/providers/requesty.ts
lines 207-221, src/api/providers/unbound.ts lines 195-209, and
src/api/providers/vercel-ai-gateway.ts lines 120-136, reusing each provider’s
existing request options and preserving current completion behavior.

---

Nitpick comments:
In `@src/core/task/__tests__/Task.spec.ts`:
- Around line 2235-2252: Replace the custom object returned by mockStreamFactory
with a standard async generator function. Move the callCount increment into the
generator execution before yielding the response, remove the manually defined
next, return, throw, and async iterator members, and preserve the existing
response text and AsyncGenerator<ApiStreamChunk> contract.
🪄 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: 098310bc-c298-461e-b612-f08806baff20

📥 Commits

Reviewing files that changed from the base of the PR and between 50801da and 688319b.

📒 Files selected for processing (30)
  • src/api/index.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/kenari.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/gemini.ts Resolved
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 15, 2026
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/abort-signal-core/complete-prompt branch from 37133de to 62ed671 Compare July 16, 2026 12:17

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

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/base-openai-compatible-provider.ts (1)

215-243: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unused options parameter in completePrompt implementations.

Although the options parameter (containing abortSignal and timeoutMs) is now included in the completePrompt signature across all providers, it is never forwarded to the underlying SDK clients (e.g., OpenAI or Anthropic). As a result, network requests will not actually be aborted when the cancellation signal is triggered, defeating the objective of stopping requests after cancellation.

  • src/api/providers/base-openai-compatible-provider.ts#L215-L243: Pass the options to the OpenAI client call: await this.client.chat.completions.create(params, { signal: options?.abortSignal, timeout: options?.timeoutMs }).
  • src/api/providers/opencode-go.ts#L488-L545: Pass the options to both the Anthropic and OpenAI client calls.
  • src/api/providers/requesty.ts#L207-L226: Pass the options to the OpenAI client call.
  • src/api/providers/vercel-ai-gateway.ts#L120-L144: Pass the options to the OpenAI client call.
  • src/api/providers/xai.ts#L145-L163: Pass the options to the Responses API client call.
🤖 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/base-openai-compatible-provider.ts` around lines 215 - 243,
Forward CompletePromptOptions.abortSignal and timeoutMs to every underlying SDK
request: update completePrompt in
src/api/providers/base-openai-compatible-provider.ts (215-243), both Anthropic
and OpenAI calls in src/api/providers/opencode-go.ts (488-545), the OpenAI call
in src/api/providers/requesty.ts (207-226), the OpenAI call in
src/api/providers/vercel-ai-gateway.ts (120-144), and the Responses API call in
src/api/providers/xai.ts (145-163). Use each client’s request options so
cancellation and timeouts are applied.
🤖 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.

Outside diff comments:
In `@src/api/providers/base-openai-compatible-provider.ts`:
- Around line 215-243: Forward CompletePromptOptions.abortSignal and timeoutMs
to every underlying SDK request: update completePrompt in
src/api/providers/base-openai-compatible-provider.ts (215-243), both Anthropic
and OpenAI calls in src/api/providers/opencode-go.ts (488-545), the OpenAI call
in src/api/providers/requesty.ts (207-226), the OpenAI call in
src/api/providers/vercel-ai-gateway.ts (120-144), and the Responses API call in
src/api/providers/xai.ts (145-163). Use each client’s request options so
cancellation and timeouts are applied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e539678-aff9-4a10-a391-e6ceb6abf6a6

📥 Commits

Reviewing files that changed from the base of the PR and between 37133de and 62ed671.

📒 Files selected for processing (31)
  • src/api/index.ts
  • src/api/providers/__tests__/fake-ai.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/kenari.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
🚧 Files skipped from review as they are similar to previous changes (17)
  • src/utils/single-completion-handler.ts
  • src/api/providers/bedrock.ts
  • src/api/providers/qwen-code.ts
  • src/api/providers/vscode-lm.ts
  • src/api/providers/openai-codex.ts
  • src/api/providers/mistral.ts
  • src/api/providers/zoo-gateway.ts
  • src/api/providers/openai.ts
  • src/api/providers/openrouter.ts
  • src/api/providers/minimax.ts
  • src/api/providers/anthropic-vertex.ts
  • src/api/providers/kenari.ts
  • src/api/providers/unbound.ts
  • src/api/providers/openai-compatible.ts
  • src/api/providers/native-ollama.ts
  • src/core/task/tests/Task.spec.ts
  • src/api/providers/openai-native.ts

@edelauna edelauna 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.

to minor comments about consistency, but otherwise looks good.

Comment thread src/api/index.ts Resolved
Comment thread src/api/providers/openai.ts Resolved
easonliang28 and others added 4 commits July 17, 2026 21:37
Add optional second parameter to all provider completePrompt methods for future abort signal and timeout support.

- Add CompletePromptOptions interface with abortSignal and timeoutMs fields
- Update 25+ provider implementations to accept options?: CompletePromptOptions parameter
- Simplify providers to use basic request calls without signal/timeout logic
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/abort-signal-core/complete-prompt branch from 6d67b5c to 98201f4 Compare July 17, 2026 13:37

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

🤖 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/base-openai-compatible-provider.ts`:
- Line 215: Forward the received CompletePromptOptions from completePrompt
through each provider’s SDK request so abortSignal and timeoutMs are applied.
Update the implementations in
src/api/providers/base-openai-compatible-provider.ts:215-215,
src/api/providers/lm-studio.ts:190-190, src/api/providers/minimax.ts:292-292,
and src/api/providers/requesty.ts:207-207; preserve existing request behavior
while passing the options to the underlying completion call.
🪄 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: c572da25-8110-4ac2-a5a7-23dba23165c3

📥 Commits

Reviewing files that changed from the base of the PR and between 62ed671 and 824a196.

📒 Files selected for processing (31)
  • src/api/index.ts
  • src/api/providers/__tests__/fake-ai.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/kenari.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
🚧 Files skipped from review as they are similar to previous changes (27)
  • src/utils/tests/enhance-prompt.spec.ts
  • src/api/providers/kenari.ts
  • src/api/providers/xai.ts
  • src/api/index.ts
  • src/api/providers/openai-compatible.ts
  • src/api/providers/openrouter.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/anthropic-vertex.ts
  • src/api/providers/poe.ts
  • src/api/providers/gemini.ts
  • src/api/providers/lite-llm.ts
  • src/api/providers/qwen-code.ts
  • src/api/providers/tests/fake-ai.spec.ts
  • src/api/providers/openai-codex.ts
  • src/api/providers/anthropic.ts
  • src/api/providers/openai-native.ts
  • src/api/providers/vercel-ai-gateway.ts
  • src/api/providers/unbound.ts
  • src/api/providers/native-ollama.ts
  • src/api/providers/zoo-gateway.ts
  • src/api/providers/mistral.ts
  • src/api/providers/vscode-lm.ts
  • src/api/providers/openai.ts
  • src/utils/single-completion-handler.ts
  • src/core/task/tests/Task.spec.ts
  • src/api/providers/fake-ai.ts
  • src/api/providers/bedrock.ts

@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/abort-signal-core/complete-prompt branch from 824a196 to 73054a4 Compare July 17, 2026 13:50

@edelauna edelauna 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.

Thanks for your contribution and all the reviews you went through.

easonLiangWorldedtech pushed a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Aug 20, 2026
- completePrompt: bridge CompletePromptOptions.abortSignal and timeoutMs into a
  request-local vscode.CancellationTokenSource (the VS Code LanguageModelChat API
  only accepts a CancellationToken, not an AbortSignal); apply timeoutMs only when
  it is a positive value
- completePrompt: report aborted requests (external signal, timeout, or host
  cancellation) as errors with name = "AbortError" on both the error and success
  paths; remove the abort listener and dispose the token source in finally
- createMessage: bridge metadata.abortSignal into the internal request
  CancellationTokenSource (Bedrock pattern: pre-aborted guard + { once: true }
  listener stored in a named const), fail fast with an AbortError when the signal
  is already aborted, surface host CancellationError with name = "AbortError", and
  detach the listener / dispose the source in finally
- vscode-lm.spec.ts: add pre-aborted and mid-flight abort, timeout, listener
  attach/detach, and backward-compatibility tests
- fake-ai.spec.ts: option pass-through tests already merged on main via Zoo-Code-Org#901;
  verified green without changes
easonLiangWorldedtech pushed a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Aug 20, 2026
…ssion tests

Add a fast-fail throwIfAborted guard to the shared abort-signal utilities and regression tests for the CompletePromptOptions interface (added by Zoo-Code-Org#901).
pull Bot pushed a commit to moqimoqidea/Zoo-Code that referenced this pull request Sep 5, 2026
…ssion tests (Zoo-Code-Org#1288)

* feat(api): add throwIfAborted helper and completePrompt options regression tests

Add a fast-fail throwIfAborted guard to the shared abort-signal utilities and regression tests for the CompletePromptOptions interface (added by Zoo-Code-Org#901).

* feat(api): add shared isRequestAborted and createAbortError helpers to abort-signal utils

The OpenAI-family provider PRs (Zoo-Code-Org#1309, Zoo-Code-Org#1311) carry per-provider copies of the same abort-detection helper (isRequestAborted) and the same abort-error constructor (createAbortError); only the provider name in the message differs. Per the CodeRabbit maintainability finding on Zoo-Code-Org#1309 (extract the shared abort helpers into utils/abort-signal.ts), these are now shared in the foundation utility:
- isRequestAborted(error, signal?) - true when the caller signal fired, a native AbortError / OpenAI SDK APIUserAbortError was raised, or the message is exactly "Request was aborted." (exact match; a substring match would misclassify unrelated errors that merely mention aborting)
- createAbortError(providerName) - fresh error with name === "AbortError" and message "The <providerName> request was aborted", satisfying the Task.ts abort contract
- exported OpenAiRequestOptions type
7 new tests (isRequestAborted 4, createAbortError 3).

* test(abort-signal): assert throwIfAborted error message

The mutation-diff gate reports a surviving StringLiteral mutant on the throwIfAborted message literal. Assert the exact message so a blanked literal fails the suite.

---------

Co-authored-by: Eason Liang <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
Co-authored-by: edelauna <[email protected]>
easonLiangWorldedtech pushed a commit to easonLiangWorldedtech/Zoo-Code that referenced this pull request Sep 16, 2026
- createMessage: bridge metadata.abortSignal to a per-request
  AbortController (Bedrock pattern: pre-aborted guard, once-listener,
  detached on completion so a task-scoped signal does not accumulate
  listeners); model resolution runs inside the shared
  resolveModelWithAbort cancellation scope (pre-aborted fast-fail,
  mid-resolution race)
- aborted/timeout requests normalize to the provider AbortError on all
  three wire formats (anthropic /v1/messages, responses /v1/responses,
  openai chat completions), both pre-stream and mid-stream; non-abort
  failures keep the wrapped "Opencode Go completion error:" identity
- the OpenAI branch runs message conversion and request construction
  inside the same cleanup scope as the Anthropic and Responses branches,
  so a conversion failure (e.g. an unstringifiable tool input) still
  detaches the bridged abort listener (CodeRabbit fix on this PR)
- completePrompt: forwards abortSignal/timeoutMs to all three SDK
  paths (timeoutMs <= 0 omits the SDK timeout option, since the SDK
  treats timeout: 0 as an immediate abort); aborted completions and
  APIConnectionTimeoutError/APITimeoutError normalize to the provider
  AbortError (series standard)

The two inner pre-stream guard mutants (the abort-normalization
condition and its provider-name literal) are documented as provably
equivalent with mutator-specific Stryker directives: createMessage's
outer catch applies the identical isRequestAborted check to the same
controller signal and re-standardizes, so the inner layer's only
unique behavior is the non-abort completion-error wrap (stays
kill-tested).

The cleanup-scope re-indentation made the gate instrument pre-existing
request-construction lines for the first time; their previously
unobservable mutants are kill-tested: R1 tool-result merge shape for
preserveReasoning models and the plain shape for non-preserveReasoning
models, the includeMaxTokens on/off branches with a ceiling-bounded
model, and the parallel_tool_calls default/explicit-false.

Unit 2/3 of the Zoo-Code-Org#1295 split (content source: 62f596c); stacks on the
shared-util unit.
Part of the abort-signal series (round 1). Builds on Zoo-Code-Org#674, Zoo-Code-Org#901, Zoo-Code-Org#1008.
Addresses Zoo-Code-Org#404.
MarkoJuhani pushed a commit to MarkoJuhani/Zoo-Code that referenced this pull request Sep 20, 2026
…ssion tests (Zoo-Code-Org#1288)

* feat(api): add throwIfAborted helper and completePrompt options regression tests

Add a fast-fail throwIfAborted guard to the shared abort-signal utilities and regression tests for the CompletePromptOptions interface (added by Zoo-Code-Org#901).

* feat(api): add shared isRequestAborted and createAbortError helpers to abort-signal utils

The OpenAI-family provider PRs (Zoo-Code-Org#1309, Zoo-Code-Org#1311) carry per-provider copies of the same abort-detection helper (isRequestAborted) and the same abort-error constructor (createAbortError); only the provider name in the message differs. Per the CodeRabbit maintainability finding on Zoo-Code-Org#1309 (extract the shared abort helpers into utils/abort-signal.ts), these are now shared in the foundation utility:
- isRequestAborted(error, signal?) - true when the caller signal fired, a native AbortError / OpenAI SDK APIUserAbortError was raised, or the message is exactly "Request was aborted." (exact match; a substring match would misclassify unrelated errors that merely mention aborting)
- createAbortError(providerName) - fresh error with name === "AbortError" and message "The <providerName> request was aborted", satisfying the Task.ts abort contract
- exported OpenAiRequestOptions type
7 new tests (isRequestAborted 4, createAbortError 3).

* test(abort-signal): assert throwIfAborted error message

The mutation-diff gate reports a surviving StringLiteral mutant on the throwIfAborted message literal. Assert the exact message so a blanked literal fails the suite.

---------

Co-authored-by: Eason Liang <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
Co-authored-by: edelauna <[email protected]>
myk1yt pushed a commit to myk1yt/Zoo-Code that referenced this pull request Oct 3, 2026
…rg#1651)

* feat(api): add abort-signal cancellation-scope helpers to the shared utils

Extend src/api/providers/utils/abort-signal.ts with the abort-signal
series helpers used by the gateway providers:

- isRequestAborted(error, signal): wider abort detection - an aborted
  signal, a DOM AbortError, the OpenAI/Anthropic SDK APIUserAbortError
  (name check), or the exact SDK abort message "Request was aborted." -
  trusting name/message only on real Error instances so a plain object
  that merely looks like an abort propagates unchanged
- createAbortError(providerName): fresh error satisfying the Task.ts
  abort contract (name "AbortError", message ending in "aborted")
- rejectOnAbort(pending, signal, providerName): settle a signal-less
  async phase (model discovery) on the provider AbortError when the
  signal fires first; the abort listener detaches when pending settles
- resolveModelWithAbort(fetchModel, signal, providerName): run model
  resolution inside a cancellation scope - entry fast-fail for a
  pre-aborted signal, the rejectOnAbort race while the lookup is
  pending, and normalization of abort-flavored lookup failures; any
  other resolution failure propagates unchanged

Includes direct unit tests for the resolveModelWithAbort cancellation
scope (pre-aborted fast-fail, no-signal pass-through, mid-resolution
race, abort normalization, non-abort propagation), the
isRequestAborted instanceof tightening tests, and the settle-guard
test utility.

Unit 1/3 of the Zoo-Code-Org#1295 split (content source: 62f596c).
Part of the abort-signal series (round 1). Builds on Zoo-Code-Org#674, Zoo-Code-Org#901, Zoo-Code-Org#1008.
Addresses Zoo-Code-Org#404.

* fix: restore throwIfAborted export dropped by the util rewrite (+ tests)

* fix(abort-r1): handle a rejecting pending promise in rejectOnAbort's pre-aborted branch

* test(abort-r1): remove the process listener on every exit path

* chore: trigger CodeRabbit re-review (findings already addressed)

---------

Co-authored-by: Eason Liang <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
SoccerField24x7 pushed a commit to SoccerField24x7/Zoo-Code that referenced this pull request Oct 29, 2026
…eMessage) (Zoo-Code-Org#1300)

* feat(api): abort signal support for vscode-lm + fake-ai completion tests

- completePrompt: bridge CompletePromptOptions.abortSignal and timeoutMs into a
  request-local vscode.CancellationTokenSource (the VS Code LanguageModelChat API
  only accepts a CancellationToken, not an AbortSignal); apply timeoutMs only when
  it is a positive value
- completePrompt: report aborted requests (external signal, timeout, or host
  cancellation) as errors with name = "AbortError" on both the error and success
  paths; remove the abort listener and dispose the token source in finally
- createMessage: bridge metadata.abortSignal into the internal request
  CancellationTokenSource (Bedrock pattern: pre-aborted guard + { once: true }
  listener stored in a named const), fail fast with an AbortError when the signal
  is already aborted, surface host CancellationError with name = "AbortError", and
  detach the listener / dispose the source in finally
- vscode-lm.spec.ts: add pre-aborted and mid-flight abort, timeout, listener
  attach/detach, and backward-compatibility tests
- fake-ai.spec.ts: option pass-through tests already merged on main via Zoo-Code-Org#901;
  verified green without changes

* fix(api): address CodeRabbit review on vscode-lm abort handling

- createMessage: sendRequest now uses the request-local cancellation source, the
  finally block disposes that local source and clears the shared field only when it
  still points at this request, and the error path no longer calls ensureCleanState
  (prevents an older finishing request from cancelling/disposing a newer request's
  token)
- completePrompt: a pre-aborted signal now fails fast before getClient() and again
  before sendRequest(), so a cancelled request never initializes or invokes the host
- completePrompt: a host vscode.CancellationError is normalized to an AbortError
  alongside isAborted()
- spec: the timeout test now expects an AbortError (the cancelled token aborts the
  completion); the pre-abort test asserts sendRequest is never called; added a
  CancellationError -> AbortError rejection test; the mock CancellationTokenSource
  cancel() now flips isCancellationRequested to match the real API

* fix(api): handle vscode-lm createMessage aborts during client init and streaming

- createMessage re-checks the external abort signal after client initialization
  (and before sendRequest), cancelling the local token source and throwing an
  AbortError when the signal aborted while getClient() was pending
- createMessage re-checks the external abort signal at the top of the stream
  consumption loop so a late abort stops the stream instead of yielding stale
  chunks (the bridged listener still covers the normal mid-flight case)
- spec: the mid-flight abort test now expects the stream to stop with an
  AbortError; added a regression test where client initialization is gated on a
  release promise and the signal aborts in that window - the generator rejects
  with AbortError and sendRequest is never called

* fix(api): start vscode-lm request cancellation before client initialization

* fix(api): cancel vscode-lm request on premature generator closure + fix pre-abort test comment

* test(vscode-lm): assert abort/timeout edge paths for the mutation-diff gate

Adds focused assertions for the remaining mutation-diff mutants on the abort-signal and timeoutMs paths: external-abort pre-checks and the in-stream abort re-throw, cancellation listener cleanup, timeout timer scheduling and clearing, and host CancellationError wrapping.

* test(vscode-lm): assert exact abort error messages and listener cleanup

Close the 18 surviving mutants of the PR Zoo-Code-Org#1300 mutation-diff gate on vscode-lm.ts:

- assert the exact canonical abort error message at every abort path in both
  createMessage and completePrompt
- rewrite the finally listener-detach guards to if (externalAbortSignal && handler)
  so the guard only runs when the signal exists, and direct the correlated
  LogicalOperator mutant
- direct the StringLiteral mutants on the guard/bridge abort errors: the catch
  re-throws its own canonical abort error, so those literals are unobservable
- add a test where the external signal aborts while the host cancellation token
  was not cancelled, exercising the right-hand operand of isAborted()
- strengthen the listener-spy tests to assert removeEventListener is called with
  the same listener reference, and clear the timeout timer unconditionally

* fix(vscode-lm): check request-local cancellation in createMessage and cover negative timeout

* fix(vscode-lm): re-check request-local token after client init

Address CodeRabbit findings on the abort-signal series:

- Re-check the request-local token immediately after getClient() so a request superseded while client initialization is pending aborts before token counting or host invocation.

- Drive the counting-abort regression through the public path by mocking countTokens on the host chat mock, removing the @ts-ignore private-method suppression.

- Add a gated-initialization supersession regression covering the new post-init guard.

* fix: normalize superseded createMessage client-init failures to AbortError

* fix(vscode-lm): reject quiet stream end after abort in createMessage

The in-loop cancellation check only fires when the host delivers another
chunk: if the external signal aborts (or a newer request supersedes this
one) while the host stream is parked and the stream then ends normally,
createMessage counted output tokens and yielded a usage chunk for the
partial text instead of rejecting. completePrompt already guards this
with its post-loop isAborted() check; createMessage now mirrors it with
the same token/signal check after the stream loop and before output-token
counting.

Regression tests cover both cancellation sources (external signal abort
and supersession via ensureCleanState) with a gated stream that ends
quietly, asserting the canonical AbortError and no usage chunk.

* test(vscode-lm): pin budget-gate edges and disable unobservable guard strings

Preflight on the unit delta flagged the context-window budget gate and the
refusal message: the gate condition (non-positive window disables trimming),
the raw-budget arithmetic, the strict > admission boundary, and the refusal
message's budget figure and pairing warning were all covered only by looser
regex assertions. Add three tests that pin each edge (zero window sends an
oversized request untouched; the refusal message is asserted in full,
including the raw budget figure; a conversation exactly at the raw budget is
sent, which the >= mutant refuses).

The post-loop quiet-end guard's own error strings are never observed: the
catch's abort branch intercepts the throw and re-throws its own canonical
abort error with identical message and name, so those StringLiteral mutants
are disabled with a concrete reason, matching the in-loop guard's existing
directives.

* test(vscode-lm): pin the raw-budget tool-schema subtraction

With no tools in the request the -toolSchemaChars operator in the raw-budget
arithmetic is equivalent to +toolSchemaChars (0 both ways), so the mutant was
unobservable. Add a refusal test with a non-empty tool schema that asserts the
exact budget figure: the mutant that flips the operator inflates the reported
budget by twice the schema size and fails the assertion.

* fix(api): reject non-finite and cap overflowing completePrompt timeouts

CodeRabbit: the timeoutMs guard let Infinity and values above
2^31-1 through to setTimeout, which Node coerces to a 1ms delay -
completePrompt could then cancel almost immediately with an
AbortError. Non-finite values now mean "no timeout" and finite
delays are capped at 2^31-1 ms.

Regression tests: Infinity schedules no timer and completes;
2^31 ms schedules a capped timer that does not fire within the
first second. Negative controls: reverting the guard fails each
test in isolation (1 red per control).

* fix(api): harden mergeAbortSignalAndTimeout against invalid delays

Port of the timeout guard finding (CodeRabbit, Zoo-Code-Org#1300 thread wave):
Node's AbortSignal.timeout validates its delay as uint32 - Infinity
and finite fractions (1500.5, 0.5) throw ERR_OUT_OF_RANGE, and
delays above 2^31-1 ms clamp to 1 ms (immediate abort). The helper
now treats non-finite values as "no timeout", rounds fractional
delays up (never down to 0), and caps the rest at 2^31-1 ms.
vscode-lm's own setTimeout guard is unaffected: setTimeout accepts
fractional delays (verified). Regression tests for non-finite,
overflowing, and fractional values; each revert fails exactly one
test.

* test(api): assert the rounded delay reaching AbortSignal.timeout

CodeRabbit: the fractional-timeout test only proved the call did
not throw and eventually aborted - a round-down mutation (1.5 -> 1)
would still pass. The test now spies AbortSignal.timeout and pins
the received delay: 2 for 1.5 and 1 for 0.5 (never 0). Negative
control: mutating Math.ceil to Math.floor fails exactly this test.

* fix(api): round fractional completion timeouts up before scheduling

CodeRabbit: the raw setTimeout path passed 1500.5 through and Node
truncates non-integer delays, cancelling at 1500 ms - inconsistent
with the PR objective and mergeAbortSignalAndTimeout's 1501. The
guard now applies Math.ceil before the cap. New test pins the delay
handed to setTimeout (1501 for 1500.5); negative control (reverting
the ceil) fails exactly that test.

* fix(api): reconcile Copilot cancellation tests with the merged abort contract

The merge brought in main's GitHubCopilotHandler cancellation tests, which
were written against main's vscode-lm internals. Two reconciliations:

- createMessage now also records the caller's abort in an abortedByCaller
  flag set synchronously by the bridge listener, and the post-getClient /
  post-count re-checks consult it in addition to the token flag. The token
  flag alone cannot be the sole signal: cleanup cancels the token too, and
  a host token's flag is not observable before its cancel settles (main's
  own implementation used the same flag for this reason).
- github-copilot.spec.ts: cancellation assertions now expect this unit's
  canonical abort error (createAbortError -> "The VS Code LM request was
  aborted", name AbortError) instead of main's interim strings, and the
  mid-stream completePrompt test asserts the contract that content racing
  a cancellation is not surfaced (rejects instead of resolving with late
  text), matching the pinned vscode-lm behavior.

Full api suite green (3220 passed / 2 skipped); tsc, eslint, prettier clean.

* test(vscode-lm): pin the bridged token in the completePrompt abort test

createMessage already asserts sendRequest received the bridged token; completePrompt only checked that the token source was disposed, so a fresh token passed to sendRequest would have gone unnoticed.

* style: apply repo Prettier to the merge resolution

* test(api): pin the abort-bridge listener identity in the cleanup assertion

The initialization-abort test only counted removeEventListener calls. Spy on addEventListener too and assert the removed listener is the exact callback the bridge registered, so a wrong-listener cleanup cannot pass.

* fix(vscode-lm): cancel in-flight completions on handler disposal

Review follow-ups: completePrompt token sources are tracked on the handler and cancelled in dispose() (removed from the set in finally), so tearing the handler down stops completion requests as well as streaming ones. completePrompt also adopts the shared hasUsableTimeout/toSdkTimeoutMs helpers instead of inlining ceil/cap. Adds two focused tests: an external abort landing while getClient() is pending rejects with the contract AbortError without a host request, and disposal during client initialization cancels the active completion token. vscode-lm.spec.ts 382 passed; tsc clean for the file.

* fix(vscode-lm): reject a cancellation that lands inside the final token count

Review follow-ups: internalCountTokens returns 0 on cancellation, so the generator re-checks the request cancellation before yielding the usage chunk instead of completing normally with a zero-output chunk. The boundary test now pins a window whose 0.8*3 character budget is an exact integer (2.4 is exact for a 128k-token window), so the equality case is representable and the >= mutant is detectable; a focused test aborts during the output count and asserts the contract AbortError. vscode-lm.spec.ts 382 passed (new count-abort test included).

* test(vscode-lm): abort from the host count instead of spying a private method

The platform-unit coverage run reported 19 downstream failures: spying on the private internalCountTokens made the abort land differently under coverage instrumentation. The test now aborts from the host countTokens mock (the output count is the call carrying the streamed text) and restores the original implementation, so the suite passes with and without coverage (383 passed in both).

* fix(vscode-lm): surface the shared canonical abort for host cancellation

Human review follow-up: replace the remaining hand-built AbortError for a host CancellationError with createAbortError(this.providerName); update the two assertions that pinned the old branded message to the canonical text. vscode-lm.spec.ts 383 passed; tsc clean.

* test(vscode-lm): pin signal-only aborts and completion token disposal

Review follow-ups: a createMessage test neutralises the bridged token cancel so only the external signal aborts (canonical AbortError); dispose tests cover a streaming completion, two concurrent completions, and a finished completion (which must not be cancelled again). vscode-lm.spec 387 passed; tsc clean.

* test(vscode-lm): yield-empty generators in the signal-only abort test

Lint follow-up: the quiet stream stubs must be generators, so they yield an empty iterable instead of only awaiting the gate. vscode-lm.spec 387 passed; eslint 0.

---------

Co-authored-by: Eason Liang <[email protected]>
Co-authored-by: edelauna <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
Co-authored-by: dsh-agent <dsh@local>
KarthickMahalingam pushed a commit to KarthickMahalingam/Zoo-Code that referenced this pull request Nov 4, 2026
…oo-Code-Org#1309)

* feat(api): add throwIfAborted helper and completePrompt options regression tests

Add a fast-fail throwIfAborted guard to the shared abort-signal utilities and regression tests for the CompletePromptOptions interface (added by Zoo-Code-Org#901).

* feat(api): abort-signal wiring for lm-studio and qwen-code (round 2)

* fix(api): align lm-studio specs with abort-signal call shape and address review

CI: platform-unit-test (ubuntu-latest) was failing on four LM Studio spec
assertions that still expected the pre-PR single-argument SDK call shape.
createMessage now calls chat.completions.create(params, { signal }) with the
request-local AbortSignal, and completePrompt passes no options when no
signal/timeout is configured. Update the two existing specs to the same
two-argument assertions used by the new qwen-code-native-tools.spec.ts
(expect.any(AbortSignal) for createMessage, undefined second argument for the
no-signal completePrompt case).

Review: (1) narrow isRequestAborted to an exact match on the OpenAI SDK abort
error text ("Request was aborted.") instead of a substring scan, so unrelated
errors that merely mention "abort" are no longer normalized as user
cancellations; (2) abort the request-local controller in the createMessage
finally block so early stream termination (break/return) cancels the
in-flight SDK request.

* test(api): cover LM Studio reasoning_content delta in abort-signal spec

Changed-line coverage verification against the full PR diff
(git diff origin/main...HEAD) found two executable added lines in
lm-studio.ts uncovered by the PR's spec files: the
reasoning_content/reasoning delta branch of createMessage (upstream main
had since re-based that block into the PR diff). Add a focused streaming
regression test to lm-studio-timeout.spec.ts that exercises the branch,
restoring 100% changed-line coverage on both provider files
(lm-studio.ts 35/35, qwen-code.ts 65/65 executable added lines).

* test(api): cover qwen-code degenerate stream shapes for full branch coverage

Codecov's patch report on the previous head flagged 5 partial-coverage
lines in qwen-code.ts (lines 328, 338, 344, 398, 399): the defensive
branches of the createMessage stream loop — a chunk with no choice
(choices[0] ?? fallback), a delta that repeats the previous full content
(empty after trimming), a think block that starts the text (empty leading
split segment), and a zeroed usage payload (prompt_tokens || 0,
completion_tokens || 0).

Add one focused degenerate-stream-shapes test to qwen-code-native-tools
spec covering all five branches in a single stream. Branch-level lcov
cross-reference on the PR's added lines now reports zero partial-coverage
added lines in both provider files, and line-level changed-line coverage
stays 100% (lm-studio.ts 35/35, qwen-code.ts 65/65 executable added
lines).

* feat(api): add shared isRequestAborted and createAbortError helpers to abort-signal utils

The OpenAI-family provider PRs (Zoo-Code-Org#1309, Zoo-Code-Org#1311) carry per-provider copies of the same abort-detection helper (isRequestAborted) and the same abort-error constructor (createAbortError); only the provider name in the message differs. Per the CodeRabbit maintainability finding on Zoo-Code-Org#1309 (extract the shared abort helpers into utils/abort-signal.ts), these are now shared in the foundation utility:
- isRequestAborted(error, signal?) - true when the caller signal fired, a native AbortError / OpenAI SDK APIUserAbortError was raised, or the message is exactly "Request was aborted." (exact match; a substring match would misclassify unrelated errors that merely mention aborting)
- createAbortError(providerName) - fresh error with name === "AbortError" and message "The <providerName> request was aborted", satisfying the Task.ts abort contract
- exported OpenAiRequestOptions type
7 new tests (isRequestAborted 4, createAbortError 3).

* refactor(api): use shared abort helpers from foundation utils in lm-studio and qwen-code

The per-provider copies of isRequestAborted / createAbortError /
OpenAiRequestOptions were extracted into src/api/providers/utils/abort-signal.ts by foundation PR Zoo-Code-Org#1288 (commit a0117fb) following the CodeRabbit maintainability finding on this PR; the providers now import the shared helpers. createAbortError takes the provider name as a parameter; provider behavior and abort messages are unchanged.

* fix(api): normalize aborts during qwen-code 401 retry and lm-studio token counting

CodeRabbit follow-ups on the abort-signal series: an abort landing while the qwen-code 401 retry request is in flight escaped callApiWithRetry as the raw SDK abort error instead of the normalized 'The Qwen Code request was aborted'; the retried apiCall now goes through the same isRequestAborted normalization. In lm-studio createMessage, an abort landing while input token counting is pending was silently dropped (a listener added to an already-aborted signal never fires), so the aborted state is bridged into the request-local controller and throwIfAborted fast-fails before the request is issued. Regression tests cover both paths.

* test(api): cover the non-abort path of the qwen-code 401 retry

Closes the remaining changed-line gap in qwen-code.ts: a 401 whose retry fails with a non-abort error must be rethrown unchanged (the abort check's false branch).

* fix(ci): declare vitest as a root devDependency so the mutation gate resolves its bin

* test(api): strengthen focused tests for the changed-code mutation gate

* test(api): harden abort-wiring tests against hanging mutants

* test(api): exclude proven-equivalent Stryker mutants from the changed-code gate

* test(api): exclude unobservable inner-abort provider-name literals from the gate

The two inner throw createAbortError("LM Studio") sites in createMessage and
completePrompt always produce an error whose name is AbortError.
isRequestAborted treats name AbortError as an aborted request, so the outer
catch re-normalizes those failures into the same contract error. The
provider-name StringLiteral at the inner sites is therefore unobservable
and the StringLiteral mutants are provably equivalent.

* fix(api): settle lm-studio and qwen-code wait points against the abort signal

- add settleOnAbort helper: await pending work but reject with the request abort error the moment the signal aborts; the underlying work keeps running (a shared deduped token refresh is never cancelled by one request's signal) and the listener is removed on either outcome

- lm-studio: race the input and output token counts against the request-local signal so a Stop during a pending count settles the generator promptly; skip usage reporting if a stop lands in the microtask gap before the usage yield

- qwen-code: race ensureAuthenticated (credential load and token refresh) against the request signal in createMessage and completePrompt; read the OpenAI SDK status through a narrow shape instead of catch (error: any)

- specs: pending-count and pending-auth abort contract tests plus settleOnAbort unit tests

- eslint-suppressions: drop the qwen-code no-explicit-any entry consumed by the catch-clause fix

* test(api): add abort kill-tests for lm-studio and qwen-code

* Adapt qwen-code spec to per-scope NativeToolCallParser API

* fix(deps): repair pnpm-lock.yaml broken by merge conflict resolution

The merge of main into this branch resolved the lockfile conflict by taking
main's lockfile plus only the PR-side importers hunk, leaving the root
importer's vitest 4.1.9 reference without a matching packages/snapshots
entry. pnpm install --frozen-lockfile failed with
ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY in every CI job.

Regenerated the lockfile with pnpm install --no-frozen-lockfile at the merge
head: adds the [email protected] package/snapshot entries and the new peer
resolutions. Verified: pnpm install --frozen-lockfile passes, lint 11/11,
check-types 11/11, code-qa workflow, lifecycle model-check 7/7, mcp
integration check, translations, knip, and the 7 PR-affected vitest suites
(143 tests).

* fix(deps): align root vitest with the 4.1.11 security bump

GitHub dependency review flagged vitest 4.1.9 (the root devDependency this
branch introduced) as vulnerable; the repo already moved the per-package
vitest pins to 4.1.11 in Zoo-Code-Org#1582 (security). Set the root pin to 4.1.11 and
regenerated pnpm-lock.yaml, which drops the now-unused [email protected]
package/snapshot entries.

Verified: pnpm install --frozen-lockfile passes, knip, and the 7
PR-affected vitest suites (143 tests).

* fix(api): race the Qwen 401 refresh wait against the abort signal and tighten listener-identity assertions

Address the CodeRabbit findings on the 401 refresh path:
- callApiWithRetry now races the refresh wait against the caller's
  signal (settleOnAbort), so a stop settles promptly instead of
  blocking on the token endpoint; the shared refresh keeps running
  and only this wait is cut.
- The abort-during-401-refresh spec now keeps the token endpoint
  pending forever, proving the wait is cut by the abort (a post-hoc
  aborted check alone would hang).
- settleOnAbort listener-cleanup specs assert the exact registered
  listener reference is removed, not just that some function was
  removed.

* fix(qwen-code): keep in-memory credentials current after a cancelled 401 wait (CodeRabbit, R2, Zoo-Code-Org#1309)

doRefreshAccessToken persisted the refreshed token but left the
in-memory credentials to the waiting caller's assignment. When
settleOnAbort rejected on abort before the shared refresh settled,
that assignment was skipped: the background refresh had already saved
the new token, yet the handler kept the stale one in memory, so the
next request reused it and triggered a second 401 refresh.

Assign this.credentials in the shared refresh itself, and restructure
the refresh-wait test so the abort lands after settleOnAbort has
registered its listener (the old mock aborted synchronously inside
the fetch, only exercising the already-aborted fast path). New
regression test: abort cuts the established wait, the refresh settles
in the background, and the in-memory credentials carry the refreshed
token.

28/28 specs pass; eslint clean (suppressions unchanged).

* chore: trigger label reconcile (CI green + CR approved, cron stalled)

* Assert the abort bridge while the request is in flight

Both tests asserted after the call had already finished, so they passed even
with a broken bridge: the provider's finally block aborts the request signal on
its own, and releasing the count gate lets counting finish normally.

- the signal test now keeps the mocked stream open, aborts the caller signal and
  checks the captured request signal before the stream is released;
- the cancellation test now asserts the pending count rejects while the gate is
  still closed, and releases the gate in a finally block.

54 tests green across the two suites; prettier, eslint and tsc clean.

* Re-trigger review at the current head

Every review thread on this PR is resolved and CI is green at this head; the
review decision still points at an older commit. This empty commit re-runs the
review so the decision and the label reflect the current head.

* Address pre-merge checks: detach settleOnAbort's listener on abort; pin completePrompt fast-fail

settleOnAbort registered an abort listener that its onAbort callback
never removed: when the pending promise never settled (e.g. a wedged
shared token refresh), the listener stayed attached to the caller's
signal for the signal's whole life. A single cleanup function now
detaches the exact registered listener at most once, called from the
abort callback and from the pending promise's fulfillment/rejection
handlers. New test aborts with a never-settling pending promise and
asserts the listener is removed by the abort callback itself (negative
control: removing the cleanup call from onAbort fails exactly that
test).

Also adds the missing focused test for LmStudioHandler.completePrompt's
pre-aborted-signal fast-fail: an already-aborted signal rejects with
the contract AbortError and chat.completions.create is never called
(negative control: removing the throwIfAborted guard fails exactly
that test).

Verification at this head: 67/67 green across abort-signal.spec.ts and
lm-studio.spec.ts; tsc --noEmit 0 errors; eslint --max-warnings=0
clean on touched files with suppression counts unchanged; prettier
clean.

* Address review: pin listener removal by function reference

Both listener-removal tests accepted any function in the removeSpy
assertion, so removing a different function than the one registered
would still pass. Per the path instruction (and CR suggestions):
the lm-studio createMessage test now captures the registered listener
from addSpy and asserts removeSpy was called with that exact
reference; the settleOnAbort rejection-propagation test now uses the
same addSpy.mock.calls[0]?.[1] pattern as the other cleanup tests in
the file.

Verification at this head: 67/67 green across abort-signal.spec.ts and
lm-studio.spec.ts; tsc --noEmit 0 errors; eslint --max-warnings=0
clean on touched files; prettier clean.

* Address pre-merge warnings: guard settleOnAbort's early return; share Qwen credential loads

settleOnAbort's already-aborted early return handed the caller the
abort error without attaching a rejection handler to the pending
promise, exposing a rejecting pending promise as an unhandled
rejection. A no-op rejection guard now runs before the early return
(same shape as rejectOnAbort's existing guard). New test aborts with a
rejecting pending promise and asserts no unhandledRejection fires
while the abort error surfaces (negative control: removing the guard
fails exactly that test).

QwenCodeHandler duplicated credential loading after cancellation:
createMessage/completePrompt race ensureAuthenticated() with
settleOnAbort, so a cancelled first request left its load running and
a second request started a fresh credential read. ensureAuthenticated
now shares the in-flight load promise the same way refreshAccessToken
shares its refresh, cleared in a finally once the load settles, so a
failed load is retried by the next request. New tests: concurrent
cancel+retry reuses one credential read; success and failure both
clear the shared promise (negative control: breaking the reuse guard
fails exactly the reuse test; the test releases the hung read in a
finally so a failing run cannot leak the pending load into later
tests).

Review test-fidelity threads: the two qwen-code-native-tools 401
refresh tests now abort only after the refresh wait is established
(fetch never settles; drain until the fetch starts) instead of
aborting synchronously inside the fetch mock, which exercised only
the already-aborted fast path; the two qwen-code listener tests now
assert the exact registered listener reference is removed instead of
expect.any(Function).

Verification at this head: 128/128 green across abort-signal,
qwen-code, qwen-code-native-tools and lm-studio specs; tsc --noEmit 0
errors; eslint --max-warnings=0 clean with suppression counts
unchanged; prettier clean.

* Address review: cover LM Studio early consumer termination

createMessage's finally block cancels the request-local SDK signal and
detaches the external abort listener, but no test pinned the early
consumer exit (break/return while the SDK stream is still pending):
the existing coverage only exercised normal completion and
external-signal aborts. New test consumes one chunk from a stream
that then stays pending, calls stream.return(), and asserts the
request-local SDK signal becomes aborted and the exact registered
external listener is removed (by reference, per the established
pattern). Negative control: removing the finally-block abort fails
exactly this test (isolated run).

Verification at this head: 27/27 green in lm-studio.spec.ts; tsc
--noEmit 0 errors; eslint --max-warnings=0 clean with suppression
counts unchanged; prettier clean.

* Address review: pin no premature abort before early termination

The early-termination test only observed the request-local signal
after stream.return(), so a premature abort (one landing while the
consumer is still iterating) would also pass. The test now captures
the SDK request options after the first chunk and asserts the
request-local signal is still live (aborted === false) before the
early return, retaining the post-return aborted assertion. Negative
control: aborting the request-local controller right after the SDK
create call fails exactly this test (isolated run: the new false
assertion reddens).

Verification at this head: 27/27 green in lm-studio.spec.ts; tsc
--noEmit 0 errors; eslint --max-warnings=0 clean with suppression
counts unchanged; prettier clean.

* chore(api): remove Stryker suppression comments from touched files

Per repo policy (AGENTS.md: Stryker suppression comments are banned;
the mutation gate rejects them in touched files). Equivalence
arguments for the resulting mutants are recorded in the PR body.

* fix(api): reject non-finite and cap overflowing mergeAbortSignalAndTimeout delays

Port of the Zoo-Code-Org#1300 timeout guard finding (CodeRabbit): Node's
AbortSignal.timeout throws ERR_OUT_OF_RANGE for non-integer delays
(Infinity) and clamps delays above 2^31-1 ms to 1 ms, so a
non-finite timeoutMs threw a RangeError instead of running the
request, and an overflowing one aborted almost immediately. The
helper now treats non-finite values as "no timeout" and caps
finite delays at 2^31-1 ms.

Regression tests added; negative controls (reverting the guard)
fail exactly the two new tests.

* fix(api): round fractional timeouts before AbortSignal.timeout

CodeRabbit follow-up on the timeout guard: Node validates
AbortSignal.timeout delays as uint32, so finite fractions (1500.5,
0.5) threw ERR_OUT_OF_RANGE just like Infinity did. The helper now
rounds delays up (a positive fraction never becomes 0 ms) before
capping at 2^31-1 ms. Regression test mergeAbortSignalAndTimeout
(undefined, 1.5); negative control (reverting the rounding) fails
exactly that test.

* test(api): assert the rounded delay reaching AbortSignal.timeout

CodeRabbit: the fractional-timeout test only proved the call did
not throw and eventually aborted - a round-down mutation (1.5 -> 1)
would still pass. The test now spies AbortSignal.timeout and pins
the received delay: 2 for 1.5 and 1 for 0.5 (never 0). Negative
control: mutating Math.ceil to Math.floor fails exactly this test.

* fix(api): restore the OpenAiRequestOptions type the abort wiring imports

upstream/main removed the type in its knip dead-code pass; lm-studio.ts and qwen-code.ts still import it, so the merge has to re-add it.

* fix(api): report an elapsed per-request timeout as TimeoutError

completePrompt merged the caller signal with the timeout and mapped every abort-shaped failure to AbortError, so a deadline expiry looked like a user stop. Distinguish them via the merged signal reason: TimeoutError when the timer fired while the caller signal stayed live, AbortError for a real cancellation. Applied to both lm-studio and qwen-code (same PR, same contract) with regression tests whose negative controls fail with expected AbortError to be TimeoutError.

* refactor(providers): share the timeout-vs-abort helpers and dedupe the LM Studio catch selection

Review follow-ups: createTimeoutError/isTimeoutAbort move from qwen-code.ts and lm-studio.ts into utils/abort-signal.ts (providerName stays a createTimeoutError parameter; behavior unchanged) and both providers import them. LM Studio completePrompt selects TimeoutError vs the contract AbortError through one private helper used by the inner create catch and the outer completion catch. The qwen elapsed-timeout test now passes a live caller signal alongside timeoutMs and asserts TimeoutError, and uses mockImplementationOnce so the signal-waiting mock cannot leak into later tests. Contract tests for the shared helpers live in abort-signal.spec.ts. qwen-code + lm-studio + abort-signal suites 111 passed; abort-signal 58 passed; tsc clean.

* fix(abort-signal): classify timeout aborts by the merged signal reason

Review follow-ups: isTimeoutAbort takes only the merged requestSignal and keys on its retained abort reason, so a timeout that fires before a later caller abort stays a TimeoutError (and a caller-first abort stays a cancellation); both providers updated. Tests cover both abort orders. The elapsed-timeout completePrompt test moved into the completePrompt suite. abort-signal + qwen-code + lm-studio specs 116 passed; tsc clean.

* chore(lm-studio): drop the unused caller signal parameter

Follow-up to the isTimeoutAbort signature change: toCompletionAbortError only needs the merged request signal. tsc clean; eslint on the touched providers exits 0.

* test(lmstudio): wait for the request instead of a fixed sleep

* fix(api): settle qwen-code streams that end quietly on abort

Review threads on Zoo-Code-Org#1309:
- qwen-code.ts: the OpenAI SDK iterator returns without throwing when its
  controller aborts (openai/core/streaming.js: isAbortError -> return), so a
  stop mid-stream left the generator looking like a completed turn with no
  usage chunk. Re-check the request-local signal after the stream loop, as
  lm-studio.ts already does. Regression test included; negative control: the
  test fails with the guard removed.
- lm-studio.ts: drop the unreachable already-aborted branch - nothing is
  awaited between throwIfAborted and the listener registration - and fix the
  comment that justified it.
- abort-signal.ts: settleOnAbort delegates to rejectOnAbort/raceAbort instead
  of maintaining a second copy of the race. raceAbort now detaches its
  listener exactly once from whichever side settles first (the abort callback
  included), so both helper suites keep their leak guards; three assertions
  that pinned the addEventListener argument count now assert only that the
  registration happened once.
- abort-signal.ts: comments describe what the helpers do instead of referring
  to a PR series.
- specs: replace fixed 10 ms sleeps and fixed-depth promise ticks with
  vi.waitFor on the observable condition, and add concurrent-request tests for
  both providers (aborting one request must not cancel the other's signal).

Local verification: api/providers + api/transform 3716 tests green, tsc
--noEmit clean, eslint --max-warnings=0 clean with no suppression changes,
prettier clean, knip strict clean.

* test(api): poll instead of assuming await depth, and exercise the merged signal for real,,Three review findings on the abort-signal tests:,,- qwen-code-native-tools.spec.ts: the two 401-refresh tests drained up to 50 microtasks with,  'await Promise.resolve()' before asserting the refresh fetch had started, and waitForCreateCall,  re-implemented vi.waitFor with a setTimeout loop. Both replaced by vi.waitFor polls, matching,  qwen-code.spec.ts. Control: a 2-deep microtask drain fails with 'expected "vi.fn()" to be called,  1 times, but got 0 times' at spec:796 while the poll passes. Honest note: on the SDK-call wait a,  2-deep drain still happened to pass today, so that half is a latent trap rather than a present,  failure - the helper is gone either way.,- abort-signal.spec.ts: the isTimeoutAbort order tests built two unlinked controllers (and copied,  the reason by hand), so they passed regardless of what the merge actually preserves. Both now,  build the signal with the real mergeAbortSignals. Control: swapping the two abort calls in the,  timeout-first test now fails with "AssertionError: expected false to be true" - the test tracks,  the merge semantics it describes.,- abort-signal.spec.ts: added the null abort-reason case. Control: removing the optional chain on,  requestSignal.reason at abort-signal.ts:320 fails the new test with 'TypeError: Cannot read,  properties of null (reading name)' at abort-signal.ts:320:85, which is the surviving,  OptionalChaining mutant mutation-diff reported on that line.,,Verification: api/providers/__tests__/qwen-code-native-tools.spec.ts +,api/providers/utils/__tests__/abort-signal.spec.ts -> 2 files, 99 tests passed. tsc --noEmit: 0,errors. eslint --max-warnings=0 exit 0 on both files with src/eslint-suppressions.json untouched.,prettier --check clean.

* test(api): ban setTimeout in provider specs, use waitFor and shared helpers

---------

Co-authored-by: Eason Liang <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
Co-authored-by: Elliott de Launay <[email protected]>
hanneke-de-vries pushed a commit to hanneke-de-vries/roo-plus that referenced this pull request Nov 10, 2026
…ssion tests (Zoo-Code-Org#1288)

* feat(api): add throwIfAborted helper and completePrompt options regression tests

Add a fast-fail throwIfAborted guard to the shared abort-signal utilities and regression tests for the CompletePromptOptions interface (added by Zoo-Code-Org#901).

* feat(api): add shared isRequestAborted and createAbortError helpers to abort-signal utils

The OpenAI-family provider PRs (Zoo-Code-Org#1309, Zoo-Code-Org#1311) carry per-provider copies of the same abort-detection helper (isRequestAborted) and the same abort-error constructor (createAbortError); only the provider name in the message differs. Per the CodeRabbit maintainability finding on Zoo-Code-Org#1309 (extract the shared abort helpers into utils/abort-signal.ts), these are now shared in the foundation utility:
- isRequestAborted(error, signal?) - true when the caller signal fired, a native AbortError / OpenAI SDK APIUserAbortError was raised, or the message is exactly "Request was aborted." (exact match; a substring match would misclassify unrelated errors that merely mention aborting)
- createAbortError(providerName) - fresh error with name === "AbortError" and message "The <providerName> request was aborted", satisfying the Task.ts abort contract
- exported OpenAiRequestOptions type
7 new tests (isRequestAborted 4, createAbortError 3).

* test(abort-signal): assert throwIfAborted error message

The mutation-diff gate reports a surviving StringLiteral mutant on the throwIfAborted message literal. Assert the exact message so a blanked literal fails the suite.

---------

Co-authored-by: Eason Liang <[email protected]>
Co-authored-by: easonLiangWorldedtech <[email protected]>
Co-authored-by: edelauna <[email protected]>
(cherry picked from commit e12a42e)
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

3 participants