Bridge API
Complete API reference for the Bridge class - the core component for connecting frontend and backend adapters.
Constructor
Section titled “Constructor”new Bridge(frontend, backend, config?)
Section titled “new Bridge(frontend, backend, config?)”Creates a new Bridge instance.
Parameters:
frontend: FrontendAdapter- Adapter for parsing input formatbackend: BackendAdapter | Router- Adapter (or router) that executes requests against an AI providerconfig?: Partial<BridgeConfig>- Optional configuration (see BridgeConfig)
Returns: Bridge instance
Example:
import { Bridge } from '@johnhenry/aimatey-core';import { OpenAIFrontendAdapter } from '@johnhenry/aimatey-frontend/openai';import { AnthropicBackendAdapter } from '@johnhenry/aimatey-backend/anthropic';
const bridge = new Bridge( new OpenAIFrontendAdapter(), new AnthropicBackendAdapter({ apiKey: process.env.ANTHROPIC_API_KEY }), { timeout: 30000, retries: 3 });Methods
Section titled “Methods”chat(request, options?)
Section titled “chat(request, options?)”Execute a chat completion request.
Parameters:
request: any- Request in frontend adapter formatoptions?: RequestOptions- Per-request overrides (see RequestOptions)
Returns: Promise<any> - Response in frontend adapter format
Throws:
ProviderError- If the provider API returns an errorNetworkError- If the network request failsValidationError- If request validation failsAdapterError(CONNECTION_TIMEOUT/PROVIDER_TIMEOUT) - If the request times outAdapterError(ROUTING_FAILED) - Ifoptions.backendnames a backend that is not registered
Example:
const response = await bridge.chat({ model: 'gpt-4', messages: [ { role: 'user', content: 'Hello!' } ]});
console.log(response.choices[0].message.content);chatStream(request, options?)
Section titled “chatStream(request, options?)”Execute a streaming chat completion request.
Parameters:
request: any- Request in frontend adapter format (withstream: true)options?: RequestOptions- Per-request overrides (see RequestOptions)
Returns: AsyncGenerator<any> - Stream of chunks in frontend adapter format. chatStream()
is an async generator, so it returns the stream synchronously - do not await the call itself.
Throws:
ProviderError- If the provider API returns an errorStreamError- If stream parsing failsValidationError- If request validation failsAdapterError(ROUTING_FAILED) - Ifoptions.backendnames a backend that is not registered
Example:
const stream = bridge.chatStream({ model: 'gpt-4', messages: [{ role: 'user', content: 'Count to 10' }], stream: true});
for await (const chunk of stream) { const content = chunk.choices?.[0]?.delta?.content; if (content) { process.stdout.write(content); }}use(middleware)
Section titled “use(middleware)”Add middleware to the bridge.
Parameters:
middleware: Middleware- Middleware to add to the chain
Returns: Bridge (for chaining)
Example:
import { createLoggingMiddleware, createCachingMiddleware } from '@johnhenry/aimatey-middleware';
bridge .use(createLoggingMiddleware({ level: 'info' })) .use(createCachingMiddleware({ ttl: 3_600_000 })); // ttl is millisecondsexecuteIR(request, options?)
Section titled “executeIR(request, options?)”Execute a request using the IR format directly (bypassing the frontend adapter).
Parameters:
request: IRChatRequest- Request in IR formatoptions?: RequestOptions- Per-request overrides (see RequestOptions)
Returns: Promise<IRChatResponse> - Response in IR format
Example:
const irResponse = await bridge.executeIR({ messages: [{ role: 'user', content: 'Hello' }], parameters: { model: 'claude-haiku-4-5-20251001', temperature: 0.7 }, metadata: { requestId: 'req_1', timestamp: Date.now() }});
console.log(irResponse.message.content);executeIRStream(request, options?)
Section titled “executeIRStream(request, options?)”Execute a streaming request using IR format directly. This is an async generator - it returns the stream synchronously, so the call itself is not awaited.
Parameters:
request: IRChatRequest- Request in IR format with streaming enabledoptions?: RequestOptions- Per-request overrides (see RequestOptions)
Returns: IRChatStream - AsyncGenerator<IRStreamChunk>
Example:
const stream = bridge.executeIRStream({ messages: [{ role: 'user', content: 'Hello' }], parameters: { model: 'claude-haiku-4-5-20251001' }, metadata: { requestId: 'req_2', timestamp: Date.now() }, stream: true});
for await (const chunk of stream) { if (chunk.type === 'content') { process.stdout.write(chunk.delta); }}clone(config)
Section titled “clone(config)”frontend and backend are readonly - a bridge cannot be re-pointed at a
different adapter after construction. To run the same request through another
provider, build a second bridge; to change only configuration, clone() returns
a new bridge with the same adapters and middleware and a merged config.
Parameters:
config: Partial<BridgeConfig>- Configuration overrides
Returns: Bridge - A new bridge instance
Example:
import { OpenAIBackendAdapter } from '@johnhenry/aimatey-backend/openai';
// Same adapters, longer timeoutconst patientBridge = bridge.clone({ timeout: 120000 });
// A different backend means a new bridgeconst openaiBridge = new Bridge( bridge.frontend, new OpenAIBackendAdapter({ apiKey: process.env.OPENAI_API_KEY }));decide(state, questions, options?)
Section titled “decide(state, questions, options?)”Answer typed questions about a state in one call. Needs a backend whose
capabilities.decisions is true and that implements decide(); it need not
support chat. The request is pre-flight validated against the backend’s declared
types and limits, then runs through the decision middleware chain.
Parameters:
state: unknown- Text, a structured object or an arrayquestions: Record<string, IRDecisionQuestion>- Namedchoice/score/noulquestionsoptions?: DecisionOptions-model,signal,metadata,principal,custom
Returns: Promise<IRDecisionResponse> - answers keyed by question name, plus model, usage
const { answers } = await bridge.decide(ticket, { refund: { type: 'noul', instructions: 'Does the customer ask for a refund?' },});See the Decisions guide.
decideFrom(request, options?)
Section titled “decideFrom(request, options?)”Like decide(), for callers that speak a provider’s wire format: the frontend’s
decisionToIR() builds the IR request and decisionFromIR() shapes the
response. Throws UNSUPPORTED_FEATURE when the frontend has no decision hooks
(TypeSafeFrontendAdapter, LayaFrontendAdapter, VercelDecideFrontendAdapter
and OpenRouterDecisionsFrontendAdapter do).
decideBatch(states, questions, options?)
Section titled “decideBatch(states, questions, options?)”The same questions over many states, in input order, through exactly what
decide() does. options extends DecisionOptions with concurrency (default:
the backend’s decisionLimits.maxConcurrency, else 4), onProgress(done, total)
and onError ('throw', the default, or 'collect', which returns a
PromiseSettledResult per state).
useDecision(middleware)
Section titled “useDecision(middleware)”Register a DecisionMiddleware ((request, next) => Promise<IRDecisionResponse>),
outermost first. Separate from use(), which is the chat stack. Returns the bridge.
on(event, handler)
Section titled “on(event, handler)”Subscribe to bridge events.
Parameters:
event: BridgeEventType | '*'- Event name, or'*'for every eventhandler: BridgeEventListener- Event handler function
Returns: Bridge (for chaining). off() and once() have the same shape.
Events:
BridgeEventType declares eleven event names (see BridgeEventType),
but only these six are emitted by Bridge today:
request:start- Fired before a non-streaming request is sentrequest:success- Fired after a successful non-streaming responserequest:error- Fired when a non-streaming request failsstream:start- Fired when streaming startsstream:complete- Fired when streaming finishesstream:error- Fired when a stream fails
The remaining names (request:cancelled, stream:chunk, backend:selected,
backend:failover, middleware:executed) are reserved and never fire.
Example:
bridge.on('request:start', (event) => { console.log('Request:', event.request.messages);});
bridge.on('request:success', (event) => { console.log('Response tokens:', event.response?.usage?.totalTokens);});
bridge.on('request:error', (event) => { console.error('Error:', event.error?.message);});Properties
Section titled “Properties”frontend
Section titled “frontend”Type: FrontendAdapter
Read-only
The frontend adapter instance the bridge was constructed with.
backend
Section titled “backend”Type: BackendAdapter | Router
Read-only
The backend adapter (or router) the bridge was constructed with.
config
Section titled “config”Type: BridgeConfig
Read-only
The resolved bridge configuration.
getMiddleware() / getStreamingMiddleware()
Section titled “getMiddleware() / getStreamingMiddleware()”There is no middlewares property. The registered middleware is read back through
methods instead:
bridge.getMiddleware(); // readonly Middleware[]bridge.getStreamingMiddleware(); // readonly StreamingMiddleware[]BridgeConfig
Section titled “BridgeConfig”Configuration options for Bridge, passed as the third constructor argument.
interface BridgeConfig { /** Enable debug mode with detailed logging (default: false) */ debug?: boolean;
/** Global request timeout in milliseconds (default: 30000) */ timeout?: number;
/** Maximum retries for transient failures (default: 0) */ retries?: number;
/** Default model to use if the request does not specify one */ defaultModel?: string;
/** Router configuration, when the backend is a Router */ routerConfig?: Partial<RouterConfig>;
/** Add a request ID to metadata if not present (default: true) */ autoRequestId?: boolean;
/** Custom configuration options */ custom?: Record<string, unknown>;}RequestOptions
Section titled “RequestOptions”Per-request overrides, passed as the second argument to chat() and chatStream().
interface RequestOptions { /** Request timeout in milliseconds */ timeout?: number;
/** AbortSignal for request cancellation */ signal?: AbortSignal;
/** Backend override, by registered name (router only) */ backend?: string;
/** Additional metadata merged into `metadata.custom` */ metadata?: Record<string, unknown>;
/** Skip middleware execution for this request (default: false) */ skipMiddleware?: boolean;
/** Custom request options */ custom?: Record<string, unknown>;}Per-request backend selection
Section titled “Per-request backend selection”backend picks a specific backend for one request, by the name it was registered
under. It only applies when the bridge’s backend is a Router - with a single
backend adapter there is no routing to override and the option is ignored.
const router = new Router({ routingStrategy: 'explicit', defaultBackend: 'openai' });router.register('openai', new OpenAIBackendAdapter({ apiKey: process.env.OPENAI_API_KEY }));router.register('anthropic', new AnthropicBackendAdapter({ apiKey: process.env.ANTHROPIC_API_KEY }));
const bridge = new Bridge(new OpenAIFrontendAdapter(), router);
// Routed by the router's configured strategyawait bridge.chat({ model: 'gpt-4', messages });
// Forced to Anthropic for this request onlyawait bridge.chat({ model: 'gpt-4', messages }, { backend: 'anthropic' });The bridge writes the override onto metadata.custom.backend, which is the channel
the router reads its explicit routing decision from. It is applied last, so it takes
precedence over a metadata.custom.backend already present on the request and over a
backend key passed through options.metadata.
A name that is not registered is rejected up front with an AdapterError carrying
ErrorCode.ROUTING_FAILED, so a typo cannot be served by a different provider:
await bridge.chat(request, { backend: 'antropic' });// AdapterError: Requested backend 'antropic' is not registered.// Registered backends: openai, anthropicA backend that is registered but is currently unhealthy or has an open circuit breaker is not an error - the router’s normal fallback applies.
BridgeEventType
Section titled “BridgeEventType”The event names accepted by on() / off() / once(). (BridgeEvent is a
different thing - the interface describing an event object.)
type BridgeEventType = | 'request:start' | 'request:success' | 'request:error' | 'request:cancelled' | 'stream:start' | 'stream:chunk' | 'stream:complete' | 'stream:error' | 'backend:selected' | 'backend:failover' | 'middleware:executed';Only request:start, request:success, request:error, stream:start,
stream:complete and stream:error are actually emitted.
Error Handling
Section titled “Error Handling”Bridge can throw the following errors:
Every error thrown by a bridge derives from AdapterError, which carries a
machine-readable code, a category, and an isRetryable flag.
ProviderError
Section titled “ProviderError”Thrown when the provider API returns an error response.
import { AdapterError, ProviderError } from '@johnhenry/aimatey-errors';
try { await bridge.chat(request);} catch (error) { if (error instanceof ProviderError) { console.log('Provider failed:', error.provenance.backend); console.log('Code:', error.code); console.log('Message:', error.message); } else if (error instanceof AdapterError) { console.log('Adapter error:', error.code, error.category); }}ValidationError
Section titled “ValidationError”Thrown when request validation fails. The per-field detail lives on
validationDetails.
import { ValidationError } from '@johnhenry/aimatey-errors';
try { await bridge.chat(invalidRequest);} catch (error) { if (error instanceof ValidationError) { console.log('Validation failed:', error.validationDetails); }}Timeouts
Section titled “Timeouts”There is no TimeoutError class. A timeout surfaces as an AdapterError whose
code is CONNECTION_TIMEOUT (the network call timed out) or PROVIDER_TIMEOUT
(the provider reported a timeout).
import { AdapterError, ErrorCode } from '@johnhenry/aimatey-errors';
const bridge = new Bridge(frontend, backend, { timeout: 5000 });
try { await bridge.chat(request);} catch (error) { if ( error instanceof AdapterError && (error.code === ErrorCode.CONNECTION_TIMEOUT || error.code === ErrorCode.PROVIDER_TIMEOUT) ) { console.log('Request timed out after 5s'); }}Advanced Usage
Section titled “Advanced Usage”Custom Backend Adapter
Section titled “Custom Backend Adapter”import type { BackendAdapter, AdapterMetadata, IRChatRequest, IRChatResponse} from '@johnhenry/aimatey-types';
class CustomBackend implements BackendAdapter { readonly metadata: AdapterMetadata = { name: 'custom', version: '1.0.0', provider: 'Custom', capabilities: { streaming: true, multiModal: false, systemMessageStrategy: 'in-messages', supportsMultipleSystemMessages: true } };
fromIR(request: IRChatRequest): unknown { return request; }
toIR(_response: unknown, originalRequest: IRChatRequest): IRChatResponse { return { message: { role: 'assistant', content: 'Custom response' }, finishReason: 'stop', usage: { promptTokens: 5, completionTokens: 5, totalTokens: 10 }, metadata: originalRequest.metadata }; }
async execute(request: IRChatRequest): Promise<IRChatResponse> { return this.toIR(null, request); }
async *executeStream(request: IRChatRequest) { yield { type: 'start' as const, sequence: 0, metadata: request.metadata }; yield { type: 'content' as const, sequence: 1, delta: 'Custom' }; yield { type: 'done' as const, sequence: 2, finishReason: 'stop' as const }; }}
const bridge = new Bridge( new OpenAIFrontendAdapter(), new CustomBackend());Middleware Stack
Section titled “Middleware Stack”import { createLoggingMiddleware, createCachingMiddleware, createRetryMiddleware, createCostTrackingMiddleware} from '@johnhenry/aimatey-middleware';
const bridge = new Bridge(frontend, backend);
// Add middleware in orderbridge .use(createLoggingMiddleware({ level: 'info' })) .use(createRetryMiddleware({ maxAttempts: 3 })) .use(createCachingMiddleware({ ttl: 3_600_000 })) // ttl is milliseconds .use(createCostTrackingMiddleware({ dailyThreshold: 100 }));See Also
Section titled “See Also”- Router API - Multi-backend routing
- Middleware API - Middleware reference
- Error Handling - Error types
- Tutorial: Simple Bridge - Getting started
- Examples - Code examples