Skip to content

feat(api): introduce RequestConfigBuilder for SDK-agnostic abort signal support#1008

Open
easonLiangWorldedtech wants to merge 8 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-signal-core/config-builder-base
Open

feat(api): introduce RequestConfigBuilder for SDK-agnostic abort signal support#1008
easonLiangWorldedtech wants to merge 8 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/abort-signal-core/config-builder-base

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

feat(api): introduce RequestConfigBuilder for SDK-agnostic abort signal support

Problem

Currently each provider manually constructs its request config by spreading { signal: metadata?.abortSignal, headers: ... } — a repetitive pattern that:

  1. Duplicates logic across every provider implementation
  2. Is error-prone — easy to forget or mistype the spread
  3. Doesn't handle SDK differences well — OpenAI uses queryParams, Bedrock uses body, Anthropic uses apiVersion
  4. Lacks immutability guarantees — direct spreading can accidentally mutate original config objects

Proposed Solution

Introduce RequestConfigBuilder — a generic, SDK-agnostic request configuration builder that provides a fluent API for building type-safe request configurations.

Usage Example

const config = new RequestConfigBuilder()
    .addAbortSignal(metadata)
    .addHeaders({ "X-Custom-Header": "value" })
    .setOption("modelId", "claude-3-opus")
    .build()

Key Features

  • Fluent chainable API — concise, readable code
  • Generic type safetyTOptions extends Record<string, any> ensures compile-time correctness per SDK
  • Zero runtime overhead — Generics erased at compile time; mergeAbortSignals() returns primary directly when no secondary provided
  • Immutability guaranteedbuild() returns shallow copy
  • Defensive by default — undefined/empty values are silently skipped, never thrown

API Methods

Instance Methods (all chainable)

  • addAbortSignal(metadata?) — Extracts abort signal from metadata and adds it to config
  • addHeaders(headers?) — Merges custom headers (empty objects skipped, no default param overhead)
  • setOption(key, value) — Type-safe single option setter
  • getOption(key) — Get an option by key
  • build() — Returns shallow copy for immutability; undefined if no options set
  • addMergedSignal(internalController, metadata?, timeoutMs?) — Merge internal controller + external signal with timeout support (for providers that maintain their own AbortController)

Static Methods

  • fromMetadata(metadata?, extraOptions?) — Quick factory for simple signal + options scenarios
  • mergeAbortSignals(primary, secondary?) — Combines two signals (returns primary directly when no secondary — zero overhead)

Files to Add/Modify

File Description
src/api/providers/config-builder/request-config-builder.ts Core builder class (~147 lines) with generic type support, chainable methods, and static utilities
src/api/providers/utils/abort-signal.ts Shared utility functions: mergeAbortSignalAndTimeout() + mergeAbortSignals() (~49 lines)
src/api/providers/__tests__/request-config-builder.spec.ts Comprehensive test suite (~523 lines) covering all methods and edge cases
src/api/providers/utils/__tests__/abort-signal.spec.ts Utility function tests (~93 lines)
src/api/providers/config-builder/README.md Full documentation with architecture diagram, usage examples for OpenAI/Bedrock/Anthropic, and extension guide (~503 lines)
src/api/providers/index.ts Export RequestConfigBuilder from the providers barrel

Design Principles

  1. Immutabilitybuild() returns a shallow copy; constructor copies defaultOptions
  2. Defensive programming — undefined/empty values are silently skipped, never thrown
  3. Generic type safetyTOptions extends Record<string, any> ensures compile-time correctness per SDK
  4. Zero runtime overhead — Generics erased at compile time; mergeAbortSignals() returns primary directly when no secondary provided
  5. Extensibility — New SDKs extend the base class and add only their specific methods

Key Improvements vs Original PR #701 Draft

Issue Status
addHeaders(headers: Record<string, string> = {}) default param overhead ✅ Fixed — changed to optional param headers?: Record<string, string>
mergeAbortSignals([single]) creates unnecessary AbortController ✅ Fixed — returns primary directly when no secondary
No provider-specific merge method ✅ Added addMergedSignal() for providers with internal AbortController
timeoutMs ≤ 0 behavior inconsistent across providers ✅ Fixed in utility function — treated as "disabled" not "abort immediately"

Relationship to Other PRs/Issues

Next Steps (after merge)

  1. Refactor existing providers (bedrock.ts, openai-codex.ts, etc.) to use addMergedSignal() instead of manual signal merging
  2. Create SDK-specific builder classes (OpenAiRequestConfigBuilder, BedrockRequestConfigBuilder) with provider-specific methods like addPath(), addQueryParams(), setApiVersion()
  3. Add integration tests verifying builders work correctly with actual SDK calls

Related

Related to #404 — provides the infrastructure foundation for consistent abort signal handling across all providers. Actual fix will be implemented when refactoring individual providers in a follow-up PR.
Closes #1007

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a reusable RequestConfigBuilder for fluent setup of headers, options, and cancellation signals.
    • Added abort-signal utilities to combine external signals with optional timeout-based abortion and cleanup handling.
    • Exposed the request configuration builder through the provider API.
  • Documentation

    • Added a new documentation page with examples, API reference, and guidance for abort-signal handling and builder extension.
  • Tests

    • Added comprehensive test coverage for builder chaining, shallow-copy immutability, signal merging, timeout behavior, cleanup execution, and edge cases.

…nfiguration

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

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

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

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a7ab38d3-3322-44a9-980c-22088a6f0532

📥 Commits

Reviewing files that changed from the base of the PR and between 7e9d8d4 and c0a7e12.

📒 Files selected for processing (2)
  • src/api/providers/__tests__/request-config-builder.spec.ts
  • src/api/providers/config-builder/request-config-builder.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/api/providers/tests/request-config-builder.spec.ts
  • src/api/providers/config-builder/request-config-builder.ts

📝 Walkthrough

Walkthrough

Changes

The provider layer adds a generic fluent request configuration builder, timeout-aware abort-signal utilities, public exports, comprehensive Vitest coverage, and documentation covering usage, extension, and API behavior.

Request configuration and abort handling

Layer / File(s) Summary
Abort-signal composition utilities
src/api/providers/utils/abort-signal.ts, src/api/providers/utils/__tests__/abort-signal.spec.ts
Adds timeout and multi-signal merging with cleanup behavior and tests for abort propagation and timer handling.
Typed request builder and signal wiring
src/api/providers/config-builder/request-config-builder.ts, src/api/providers/index.ts, src/api/providers/__tests__/request-config-builder.spec.ts
Adds fluent metadata, header, signal, and typed-option configuration, immutable builds, static helpers, public export, and integration coverage.
Builder usage and API documentation
src/api/providers/config-builder/README.md
Documents quick starts, abort behavior, generics, SDK extension patterns, API methods, tests, compatibility notes, and design principles.

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

Sequence Diagram(s)

sequenceDiagram
  participant HandlerMetadata
  participant RequestConfigBuilder
  participant AbortSignalUtilities
  participant RequestConfig
  HandlerMetadata->>RequestConfigBuilder: provide abort signal and request metadata
  RequestConfigBuilder->>AbortSignalUtilities: merge controller, metadata signal, and timeout
  AbortSignalUtilities-->>RequestConfigBuilder: return merged signal and cleanup
  RequestConfigBuilder-->>RequestConfig: build shallow-copied options
Loading

Possibly related issues

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: edelauna

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the new RequestConfigBuilder and its abort-signal focus, matching the main change.
Description check ✅ Passed The description is largely complete and covers the feature, implementation, and issue linkage, despite lacking a dedicated test procedure.
Linked Issues check ✅ Passed The code changes implement the builder, abort-signal utilities, tests, docs, and export requested by #1007.
Out of Scope Changes check ✅ Passed All added files support the new builder feature; no unrelated code changes are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 24, 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

🧹 Nitpick comments (3)
src/api/providers/config-builder/README.md (2)

196-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Documented mergeAbortSignals implementation doesn't match the actual code.

This section shows a hand-written implementation (manual AbortController + addEventListener, with a special case returning primarySignal unchanged when secondarySignal is already aborted). The real implementation in abort-signal.ts delegates to native AbortSignal.any(), which behaves differently — notably, it will immediately reflect an already-aborted secondary signal, the opposite of what this doc claims. This will mislead anyone extending the builder based on this doc.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/config-builder/README.md` around lines 196 - 221, Update
the “How mergeAbortSignals Works” section to accurately document the actual
implementation in mergeAbortSignals and its use of native AbortSignal.any().
Remove the hand-written AbortController example and state that any
already-aborted input, including secondarySignal, causes the returned signal to
be aborted immediately.

479-495: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

API reference is missing addMergedSignal / mergeAbortSignalAndTimeout.

The Instance/Static Methods tables don't document addMergedSignal, even though it's the method intended to address issue #404 (merging a provider's internal controller with an external abort signal and optional timeout). Worth adding given it's a key part of this PR's purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/config-builder/README.md` around lines 479 - 495, Update
the README API reference tables for the configuration builder to document the
missing addMergedSignal instance method and mergeAbortSignalAndTimeout static
method, including their parameters, return types, and behavior for combining
provider/external abort signals with an optional timeout. Keep the descriptions
aligned with the actual method signatures and implementation.
src/api/providers/config-builder/request-config-builder.ts (1)

42-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Header merge is case-sensitive; HTTP header names are not.

addHeaders merges via plain object spread on raw key strings. Differently-cased keys (e.g. "x-custom" vs "X-Custom") won't be recognized as the same header, so a later addHeaders call intended to override an earlier one could instead add a duplicate header with different casing sent to the wire.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/config-builder/request-config-builder.ts` around lines 42 -
50, Update addHeaders to merge HTTP header names case-insensitively, so
differently-cased keys are treated as the same header and later values override
earlier ones. Preserve the existing headers and options behavior while ensuring
the resulting headers do not contain duplicate names differing only by casing.
🤖 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/config-builder/request-config-builder.ts`:
- Around line 63-73: Update RequestConfigBuilder.addMergedSignal and build so
the internal cleanup handle is not stored in or returned as part of the SDK
options object. Expose cleanup separately through the builder’s public result or
accessor, while preserving the merged signal in the built options and existing
behavior when no merged signal was added.

---

Nitpick comments:
In `@src/api/providers/config-builder/README.md`:
- Around line 196-221: Update the “How mergeAbortSignals Works” section to
accurately document the actual implementation in mergeAbortSignals and its use
of native AbortSignal.any(). Remove the hand-written AbortController example and
state that any already-aborted input, including secondarySignal, causes the
returned signal to be aborted immediately.
- Around line 479-495: Update the README API reference tables for the
configuration builder to document the missing addMergedSignal instance method
and mergeAbortSignalAndTimeout static method, including their parameters, return
types, and behavior for combining provider/external abort signals with an
optional timeout. Keep the descriptions aligned with the actual method
signatures and implementation.

In `@src/api/providers/config-builder/request-config-builder.ts`:
- Around line 42-50: Update addHeaders to merge HTTP header names
case-insensitively, so differently-cased keys are treated as the same header and
later values override earlier ones. Preserve the existing headers and options
behavior while ensuring the resulting headers do not contain duplicate names
differing only by casing.
🪄 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: b693b2e0-7220-4c8d-8f69-b87c93c8adb4

📥 Commits

Reviewing files that changed from the base of the PR and between 2634d5c and cf41486.

📒 Files selected for processing (6)
  • src/api/providers/__tests__/request-config-builder.spec.ts
  • src/api/providers/config-builder/README.md
  • src/api/providers/config-builder/request-config-builder.ts
  • src/api/providers/index.ts
  • src/api/providers/utils/__tests__/abort-signal.spec.ts
  • src/api/providers/utils/abort-signal.ts

Comment thread src/api/providers/config-builder/request-config-builder.ts
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/abort-signal-core/config-builder-base branch from 7e9d8d4 to c0a7e12 Compare July 24, 2026 18:43
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 24, 2026
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): introduce RequestConfigBuilder for SDK-agnostic abort signal support

2 participants