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:
- Duplicates logic across every provider implementation
- Is error-prone — easy to forget or mistype the spread
- Doesn't handle SDK differences well — OpenAI uses
queryParams, Bedrock uses body, Anthropic uses apiVersion
- 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 safety —
TOptions 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 guaranteed —
build() 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
- Immutability —
build() returns a shallow copy; constructor copies defaultOptions
- Defensive programming — undefined/empty values are silently skipped, never thrown
- Generic type safety —
TOptions 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
- 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)
- Refactor existing providers (bedrock.ts, openai-codex.ts, etc.) to use
addMergedSignal() instead of manual signal merging
- Create SDK-specific builder classes (
OpenAiRequestConfigBuilder, BedrockRequestConfigBuilder) with provider-specific methods like addPath(), addQueryParams(), setApiVersion()
- Add integration tests verifying builders work correctly with actual SDK calls
Related
Related #404 (provides foundation for consistent abort signal handling across all providers)
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:queryParams, Bedrock usesbody, Anthropic usesapiVersionProposed Solution
Introduce
RequestConfigBuilder— a generic, SDK-agnostic request configuration builder that provides a fluent API for building type-safe request configurations.Usage Example
Key Features
TOptions extends Record<string, any>ensures compile-time correctness per SDKmergeAbortSignals()returns primary directly when no secondary providedbuild()returns shallow copyAPI Methods
Instance Methods (all chainable)
addAbortSignal(metadata?)— Extracts abort signal from metadata and adds it to configaddHeaders(headers?)— Merges custom headers (empty objects skipped, no default param overhead)setOption(key, value)— Type-safe single option settergetOption(key)— Get an option by keybuild()— Returns shallow copy for immutability;undefinedif no options setaddMergedSignal(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 scenariosmergeAbortSignals(primary, secondary?)— Combines two signals (returns primary directly when no secondary — zero overhead)Files to Add/Modify
src/api/providers/config-builder/request-config-builder.tssrc/api/providers/utils/abort-signal.tsmergeAbortSignalAndTimeout()+mergeAbortSignals()(~49 lines)src/api/providers/__tests__/request-config-builder.spec.tssrc/api/providers/utils/__tests__/abort-signal.spec.tssrc/api/providers/config-builder/README.mdsrc/api/providers/index.tsRequestConfigBuilderfrom the providers barrelDesign Principles
build()returns a shallow copy; constructor copies defaultOptionsTOptions extends Record<string, any>ensures compile-time correctness per SDKmergeAbortSignals()returns primary directly when no secondary providedKey Improvements vs Original PR #701 Draft
addHeaders(headers: Record<string, string> = {})default param overheadheaders?: Record<string, string>mergeAbortSignals([single])creates unnecessary AbortControlleraddMergedSignal()for providers with internal AbortControllerRelationship to Other PRs/Issues
abortSignalto metadata interface and wired it through Task.ts. This builder builds on top of that foundation.Next Steps (after merge)
addMergedSignal()instead of manual signal mergingOpenAiRequestConfigBuilder,BedrockRequestConfigBuilder) with provider-specific methods likeaddPath(),addQueryParams(),setApiVersion()Related
Related #404 (provides foundation for consistent abort signal handling across all providers)