Repository navigation
feat(api): abort signal support for unbound, vercel-ai-gateway, zoo-gateway - #1653
easonLiangWorldedtech wants to merge 12 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: 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.
- 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: 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.
…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: 62f596c); 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.
|
| Layer / File(s) | Summary |
|---|---|
Model resolution and streaming cancellation 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__/*.spec.ts |
The handlers resolve models with abort handling and bridge caller signals to streaming requests. They normalize recognized cancellations and remove listeners. Tests cover lookup and stream cancellation, listener cleanup, and stream-frame behavior. |
Completion cancellation and timeout options 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__/*.spec.ts |
Completion requests forward abort signals and positive timeouts. They omit nonpositive timeouts and normalize recognized abort and timeout errors. Tests cover request options, cancellation, and calls without options. |
Priority:
Estimated code review effort: 4 (Complex) | ~55 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant Caller
participant ProviderHandler
participant resolveModelWithAbort
participant OpenAI SDK
ProviderHandler->>resolveModelWithAbort: Resolve model with caller signal
resolveModelWithAbort-->>ProviderHandler: Return model or abort error
ProviderHandler->>OpenAI SDK: Start stream with bridged signal
Caller->>OpenAI SDK: Abort request signal
OpenAI SDK-->>ProviderHandler: Stream result or abort error
Possibly related issues
- RouterProvider: signal-aware shared-flight model discovery so a cancelled waiter can stop the fetch #2202: The change handles caller cancellation during provider model resolution. The issue tracks cancellation of the shared discovery fetch after its last waiter cancels, which is a separate follow-up.
Merge Risk: 
Minimal · up to 76f0d
-
No actionable issue remains from this review; the change is ready for normal merge checks.
Security Architecture Review
-
Security architecture risk: 🔵 Low · up to
07aebThe reviewed paths retain existing authentication and request permissions while honoring cancellation. Remaining uncertainty concerns how quickly in-flight work stops when a consumer exits before a response arrives.
Retained concerns
No architecture-level concerns identified.Security review details
-
Security Blast Radius
- inferred — The added control affects requests subscribing to the caller's signal. Controllers and completion options are invocation-local rather than stored as handler-wide cancellation state. Reusing a signal can intentionally cancel multiple subscribing requests, but the inspected changes do not grant new credential or tool authority.
Trust Boundaries and Controls
- observed — Caller cancellation becomes transport-control state: the shared helper checks the signal before initiating lookup, and provider request paths forward a caller signal or request-local controller signal. These controls do not alter credential selection or convert cancellation into permission to perform protected work.
Resilience and Maintainability Implications
- inferred — Immediate resource release from iterator.return() alone while Zoo's initial SDK await is pending remains unverified. The pending-await structure predates this PR, while the PR adds an explicit external-signal cancellation route. This is a bounded cleanup uncertainty, not evidence of newly expanded authority or a verified security regression.
-
Pre-merge checks: 
6 · 
2
-
Failed checks: 2 warnings Passed checks: 6 passed-
Full details: Description check
-
Explanation
The description provides detailed implementation context, references issue
#404, explains testing, and records verification results. It does not reproduce every template section, such as the completed checklist and explicit documentation-impact entry, but it contains the critical information needed for review.
Full details: Security Boundaries
-
Explanation
No changed path meets the security failure conditions. The provider changes only add abort-signal bridging, validated timeout forwarding, cancellation normalization, and cleanup in
unbound.ts,vercel-ai-gateway.ts,zoo-gateway.ts, andopencode-go.ts.hasUsableTimeoutrejects non-finite and non-positive values, whiletoSdkTimeoutMsrounds and caps usable values. The changes do not expose secrets or PII, execute input, or remove authentication or model controls. Zoo Gateway still callsensureAuthenticated()for non-aborted requests; the new pre-abort return performs no request or privileged action.
Full details: Persistence Integrity
-
Explanation
No changed persistence path exists. The implementation changes only model resolution, abort bridging, timeout options, and provider request/stream handling in the four provider files. For example, the changed paths await model resolution and SDK calls, then return response text or stream chunks (unbound.ts:159-307; vercel-ai-gateway.ts:90-244; zoo-gateway.ts:220-388; opencode-go.ts:245-1015). The pull request adds no persistence write, transaction, rollback, or persisted-default propagation path.
-
Full details: Regression Evidence
-
Explanation
The standing regression remains. The PR adds provider-level branches that omit the SDK timeout when
hasUsableTimeout(timeoutMs)is false: Unbound (unbound.ts:300-302), Vercel AI Gateway (vercel-ai-gateway.ts:235-237), Zoo Gateway (zoo-gateway.ts:382-384), and OpenCode Go Anthropic, Responses, and Chat Completions (opencode-go.ts:879-880,929-930,1007-1008). The focused provider specs cover positive values andtimeoutMs: 0, but they do not callcompletePromptwith a negativetimeoutMs. The shared helper test covers-5, but it does not verify the changed provider-to-SDK wiring. A regression that forwards a negative timeout would pass the provider tests.Resolution
Add focused
completePrompttests with a negativetimeoutMs. Assert that the SDK options omittimeoutfor Unbound, Vercel AI Gateway, Zoo Gateway, and OpenCode Go's Anthropic, Responses, and Chat Completions paths. In signal-forwarding cases, also assert that the abort signal remains present.
Full details: Lifecycle Resource Cleanup
-
Explanation
Zoo model resolution still leaks changed lifecycle work after cancellation. The PR changes
ZooGatewayHandler.createMessageandcompletePromptto callresolveModelWithAbort(() => this.fetchModel(), signal, ...)(src/api/providers/zoo-gateway.ts:220,356-360).resolveModelWithAbortrejects the caller but explicitly leaves the underlying lookup running (src/api/providers/utils/abort-signal.ts:157-162).RouterProvider.fetchModelstartsgetModelsandrefreshModelswithout a signal (src/api/providers/router-provider.ts:76-108). Zoo isauthScoped(src/api/providers/zoo-gateway.ts:187-189), so the model cache bypasses its single-flight path and calls the provider without a signal (src/api/providers/fetchers/modelCache.ts:230-236,433-439).ZooGatewayHandler.fetchModelsandgetZooGatewayModelsthen issue Axios model requests without an abort signal (src/api/providers/zoo-gateway.ts:191-193;src/api/providers/fetchers/zoo-gateway.ts:36-40). If cancellation occurs during catalog lookup, the caller receivesAbortErrorwhile the Axios request remains active until its independent timeout or response. A new Zoo handler can start duplicate catalog work before the first request finishes. The added tests release the mocked lookup after asserting cancellation (src/api/providers/__tests__/zoo-gateway.spec.ts:717-747,816-862), so they do not verify request cancellation or overlap cleanup.Resolution
Thread the caller signal through
RouterProvider.fetchModel,getModels/refreshModels,ZooGatewayHandler.fetchModels, andgetZooGatewayModelsinto the Axios request. Preserve single-flight behavior for non-auth-scoped providers and cancel an auth-scoped Zoo catalog request when its last waiter aborts. Add a regression test that aborts a pending Zoo catalog lookup, starts a new handler, and verifies that the canceled lookup is released without overlapping the new request.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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 @coderabbitai help to get the list of available commands.
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! |
Conflicts were only in src/api/providers/utils/abort-signal.ts and its spec. Resolved by keeping main's canonical helper: the branch carried a copy that lacked throwIfAborted (main's fetchers import it, so the merge broke tsc) and the pre-aborted branch's unhandled-rejection guard. The PR keeps only its own provider wiring. Verified on the merged tree: tsc clean, eslint clean, prettier clean, 176 tests green (abort-signal + unbound + vercel-ai-gateway + zoo-gateway specs). Co-Authored-By: Claude <noreply@anthropic.com>
|
@coderabbitai full review |
|
|
@coderabbitai full review |
|
|
@coderabbitai full review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Semantic merge of the gateway-siblings abort unit with main's changes: - opencode-go: every request site now carries both the unit's signal/timeout options and main's per-instance x-opencode-session header (taskId when present, else sessionId); spec assertions pin the combined options objects and main's session-ID tests are retained. - unbound / vercel-ai-gateway / zoo-gateway: keep the unit's abort wiring (resolveModelWithAbort / entry guard) over main's _options parameter renames; adopt main's fetchModels(_options) lint rename. - Unused mock options params renamed to _options in the merged spec. 542/542 green across the four provider specs; tsc/eslint/prettier clean.
The CI lint gate (src: pnpm run lint) flags the six mock implementations that abort internally and never read the request options: rename the unused second parameter to _options to satisfy @typescript-eslint/no-unused-vars. No behavior change; specs green.
|
@coderabbitai review |
✅ Action performedReview finished.
|
…teway-siblings # Conflicts: # src/api/providers/opencode-go.ts
…sions Merge follow-ups: the refreshed main lint rule requires hasUsableTimeout/toSdkTimeoutMs instead of forwarding timeoutMs directly (0 aborts immediately, negative/non-finite/fractional throw), applied to completePrompt in opencode-go, unbound, vercel-ai-gateway and zoo-gateway. Also removes Stryker disable comments (repo policy prohibits them; mutation gate rejects new directives) - the covered conditions are equivalent mutants documented on the PR: the pre-abort bridge branches are unreachable behind the entry guard plus rejectOnAbort race, and the inner abort-normalization throw is re-normalized by createMessage outer catch. Remaining 582 provider tests pass; tsc clean.
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
Updated this round (2026-10-13)
Merged current upstream
maininto the branch to clear the dirty state. Conflicts:vercel-ai-gateway.ts/zoo-gateway.ts(imports only — main's model-cache fetcher imports plus this branch's abort-signal utility imports, both kept) andunbound.spec.ts(main's new "model catalog" describe block coexists with this branch's appended abort/cancellation suites — both kept).Verification at this head: 535/535 green across the touched provider specs (unbound, vercel-ai-gateway, zoo-gateway, opencode-go);
tsc --noEmit→ 0 errors; per-file ESLint (--max-warnings=0) clean;prettier --checkclean on touched files.