feat(api): abort signal support for unbound, vercel-ai-gateway, zoo-gateway - #1653
easonLiangWorldedtech wants to merge 3 commits into
Conversation
…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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (11)
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:
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:
Check strict typing and exhaustive behavior across normal, boundary, error, cancellation, retry, and compatibility paths.⚙️ CodeRabbit configuration file Files:
Verify extension/webview contracts, cancellation and error propagation, VS Code lifecycle correctness, and behavior under retries and partial failure.⚙️ CodeRabbit configuration file Files:
Act as an adversarial second-opinion reviewer.⚙️ CodeRabbit configuration file Files:
🪛 ESLintsrc/api/providers/unbound.ts[error] 202-202: Unexpected any. Specify a different type. ( 🔇 Additional comments (11)
📝 SummarySummary by CodeRabbit
WalkthroughThe 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 ChangesProvider cancellation and timeout 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
Merge Risk: ⚪ Minimal · up to No actionable regression remains; the cancellation and timeout changes are mergeable after normal checks. 🚥 Pre-merge checks | ✅ 6 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (6 passed)
Full details: Regression EvidenceExplanation The new provider tests cover positive timeouts, Resolution Add focused provider tests for Full details: Lifecycle Resource CleanupExplanation A changed Opencode Go streaming path can leak the external abort listener. Resolution Move the default OpenAI message/body construction inside the existing
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Review statusThanks 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 Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Wires the external abort signal and per-request timeout into the
completePrompt(non-streaming) andcreateMessage(streaming) paths of the Unbound, Vercel AI Gateway, and Zoo Gateway providers. Stacks on the shared-util and opencode-go units of this split (resolveModelWithAbortinutils/abort-signal.ts).Each provider:
createMessage: bridgesmetadata?.abortSignal(Bedrock pattern: pre-aborted guard,{ once: true }listener, detached on completion so a task-scoped signal does not accumulate listeners) into a per-requestAbortController; model resolution runs inside the sharedresolveModelWithAbortcancellation 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: forwardsabortSignal/timeoutMsto the OpenAI SDK (timeoutMs <= 0omits the SDK timeout option, since the SDK treatstimeout: 0as an immediate abort); aborted completions andAPIConnectionTimeoutErrornormalize 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:
completePromptpass-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.createMessagebridging 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)
rejectOnAbortrace, 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).createMessagecatches use the widerisRequestAbortedcondition (aborted signal, DOM AbortError, SDK APIUserAbortError, exact "Request was aborted." message; name/message checks require a realErrorinstance).Evidence