Skip to content

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

Description

@easonLiangWorldedtech

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 #404 (provides foundation for consistent abort signal handling across all providers)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions