npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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 instructions and this inputData, return an output."

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):

  • rawResponse
  • rawApiResponse
  • originalResponse
  • apiResponse
  • httpResponse

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: output can 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.