Skip to content

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

Open
easonLiangWorldedtech wants to merge 5 commits into
mainfrom
feat/abort-signal-core/config-builder
Open

feat(api): introduce RequestConfigBuilder for SDK-agnostic abort signal support#2
easonLiangWorldedtech wants to merge 5 commits into
mainfrom
feat/abort-signal-core/config-builder

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Owner

Summary

This PR introduces RequestConfigBuilder — a generic, SDK-agnostic request configuration builder that provides a fluent API for building type-safe request configurations. It serves as the second foundation for abort signal support in Zoo-Code, complementing #674 which wired the AbortSignal through the core metadata layer.

Why this builder?

#674 successfully added abortSignal?: AbortSignal to ApiHandlerCreateMessageMetadata and wired it from Task.ts. However, each provider still 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

This builder solves all of that with a unified, chainable API:

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

Changes

File Description
src/api/providers/config-builder/request-config-builder.ts Core builder class (136 lines) with generic type support, chainable methods, and static utilities
src/api/providers/__tests__/request-config-builder.spec.ts Comprehensive test suite (461 lines, 50+ tests) covering all methods and edge cases
src/api/providers/config-builder/README.md Full documentation with architecture diagram, usage examples for OpenAI/Bedrock/Anthropic, and extension guide
src/api/providers/index.ts Export RequestConfigBuilder from the providers barrel

API Overview

Instance Methods (all chainable)

  • addAbortSignal(metadata?) — Extracts abort signal from metadata and adds it to config
  • addHeaders(headers) — Merges custom headers (empty objects skipped)
  • 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

Static Methods

  • fromMetadata(metadata?, extraOptions?) — Quick factory for simple signal + options scenarios
  • mergeAbortSignals(primary, secondary?) — Combines two signals (abort when either fires)

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
  5. Extensibility — New SDKs extend the base class and add only their specific methods

Pros & Cons

✅ Pros

  • Unified interface across all SDKs (OpenAI, Bedrock, Anthropic, Vertex AI)
  • Type-safe via TypeScript generics (TOptions)
  • Fluent chainable API — concise, readable code
  • Immutability guaranteedbuild() returns shallow copy
  • Defensive by default — no errors on undefined/empty inputs
  • Easy to extend — new SDK support = one extended class with 2-3 methods

⚠️ Cons

  • Extra abstraction layer — may feel like over-engineering for simple single-provider setups
  • Learning curve — team needs to understand generics + builder pattern
  • Migration period — existing providers still use manual config spreading; gradual migration needed
  • SDK-specific builders not yet implemented — only the base class is included (OpenAI/Bedrock/Anthropic extensions are documented as future work in README)

Relationship to other PRs

PR Status Role
#434 Closed Original attempt: manually added signal to 59 files across 16+ providers. Too scattered, hard to maintain.
#674 Merged ✅ Core plumbing: added abortSignal to metadata interface and wired it through Task.ts. This PR builds on top of that foundation.
This PR Open Builder pattern: unified, type-safe config construction for all providers going forward.

Next Steps (after merge)

  1. Create SDK-specific builder classes (OpenAiRequestConfigBuilder, BedrockRequestConfigBuilder, etc.) with provider-specific methods like addPath(), addQueryParams(), setApiVersion()
  2. Gradually migrate existing providers to use the builder pattern
  3. Add integration tests verifying builders work correctly with actual SDK calls

Related

Closes #434 (supersedes the manual approach with a reusable pattern)

@easonLiangWorldedtech easonLiangWorldedtech force-pushed the feat/abort-signal-core/config-builder branch from cac1a1c to c2dd146 Compare June 26, 2026 16:10
…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
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the feat/abort-signal-core/config-builder branch from c2dd146 to ba54c38 Compare June 26, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants