Skip to content

feat(api): abort signal support for unbound, vercel-ai-gateway, zoo-gateway - #1653

Open
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-r1-u3-gateway-siblings
Open

easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-r1-u3-gateway-siblings

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor

Wires the external abort signal and per-request timeout into the completePrompt (non-streaming) and createMessage (streaming) paths of the Unbound, Vercel AI Gateway, and Zoo Gateway providers. Stacks on the shared-util and opencode-go units of this split (resolveModelWithAbort in utils/abort-signal.ts).

Each provider:

  • createMessage: bridges metadata?.abortSignal (Bedrock pattern: pre-aborted guard, { once: true } listener, detached on completion so a task-scoped signal does not accumulate listeners) into a per-request AbortController; model resolution runs inside the shared resolveModelWithAbort cancellation scope (pre-aborted fast-fail before any catalog/SDK work, mid-resolution race). Aborted requests (pre-aborted, mid-resolution, mid-stream) normalize to the provider AbortError; non-abort failures propagate or wrap unchanged.
  • completePrompt: forwards abortSignal/ timeoutMs to the OpenAI SDK (timeoutMs <= 0 omits the SDK timeout option, since the SDK treats timeout: 0 as an immediate abort); aborted completions and APIConnectionTimeoutError normalize to the provider AbortError (series standard).

Zoo Gateway places the entry fast-fail before ensureAuthenticated() so a pre-aborted task surfaces the AbortError instead of an auth failure.

Tests:

  • Ported the reference abort/timeout completePrompt pass-through tests (signal, timeoutMs incl. 0, and no-options backward compatibility) for all three providers, plus normalization/identity tests for aborted and timed-out completions.
  • createMessage bridging tests per provider: pre-aborted signal -> rejects with the standardized AbortError before any request work (catalog and SDK both uncalled; unbound additionally asserts the SDK-level rejection identity since its error wrapper preserves main's behavior); abort mid-resolution -> settles on the standardized AbortError before the lookup is released, no late request; detach tests use the reference-identity pattern (the resolution race registers its own "abort" listener, so the bridge is the last registration).

Series and unit

Unit 3/3 of the #1295 split (content source: 62f596c5d); stacks on the shared-util and opencode-go units. Three sibling OpenAI-SDK gateway providers with an identical abort-wiring shape are kept as one unit to avoid tripling the review surface for mechanically identical changes (a+d soft-cap rationale per the split budget; the mutation gate is measured on the unit delta and is green).

Part of the abort-signal series (round 1). Builds on #674, #901, #1008. Addresses #404.

Review response (maintainer review of #1295)

  • The model-resolution guard, the rejectOnAbort race, and the abort normalization now live once in the shared util (resolveModelWithAbort, unit 1/3) instead of being inlined per provider; all three providers call it and each spec exercises it end-to-end (pre-aborted -> catalog and SDK both uncalled; mid-resolution -> standardized AbortError before the lookup is released, no late request).
  • The createMessage catches use the wider isRequestAborted condition (aborted signal, DOM AbortError, SDK APIUserAbortError, exact "Request was aborted." message; name/message checks require a real Error instance).
  • The detach tests assert reference identity on the last "abort" registration, because the resolution race registers its own listener on the same external signal.

Evidence

  • vitest: 139/139 passing across the three gateway suites (134 changed executable lines, all covered)
  • Local Stryker mutation gate (unit delta vs own base): 185 valid mutants (≤400), 176 killed, 9 directive-ignored (the per-request bridge's unreachable pre-aborted branch per provider, with invariant-citing proofs), 0 Survived / 0 NoCoverage / 0 Timeout

…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: 62f596c5d).
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.
- 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
- 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).

Unit 2/3 of the Zoo-Code-Org#1295 split (content source: 62f596c5d); 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.
…ateway

Each provider's createMessage bridges metadata.abortSignal to a
per-request AbortController (Bedrock pattern: pre-aborted guard,
{ once: true } listener, detached on completion so a task-scoped
signal does not accumulate listeners) and runs model resolution inside
the shared resolveModelWithAbort cancellation scope; aborted requests
(pre-aborted, mid-resolution, mid-stream) normalize to the provider
AbortError while non-abort failures propagate or wrap unchanged.
completePrompt forwards abortSignal/timeoutMs to the OpenAI SDK
(timeoutMs <= 0 omits the SDK timeout option); aborted completions and
APIConnectionTimeoutError normalize to the provider AbortError (series
standard).

Zoo Gateway places the entry fast-fail before ensureAuthenticated() so
a pre-aborted task surfaces the AbortError instead of an auth failure
(pinned by an unauthenticated pre-abort test).

Unit 3/3 of the Zoo-Code-Org#1295 split (content source: 62f596c5d); stacks on the
shared-util and opencode-go units. Three sibling OpenAI-SDK gateway
providers with an identical abort-wiring shape are kept as one unit to
avoid tripling the review surface for mechanically identical changes
(a+d soft-cap rationale per the split budget; the mutation gate is
measured on the unit delta and is green).

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

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 58f1e37f-47ed-4848-92cf-c6b5855dc442

📥 Commits

Reviewing files that changed from the base of the PR and between 9973630 and 044e22b.

📒 Files selected for processing (11)
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/unbound.ts
  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/api/providers/utils/abort-signal.ts
  • src/api/providers/vercel-ai-gateway.ts
  • src/api/providers/zoo-gateway.ts
  • src/test-utils/settle-guard.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.

📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (5)
Treat model, provider, MCP, path, command, and tool data as untrusted.

⚙️ CodeRabbit configuration file

Files:

  • src/api/providers/zoo-gateway.ts
  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/api/providers/unbound.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/utils/abort-signal.ts
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  • src/api/providers/vercel-ai-gateway.ts
Require regression coverage at the lowest valid harness with behavior-focused assertions, including relevant negative, error, false/unset, and boundary cases.

⚙️ CodeRabbit configuration file

Files:

  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
Check strict typing and exhaustive behavior across normal, boundary, error, cancellation, retry, and compatibility paths.

⚙️ CodeRabbit configuration file

Files:

  • src/api/providers/zoo-gateway.ts
  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/test-utils/settle-guard.ts
  • src/api/providers/unbound.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/utils/abort-signal.ts
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  • src/api/providers/vercel-ai-gateway.ts
Verify extension/webview contracts, cancellation and error propagation, VS Code lifecycle correctness, and behavior under retries and partial failure.

⚙️ CodeRabbit configuration file

Files:

  • src/api/providers/zoo-gateway.ts
  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/test-utils/settle-guard.ts
  • src/api/providers/unbound.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/utils/abort-signal.ts
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  • src/api/providers/vercel-ai-gateway.ts
Act as an adversarial second-opinion reviewer.

⚙️ CodeRabbit configuration file

Files:

  • src/api/providers/zoo-gateway.ts
  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/test-utils/settle-guard.ts
  • src/api/providers/unbound.ts
  • src/api/providers/opencode-go.ts
  • src/api/providers/__tests__/unbound.spec.ts
  • src/api/providers/__tests__/zoo-gateway.spec.ts
  • src/api/providers/utils/abort-signal.ts
  • src/api/providers/__tests__/opencode-go.spec.ts
  • src/api/providers/__tests__/vercel-ai-gateway.spec.ts
  • src/api/providers/vercel-ai-gateway.ts
🪛 ESLint
src/api/providers/unbound.ts

[error] 202-202: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🔇 Additional comments (11)
src/api/providers/utils/abort-signal.ts (1)

52-65: LGTM!

Also applies to: 80-146

src/api/providers/utils/__tests__/abort-signal.spec.ts (1)

11-96: LGTM!

Also applies to: 98-195, 331-339

src/test-utils/settle-guard.ts (1)

10-26: LGTM!

src/api/providers/opencode-go.ts (1)

206-233: LGTM!

Also applies to: 236-284, 316-369, 463-483, 612-624, 809-852, 862-870, 898-912, 939-964

src/api/providers/__tests__/opencode-go.spec.ts (1)

73-88: LGTM!

Also applies to: 406-485, 488-743, 902-917, 986-1299, 1528-1584, 1628-1739, 2225-2287

src/api/providers/unbound.ts (1)

129-143: LGTM!

Also applies to: 169-201, 203-249, 263-289

src/api/providers/__tests__/vercel-ai-gateway.spec.ts (1)

299-347: LGTM!

Also applies to: 836-1172, 1177-1193

src/api/providers/__tests__/zoo-gateway.spec.ts (1)

549-663: LGTM!

Also applies to: 664-672, 675-882

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

2-25: LGTM!

Also applies to: 192-354, 374-430, 432-561, 563-938

src/api/providers/vercel-ai-gateway.ts (1)

62-69: LGTM!

Also applies to: 100-178, 200-228

src/api/providers/zoo-gateway.ts (1)

185-200: LGTM!

Also applies to: 236-259, 304-309, 319-320, 341-366


📝 Summary

Summary by CodeRabbit

  • New Features

    • Added cooperative cancellation for streaming and completion requests across supported AI providers.
    • Added timeout forwarding and consistent handling of canceled or timed-out requests.
    • Model resolution now responds promptly to cancellation.
  • Bug Fixes

    • Improved error handling so cancellations produce a standardized abort error while unrelated failures retain their original details.
    • Ensured cancellation listeners are cleaned up after requests finish.

Walkthrough

The providers now support caller cancellation and positive request timeouts across streaming and completion requests. Shared helpers handle abort-aware model resolution, listener cleanup, error classification, and standardized AbortError results. Tests cover OpenAI, Anthropic, Responses, and gateway paths.

Changes

Provider cancellation and timeout handling

Layer / File(s) Summary
Abort utility contracts and validation
src/api/providers/utils/abort-signal.ts, src/api/providers/utils/__tests__/abort-signal.spec.ts, src/test-utils/settle-guard.ts
Added rejectOnAbort and resolveModelWithAbort. Tightened abort error classification to Error instances. Added settlement and listener-cleanup tests.
Streaming cancellation and model resolution
src/api/providers/opencode-go.ts, src/api/providers/unbound.ts, src/api/providers/vercel-ai-gateway.ts, src/api/providers/zoo-gateway.ts, src/api/providers/__tests__/*
Streaming requests now use per-request abort controllers. Model resolution is abort-aware. Abort failures normalize to provider-specific AbortError values, and listeners are removed after completion. Streaming tests cover signal forwarding, tool calls, usage chunks, malformed frames, and non-abort error propagation.
Completion options and error normalization
src/api/providers/opencode-go.ts, src/api/providers/unbound.ts, src/api/providers/vercel-ai-gateway.ts, src/api/providers/zoo-gateway.ts, src/api/providers/__tests__/*
Completion requests forward abort signals and positive timeouts. Timeout 0 is omitted. SDK abort and timeout errors normalize to standard abort errors while other failures retain existing handling.

Priority: ➖ Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OpencodeGoHandler
  participant resolveModelWithAbort
  participant OpenAI_or_Anthropic_SDK
  Client->>OpencodeGoHandler: createMessage(abortSignal)
  OpencodeGoHandler->>resolveModelWithAbort: resolve model
  resolveModelWithAbort-->>OpencodeGoHandler: model or AbortError
  OpencodeGoHandler->>OpenAI_or_Anthropic_SDK: stream with internal signal
  Client->>OpencodeGoHandler: abort
  OpencodeGoHandler->>OpenAI_or_Anthropic_SDK: abort internal request
  OpenAI_or_Anthropic_SDK-->>OpencodeGoHandler: abort or response
  OpencodeGoHandler-->>Client: standardized AbortError or stream
Loading

Merge Risk: ⚪ Minimal · up to 044e2

No actionable regression remains; the cancellation and timeout changes are mergeable after normal checks.

🚥 Pre-merge checks | ✅ 6 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Regression Evidence ⚠️ Warning The new provider tests cover positive timeouts, timeoutMs: 0, and omitted options, but they do not cover negative timeout values. The changed implementations explicitly promise timeoutMs <= 0 disa… Add focused provider tests for timeoutMs: -1 (or a parameterized non-positive case) for Unbound, Vercel AI Gateway, Zoo Gateway, and each Opencode Go wire format. Assert that the SDK request options omit timeout while preserving any pro…
Lifecycle Resource Cleanup ⚠️ Warning A changed Opencode Go streaming path can leak the external abort listener. createMessage registers abortListener at src/api/providers/opencode-go.ts:223-231. For the default OpenAI format, it th… Move the default OpenAI message/body construction inside the existing try/finally, or wrap all post-registration setup in a cleanup scope. Ensure every exception after addEventListener removes the exact abortListener, including conver…
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Boundaries ✅ Passed No changed path meets the security-boundary failure condition. The production diff adds abort-signal bridging, timeout options, and abort-error normalization in src/api/providers/opencode-go.ts, `un…
Persistence Integrity ✅ Passed No changed persistence path exists. The authoritative diff changes provider API request handling, abort-signal utilities, and test helpers. The added asynchronous operations await model resolution, SD…
Title check ✅ Passed The title clearly identifies the main feature and the three gateway providers changed in this unit. It is concise and relevant, although it does not mention the shared utility or stacked opencode-go w…
Description check ✅ Passed The description is detailed and covers the linked issue, implementation approach, provider behavior, test coverage, series context, and test evidence. It does not reproduce every template section or c…
Full details: Regression Evidence

Explanation

The new provider tests cover positive timeouts, timeoutMs: 0, and omitted options, but they do not cover negative timeout values. The changed implementations explicitly promise timeoutMs &lt;= 0 disables the SDK timeout and implement this with timeoutMs &gt; 0 in Unbound, Vercel AI Gateway, Zoo Gateway, and all three Opencode Go completion paths. A regression that forwards a negative timeout would pass the existing tests.

Resolution

Add focused provider tests for timeoutMs: -1 (or a parameterized non-positive case) for Unbound, Vercel AI Gateway, Zoo Gateway, and each Opencode Go wire format. Assert that the SDK request options omit timeout while preserving any provided abort signal.

Full details: Lifecycle Resource Cleanup

Explanation

A changed Opencode Go streaming path can leak the external abort listener. createMessage registers abortListener at src/api/providers/opencode-go.ts:223-231. For the default OpenAI format, it then runs convertToR1Format/convertToOpenAiMessages and convertToolsForOpenAI at lines 290-314 before entering the try/finally at lines 316-368. If message or tool conversion throws, the generator exits without running that finally, so the listener remains on a still-active task signal. convertToolsForOpenAI can throw for malformed tool input because it dereferences tool.type and tool.function.name (BaseProvider lines 35-42). The Anthropic and Responses branches do not have this gap because their yield* calls are already inside their cleanup scopes.

Resolution

Move the default OpenAI message/body construction inside the existing try/finally, or wrap all post-registration setup in a cleanup scope. Ensure every exception after addEventListener removes the exact abortListener, including conversion and request-construction failures.

  • Fix all pre-merge checks with AI
✨ 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.

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Review status

Thanks for contributing. This comment tracks the review sequence and the next action.

Current step: Awaiting fresh human maintainer or CODEOWNER approval.

Automated review is complete for the latest commit but does not replace human approval.

Review-state labels are managed by this workflow; do not edit them manually.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Sep 16, 2026
@github-actions github-actions Bot added awaiting-maintainer CodeRabbit approved; waiting for a human maintainer and removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-maintainer CodeRabbit approved; waiting for a human maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants