@x12i/ai-provider-interface
v3.2.1
Published
SDK-first Provider AI Interface for router-adapter architecture. Supports sync, streaming, and batch operations with lossless raw vendor responses.
Maintainers
Readme
@x12i/ai-provider-interface
A minimal TypeScript interface library defining a normalized contract for AI-provider adapters.
It treats providers as cognitive functions:
"Given these
instructionsand thisinputData, return anoutput."
No chat, no message history — just clean, task-style aiCall operations.
🧩 What this package is
- Interface only – pure TypeScript types and interfaces, no runtime code.
- A common contract all AI providers must implement (OpenAI, Anthropic, local models, etc.).
- Designed for routers, pipelines, and reasoning engines, not chat UIs.
Your orchestration layer (router, queues, retries, rate-limits, logging) depends only on this interface, and can swap providers without changes.
🚀 Installation
npm install @x12i/ai-provider-interface
# or
yarn add @x12i/ai-provider-interface🔧 Core Concepts
The core idea is a single function:
aiCall(request: AIRequest): Promise<AIResponse>Where:
instructions: describe what the AI should do.inputData: provide the data to reason about (JSON, text, etc.).config: fine-tunes how the provider should run (max tokens, temperature, etc.).
AIRequest
export interface AIRequest {
/**
* Natural-language description of what the AI should do.
* Example: "Summarize the incidents by severity and output a JSON array."
*/
instructions: string;
/**
* The input data to reason about or transform.
* Can be text, JSON, an object, array, etc.
*/
inputData?: unknown;
/**
* Optional configuration for the call (provider-agnostic).
*/
config?: AIRequestConfig;
/**
* Optional tags/metadata for routing, logging, tracing, etc.
*/
tags?: string[];
/**
* Optional correlation / trace ID, passed through by the router.
*/
traceId?: string;
}AIRequestConfig
export interface AIRequestConfig {
maxTokens?: number;
temperature?: number;
topP?: number;
timeoutMs?: number;
// Provider-specific hints can be added via an index signature if needed
[key: string]: unknown;
}AIResponse
export interface AIResponse<Output = unknown> {
/**
* The main result of the AI call.
* This can be plain text or structured JSON, depending on the task.
*/
output: Output;
/**
* Optional raw text (if the provider always returns text and you parse it).
*/
rawText?: string;
/**
* Optional raw property (if present).
*/
raw?: string;
/**
* Optional parsed JSON content (if applicable).
*/
parsedContent?: any;
/**
* Optional content string (main content).
*/
content?: string;
/**
* Optional normalization metadata that providers may include when they normalize
* requests or configurations to match API requirements.
*
* This field is only present when normalization actually occurs.
*/
normalization?: {
/**
* Whether the request messages/format were normalized.
* For example, converting gateway messages array to API-specific format.
*/
messagesNormalized?: boolean;
/**
* Whether configuration parameters were normalized.
* This includes parameter name conversions, removals, or value adjustments.
*/
configNormalized?: boolean;
/**
* The normalized request that was actually sent to the API.
* This allows consumers to see exactly what parameters were used.
*/
normalizedRequest?: Record<string, any>;
/**
* Warnings about parameter modifications during normalization.
* Examples:
* - "Omitted temperature (not supported by gpt-5-nano)"
* - "Converted maxTokens -> max_output_tokens"
*/
warnings?: string[];
};
/**
* Optional: Full raw API response object from the provider.
* This is the primary field name for raw response preservation.
* Contains the complete, unmodified response object as received from the provider's API.
*/
fullRawResponse?: any;
/**
* Optional: Raw API response object (legacy/alternative field names).
*/
rawResponse?: any;
rawApiResponse?: any;
originalResponse?: any;
apiResponse?: any;
httpResponse?: any;
/**
* Metadata about how the call was executed.
*/
metadata: AIResponseMetadata;
/**
* Optional usage information (tokens, cost, etc.).
*/
usage?: UsageInfo;
}Raw Response Preservation
Providers may include the complete, unmodified response from the vendor's API in raw response fields. This enables debugging, auditing, and access to provider-specific data that isn't captured in the normalized response.
Primary field name (recommended): fullRawResponse
Alternative field names (for backward compatibility):
rawResponserawApiResponseoriginalResponseapiResponsehttpResponse
Example usage:
const response = await provider.aiCall(request);
// Access raw response (try primary field first, then fallbacks)
const rawResponse = response.fullRawResponse
|| response.rawResponse
|| response.rawApiResponse
|| response.originalResponse
|| response.apiResponse
|| response.httpResponse;
if (rawResponse) {
console.log('Full raw API response:', rawResponse);
// Access provider-specific fields, e.g.:
// console.log('Response ID:', rawResponse.id);
// console.log('Model used:', rawResponse.model);
}What constitutes a "raw response":
- Complete HTTP response (status, headers, body)
- Full API response object from the provider's SDK
- Original response structure before any processing
- Provider-specific metadata and fields
AIResponseMetadata and UsageInfo
export interface AIResponseMetadata {
provider: ProviderName;
model?: string;
providerRequestId?: string;
durationMs?: number;
// Any other provider-specific metadata
[key: string]: unknown;
}
export interface UsageInfo {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
// Optional cost fields or other accounting info
[key: string]: unknown;
}Provider identity
export type ProviderName = string; // e.g. "openai", "anthropic", "local-llm"Capabilities
export interface AICapabilities {
/**
* Whether the provider supports synchronous aiCall (always true for valid providers).
*/
supportsSync: boolean;
/**
* Whether the provider supports streaming via aiCallStream.
* True only if vendor has genuine streaming APIs.
*/
supportsStreaming: boolean;
/**
* Whether the provider supports batch job APIs.
* True only if vendor has real batch job APIs.
*/
supportsBatch: boolean;
}Routers use getCapabilities() to determine which features are available and how to call the provider.
Error Types
export type AIAbility = 'sync' | 'streaming' | 'batch';
export interface AIAbilityNotSupportedError extends Error {
name: 'AIAbilityNotSupportedError';
ability: AIAbility;
provider: ProviderName;
code: 'ABILITY_NOT_SUPPORTED';
}Providers throw AIAbilityNotSupportedError when a method is called for an ability the vendor doesn't support.
🧠 Interface: Implementing a Provider
Every provider adapter implements a single interface with all methods required:
export interface AIProviderInterface {
getProviderName(): ProviderName;
getCapabilities(): AICapabilities;
aiCall<Output = unknown>(request: AIRequest): Promise<AIResponse<Output>>;
aiCallStream(request: AIRequest): AsyncIterable<AIStreamChunk>;
submitBatch(requests: AIRequest[]): Promise<AIBatchJobHandle>;
getBatchStatus(job: AIBatchJobHandle): Promise<AIBatchJobHandle>;
getBatchResult<Output = unknown>(job: AIBatchJobHandle): Promise<AIBatchResult<Output>>;
}Important:
- All methods must be implemented, even if the underlying vendor doesn't support streaming or batch.
- For unsupported abilities, methods must throw
AIAbilityNotSupportedError.- This package only defines the shape of the interface.
- How you handle retries, rate-limits, logging, etc. is up to the router/orchestrator that uses this interface.
📦 Example Provider Implementation
Here's a complete provider implementation skeleton:
import type {
AIProviderInterface,
AIRequest,
AIResponse,
AICapabilities,
ProviderName,
AIStreamChunk,
AIBatchJobHandle,
AIBatchResult,
AIAbilityNotSupportedError,
} from '@x12i/ai-provider-interface';
export class MyProvider implements AIProviderInterface {
getProviderName(): ProviderName {
return 'my-provider';
}
getCapabilities(): AICapabilities {
return {
supportsSync: true, // always true
supportsStreaming: false, // set based on vendor support
supportsBatch: false, // set based on vendor support
};
}
async aiCall<Output = unknown>(request: AIRequest): Promise<AIResponse<Output>> {
const { instructions, inputData, config } = request;
// 1. Convert instructions + inputData into the provider's native API format.
// 2. Call the provider's SDK / HTTP API.
// 3. Normalize the result back into AIResponse<Output>.
const output: Output = /* parsed/normalized provider result */ (undefined as unknown as Output);
return {
output,
rawText: typeof output === 'string' ? (output as string) : undefined,
// Optional: Include raw response for debugging/auditing
// fullRawResponse: rawApiResponseObject,
// Optional: Include normalization info if request/config was modified
// normalization: { messagesNormalized: true, configNormalized: false, ... },
metadata: {
provider: this.getProviderName(),
model: 'my-model',
},
usage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
},
};
}
aiCallStream(request: AIRequest): AsyncIterable<AIStreamChunk> {
if (!this.getCapabilities().supportsStreaming) {
throw {
name: 'AIAbilityNotSupportedError',
message: 'Streaming is not supported by this provider',
ability: 'streaming',
provider: this.getProviderName(),
code: 'ABILITY_NOT_SUPPORTED',
} as AIAbilityNotSupportedError;
}
// If supported: implement streaming using vendor APIs
throw new Error('Not implemented');
}
async submitBatch(requests: AIRequest[]): Promise<AIBatchJobHandle> {
if (!this.getCapabilities().supportsBatch) {
throw {
name: 'AIAbilityNotSupportedError',
message: 'Batch is not supported by this provider',
ability: 'batch',
provider: this.getProviderName(),
code: 'ABILITY_NOT_SUPPORTED',
} as AIAbilityNotSupportedError;
}
// If supported: implement batch submission
throw new Error('Not implemented');
}
async getBatchStatus(job: AIBatchJobHandle): Promise<AIBatchJobHandle> {
if (!this.getCapabilities().supportsBatch) {
throw {
name: 'AIAbilityNotSupportedError',
message: 'Batch is not supported by this provider',
ability: 'batch',
provider: this.getProviderName(),
code: 'ABILITY_NOT_SUPPORTED',
} as AIAbilityNotSupportedError;
}
// If supported: poll vendor batch status
throw new Error('Not implemented');
}
async getBatchResult<Output = unknown>(
job: AIBatchJobHandle,
): Promise<AIBatchResult<Output>> {
if (!this.getCapabilities().supportsBatch) {
throw {
name: 'AIAbilityNotSupportedError',
message: 'Batch is not supported by this provider',
ability: 'batch',
provider: this.getProviderName(),
code: 'ABILITY_NOT_SUPPORTED',
} as AIAbilityNotSupportedError;
}
// If supported: fetch vendor batch results
throw new Error('Not implemented');
}
}Your actual implementation will call the real SDK/API and map its response into this normalized shape.
🧪 Using the Types in a Router
A router/orchestrator would depend only on these types:
import type { AIRequest, AIResponse, AIProviderInterface } from '@x12i/ai-provider-interface';
async function runTask(
provider: AIProviderInterface,
instructions: string,
inputData: unknown
): Promise<AIResponse> {
const request: AIRequest = {
instructions,
inputData,
config: { maxTokens: 300, temperature: 0 },
};
// Router/orchestrator can add:
// - retry
// - rate limit
// - logging
// - parallelization
// but the provider interface stays simple.
return provider.aiCall(request);
}Example call:
const response = await runTask(
provider,
'Summarize the following incidents, grouped by severity, output as JSON.',
incidentsArray
);
console.log(response.output); // structured summary📄 Package Information
- Name:
@x12i/ai-provider-interface - Repository:
[email protected]:x12i/ai-provider-interface.git - Runtime: None – this package ships only interfaces and types.
- Dependencies: None.
🔄 Streaming Support
Providers that support streaming implement aiCallStream():
for await (const chunk of provider.aiCallStream(request)) {
if (chunk.deltaText) {
process.stdout.write(chunk.deltaText);
}
if (chunk.done) {
console.log('\nStream complete');
}
}If streaming is not supported, aiCallStream() throws AIAbilityNotSupportedError.
📦 Batch Support
Providers that support batch jobs implement batch methods:
// Submit batch
const job = await provider.submitBatch([request1, request2, request3]);
// Check status
let status = await provider.getBatchStatus(job);
while (status.status === 'pending' || status.status === 'running') {
await sleep(1000);
status = await provider.getBatchStatus(job);
}
// Get results
if (status.status === 'completed') {
const result = await provider.getBatchResult(job);
console.log(result.responses);
}If batch is not supported, batch methods throw AIAbilityNotSupportedError.
📁 File-Based Batch API Support
Some providers (e.g., OpenAI) support file-based batch processing where you upload a file containing requests, create a batch job, and download results. These methods are optional and only available on providers that support them.
Types
// Upload a file for batch processing
export interface BatchFileUploadRequest {
file: Buffer | File; // File content (Node.js Buffer or browser File)
filename: string; // Original filename
purpose: 'batch'; // File purpose
}
export interface BatchFileUploadResponse {
id: string; // File ID
object: 'file';
bytes: number; // File size
created_at: number; // Unix timestamp
filename: string;
purpose: string;
}
// Create a batch job from uploaded file
export interface BatchJobCreateRequest {
input_file_id: string; // ID of uploaded file
endpoint: string; // API endpoint (e.g., '/v1/chat/completions')
completion_window?: string; // Optional completion window (e.g., '24h')
}
export interface BatchJobResponse {
id: string;
object: 'batch';
endpoint: string;
status: BatchJobStatus; // 'validating' | 'in_progress' | 'finalizing' | 'completed' | 'failed' | 'expired' | 'cancelled'
input_file_id: string;
output_file_id: string | null; // ID of output file (when completed)
error_file_id: string | null; // ID of error file (if any errors)
request_counts: {
total: number;
completed: number;
failed: number;
};
// ... timestamps and metadata
}
// Retrieve batch job
export interface BatchJobRetrieveRequest {
batchId: string;
}
// Download file content
export interface FileContentRequest {
fileId: string;
}
export interface FileContentResponse {
content: string; // NDJSON format (newline-delimited JSON)
}Usage
import type {
AIProviderInterface,
BatchFileUploadRequest,
BatchJobCreateRequest,
BatchJobRetrieveRequest,
FileContentRequest,
} from '@x12i/ai-provider-interface';
// Check if provider supports file-based batch API
if (provider.uploadBatchFile && provider.createBatchJob) {
// 1. Upload file (NDJSON format)
const uploadResponse = await provider.uploadBatchFile({
file: fileBuffer,
filename: 'batch-requests.jsonl',
purpose: 'batch',
});
// 2. Create batch job
const batchJob = await provider.createBatchJob({
input_file_id: uploadResponse.id,
endpoint: '/v1/chat/completions',
completion_window: '24h',
});
// 3. Poll for completion
let status = await provider.retrieveBatchJob!({
batchId: batchJob.id,
});
while (status.status === 'validating' ||
status.status === 'in_progress' ||
status.status === 'finalizing') {
await sleep(5000);
status = await provider.retrieveBatchJob!({
batchId: batchJob.id,
});
}
// 4. Download results
if (status.status === 'completed' && status.output_file_id) {
const outputFile = await provider.downloadFileContent!({
fileId: status.output_file_id,
});
// Parse NDJSON content
const results = outputFile.content
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
}
// 5. Download errors (if any)
if (status.error_file_id) {
const errorFile = await provider.downloadFileContent!({
fileId: status.error_file_id,
});
// Process errors...
}
}Provider Implementation
export class OpenAIProvider implements AIProviderInterface {
// ... other methods ...
async uploadBatchFile(
request: BatchFileUploadRequest
): Promise<BatchFileUploadResponse> {
// Implementation using OpenAI SDK
const formData = new FormData();
formData.append('file', request.file, request.filename);
formData.append('purpose', request.purpose);
const response = await this.client.files.create({
file: formData,
purpose: request.purpose,
});
return {
id: response.id,
object: response.object,
bytes: response.bytes,
created_at: response.created_at,
filename: response.filename,
purpose: response.purpose,
};
}
async createBatchJob(
request: BatchJobCreateRequest
): Promise<BatchJobResponse> {
// Implementation using OpenAI SDK
const batch = await this.client.batches.create({
input_file_id: request.input_file_id,
endpoint: request.endpoint,
completion_window: request.completion_window,
});
return this.mapBatchJobResponse(batch);
}
async retrieveBatchJob(
request: BatchJobRetrieveRequest
): Promise<BatchJobResponse> {
const batch = await this.client.batches.retrieve(request.batchId);
return this.mapBatchJobResponse(batch);
}
async downloadFileContent(
request: FileContentRequest
): Promise<FileContentResponse> {
const file = await this.client.files.retrieveContent(request.fileId);
return { content: file };
}
}Note: File-based batch API methods are optional. Providers that don't support file-based batch processing should leave these methods
undefined. The router can check for method existence before calling them.
⚠️ Error Handling
AIAbilityNotSupportedError
When a provider doesn't support streaming or batch, methods throw AIAbilityNotSupportedError:
try {
const stream = provider.aiCallStream(request);
} catch (error) {
if (error.name === 'AIAbilityNotSupportedError') {
// Fall back to aiCall()
const response = await provider.aiCall(request);
}
}Provider Errors
For runtime errors (HTTP 4xx/5xx, network errors, parsing errors), providers reject with standard Error objects. The router handles retries, backoff, and error recovery.
✅ Design Principles
- AI as functions:
instructions + inputData → output. Not chat. Not sessions. - Provider-agnostic: one interface, many providers.
- Single contract: all methods (sync, streaming, batch) are part of the base interface.
- Explicit capabilities: providers declare what they support via
getCapabilities(). - Standardized errors: unsupported abilities throw
AIAbilityNotSupportedError. - Interface-only: all retrying, batching, routing, metrics, etc. live outside this package.
- No provider-level retries: providers must not implement retry loops or backoff (that's the router's job).
- Structured outputs:
outputcan be text or JSON, depending on how you parse/normalize.
If you want, next step I can help you define a companion package like @x12i/ai-router that uses this interface and adds concurrency, retries, and provider selection on top.
