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-providers-router

v4.9.2

Published

Unified router for all LLM provider implementations

Readme

@x12i/ai-providers-router

Unified LLM provider router for Node.js. Routes requests to installed provider packages using the ProviderModule architecture from @x12i/ai-provider-interface.

Highlights

  • Multi-provider routing — OpenAI, Grok, and more via lazy-loaded provider packages
  • OpenRouter mode — Access 350+ models from 60+ providers through one API key
  • Sync, stream, and batch — Gated by each provider's declared capabilities
  • Fallback chains — Automatic provider/model failover with full attempt traces
  • Structured diagnostics — Usage, cost, timing, and ordered metadata.attempts[]
  • Reasoning support — Cross-vendor effort, visibility, and encrypted trace handling
  • ERC 2.0 — Zero-config initialization from environment variables
  • Structured logging — Powered by @x12i/logxer

This router never installs provider packages at runtime. You must install the packages you intend to use.


Table of contents


Install

npm i @x12i/ai-providers-router

Bundled provider packages (included as dependencies):

  • @x12i/ai-provider-openai — OpenAI and OpenRouter-compatible APIs
  • @x12i/ai-provider-grok — Grok / xAI

Optional provider packages (install when you need direct access):

npm i @x12i/ai-provider-anthropic
npm i @x12i/ai-provider-google
npm i @x12i/ai-provider-groq
# ... other @x12i/ai-provider-* packages

For OpenRouter mode, only @x12i/ai-provider-openai is required to reach models from many vendors through OpenRouter's unified API.


Quick start

import { createRouter, type AIRouterRequest, type AIResponse } from '@x12i/ai-providers-router';

const router = await createRouter();

const req: AIRouterRequest = {
  request: {
    messages: [{ role: 'user', content: 'Write 3 bullets about routers.' }],
    config: { model: 'gpt-4o-mini', maxTokens: 200 },
  },
  provider: 'openai',
  mode: 'sync',
};

const res: AIResponse = await router.invoke(req);
console.log(res.outputText);
console.log(res.usage);
console.log(res.rawResponse); // always present — lossless provider payload

Set provider API keys in your environment (see Configuration). With no arguments, createRouter() auto-discovers settings via ERC.


Architecture

AIRouterRequest
  → request interceptors (e.g. OpenRouter routing)
  → ProviderModule (from installed @x12i/ai-provider-* package)
  → router-side adapter (request → ProviderSDKCallSpec)
  → provider.execute() | stream() | submitBatch()
  → router-side adapter (ProviderSDKExecResult → AIResponse)
  → response interceptors
  → AIResponse (with lossless rawResponse)

| Layer | Role | |-------|------| | ProviderModule | Provider packages implement @x12i/ai-provider-interface | | Router adapters | Convert router requests to ProviderSDKCallSpec and parse responses | | Capability gating | Router checks provider.capabilities.modes.sync/stream/batch | | Execution semantics | Router owns timeoutMs, retries, idempotencyKey, AbortSignal |


Provider IDs

Direct providers (require matching @x12i/ai-provider-* package and API key):

| ID | Vendor | |----|--------| | openai | OpenAI | | grok | Grok / xAI | | anthropic | Claude | | google | Gemini | | groq | GroqCloud |

OpenRouter (unified gateway):

| ID | Role | |----|------| | openrouter | Explicit OpenRouter transport | | Any vendor ID | Routed through OpenRouter when preferred (USE_OPENROUTER=true, default) or as fallback when no direct key |

Grok ≠ Groq — Grok is xAI (grok / xai). Groq is GroqCloud (groq).


OpenRouter mode

OpenRouter is a unified API gateway. With an OPENROUTER_API_KEY, the router can reach models from many vendors through one key — using familiar provider names (openai, grok, anthropic, …) and automatic model mapping (e.g. openai + gpt-4oopenai/gpt-4o).

Enable OpenRouter

Set an API key (canonical name preferred):

export OPENROUTER_API_KEY=sk-or-your-key-here
# Legacy alias also supported:
# export OPEN_ROUTER_KEY=sk-or-your-key-here

Optional ranking headers:

export OPENROUTER_HTTP_REFERER=https://your-site.com   # legacy: OPEN_ROUTER_HTTP_REFERER
export OPENROUTER_X_TITLE=Your Site Name               # legacy: OPEN_ROUTER_X_TITLE

USE_OPENROUTER — prefer vs fallback

USE_OPENROUTER does not turn OpenRouter on or off. The router always registers OpenRouter when a key is present. This flag controls whether OpenRouter is preferred over direct provider keys.

| USE_OPENROUTER | OPENROUTER_API_KEY | Direct provider key (e.g. OPENAI_API_KEY) | What happens | |------------------|----------------------|---------------------------------------------|--------------| | unset or true (default) | set | set or unset | Prefer OpenRouter — all vendor calls route through OpenRouter, even when a direct key exists | | unset or true | set | not set | Route through OpenRouter | | false | set | set for requested vendor | Direct provider — use the vendor's own key/API | | false | set | not set for requested vendor | OpenRouter fallback — e.g. request anthropic with no ANTHROPIC_API_KEY still works via OpenRouter |

Default: prefer OpenRouter whenever OPENROUTER_API_KEY is set (USE_OPENROUTER defaults to true).

To use direct provider keys when available, while keeping OpenRouter as fallback for vendors without keys:

export USE_OPENROUTER=false
export OPENROUTER_API_KEY=sk-or-...
export OPENAI_API_KEY=sk-...          # openai requests → direct OpenAI
# no ANTHROPIC_API_KEY              # anthropic requests → OpenRouter fallback

Programmatic override:

const router = await createRouter({
  useOpenRouter: false, // direct when keys exist; OpenRouter fallback otherwise
});

Behavior summary

  • OpenRouter is always available when OPENROUTER_API_KEY is set — used as the default transport or as fallback
  • USE_OPENROUTER=true (default): routes through OpenRouter even if OPENAI_API_KEY, GROK_API_KEY, etc. are also set; direct provider packages are not auto-registered (avoids singleton config conflicts)
  • USE_OPENROUTER=false: auto-registers direct providers when their API keys exist; OpenRouter handles any vendor without a direct key
  • Works with createRouter() and new LLMProviderRouter() — auto-registration on first call
  • Provider names stay the same in your code; the router handles transport selection internally
  • Catalog data (.metadata/openrouter_catalog_with_vendor_mapping.json) drives model validation and provider inference
  • Responses on the OpenRouter path are parsed directly from OpenAI-compatible formats (no ai-io-normalizer)

Examples

Same provider name, OpenRouter underneath:

const req: AIRouterRequest = {
  request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'gpt-4o' } },
  provider: 'openai',
  mode: 'sync',
};
await router.invoke(req);

OpenRouter model format directly:

const req: AIRouterRequest = {
  request: {
    messages: [{ role: 'user', content: 'Hello!' }],
    config: { model: 'anthropic/claude-3-opus' },
  },
  provider: 'openrouter',
  mode: 'sync',
};
await router.invoke(req);

Provider inference from model name (no provider field):

const req: AIRouterRequest = {
  request: { messages: [{ role: 'user', content: 'Hello!' }], config: { model: 'gpt-4o' } },
  mode: 'sync',
};
await router.invoke(req); // infers openai

Troubleshooting

If you see "No provider specified and no providers registered":

  1. Confirm OPENROUTER_API_KEY (or OPEN_ROUTER_KEY) is set and non-empty
  2. Ensure the key does not start with ENV. (unresolved placeholder)
  3. Set config.provider in the request (e.g. { provider: 'openai', model: 'gpt-4o' })
  4. The OpenRouter adapter is always registered — no extra setup required

See also debugging guide.


Configuration

Zero-config (createRouter)

import { createRouter } from '@x12i/ai-providers-router';

const router = await createRouter(); // reads process.env

Programmatic (advanced mode)

const router = await createRouter({
  logLevel: 'info',
  verbose: false,
  timeoutMs: 60_000,
  useOpenRouter: true, // default: prefer OpenRouter when OPENROUTER_API_KEY is set
  fallbackChain: [{ provider: 'openai', model: 'gpt-4o-mini' }, { provider: 'grok', model: 'grok-2' }],
  openrouter: { apiKey: 'sk-or-...', httpReferer: 'https://example.com', xTitle: 'My App' },
  usageTracker: {
    recordRequest(e) {
      // provider, timestamp, duration, tokens, cost, success
    },
  },
  providerConfigs: {
    openai: { apiKey: 'sk-...', baseURL: 'https://api.openai.com/v1' },
    grok: { apiKey: 'xai-...' },
  },
});

Passing any explicit config object to createRouter(config) overrides zero-config env discovery for that call.

Environment variables

| Variable | Default | Description | |----------|---------|-------------| | Router | | | | AI_PROVIDER_ROUTER_LOGS_LEVEL | (see logging) | Canonical log threshold via logxer (error, warn, info, debug, verbose, off) | | AI_PROVIDER_ROUTER_LOG_LEVEL | info | Legacy alias for log level (used when _LOGS_LEVEL is unset) | | AI_PROVIDER_ROUTER_VERBOSE | false | Log full AI request/response payloads (sanitized) | | AI_PROVIDER_ROUTER_TIMEOUT_MS | 60000 | Default operation timeout (ms) | | OpenAI | | | | OPENAI_API_KEY | — | Required for direct OpenAI calls | | OPENAI_API_BASE | — | Custom API base URL | | OPENAI_ORGANIZATION | — | Organization ID | | Grok / xAI | | | | GROK_API_KEY | — | Required for direct Grok calls | | XAI_API_BASE | — | Custom xAI base URL | | OpenRouter | | | | OPENROUTER_API_KEY | — | Enables OpenRouter (always registered when set) | | OPEN_ROUTER_KEY | — | Legacy alias for OPENROUTER_API_KEY | | USE_OPENROUTER | true | Prefer OpenRouter over direct keys when OR key is set; set false to use direct providers when keys exist (OpenRouter remains fallback) | | OPENROUTER_HTTP_REFERER | — | Optional ranking header | | OPENROUTER_X_TITLE | — | Optional ranking header | | Other providers | | | | ANTHROPIC_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY, … | — | Used when those providers are installed |

Full reference: Environment variables · Configuration guide


Logging

The router uses @x12i/logxer for structured, package-scoped logging.

Logxer identity (npm package @x12i/ai-providers-router):

| Identifier | Value | Used for | |------------|-------|----------| | package (log field) | AIProviderRouter | Structured logs, shadow capture, Mongo package column | | envPrefix | AI_PROVIDER_ROUTER | Env vars, LOGXER_PACKAGE_LEVELS, stack packageLevels keys | | debugNamespace | ai-providers-router | DEBUG=ai-providers-router |

Exported constants: ROUTER_LOG_ENV_PREFIX, ROUTER_LOGXER_PACKAGE.

# Canonical (preferred)
AI_PROVIDER_ROUTER_LOGS_LEVEL=info

# Legacy (still supported when _LOGS_LEVEL is unset)
AI_PROVIDER_ROUTER_LOG_LEVEL=info

# Log full AI request/response payloads (requires _LOGS_LEVEL=verbose to print)
AI_PROVIDER_ROUTER_VERBOSE=true

Tiered AI interaction logging

| Level | Env threshold | Router verbose flag | What you get | |-------|---------------|----------------------|--------------| | info | _LOGS_LEVEL=info | — | One AI call completed line per invoke / stream / batch (provider, model, duration, tokens, requestId) | | debug | _LOGS_LEVEL=debug | — | Routing resolution, fallback attempts, retries, batch poll progress | | verbose | _LOGS_LEVEL=verbose | VERBOSE=true | Full sanitized request/response via AI interaction complete | | error | always (if enabled) | — | Failed invoke/stream/batch with stack |

Log levels: error · warn · info · debug · verbose · off

When neither _LOGS_LEVEL nor _LOG_LEVEL is set, no logLevel / logging is passed, and the logxer registry has no entry, the router defaults to info (not logxer's package-only default of warn). createRouter() loads LOGXER_PACKAGE_LEVELS / LOGXER_PACKAGE_LOGS_DEFAULT via applyPackageLogLevelsFromEnv() after .env.

Host apps (logxer ≥ 4.5) — provider packages (@x12i/ai-provider-openai, etc.) are not on the logxer 4.5 stack. Stack/registry options apply only to this router's logs (AI_PROVIDER_ROUTER). Configure your other libraries from @x12i/logxer in the host; pass the same StackLoggingOptions into createRouter when you want one object for the whole app:

import { configurePackageLogLevels, type StackLoggingOptions } from '@x12i/logxer';
import { createRouter, ROUTER_LOG_ENV_PREFIX } from '@x12i/ai-providers-router';

configurePackageLogLevels({
  default: 'warn',
  levels: {
    MY_GATEWAY: 'info',
    [ROUTER_LOG_ENV_PREFIX]: 'debug',
  },
});

const logging: StackLoggingOptions = {
  packageLevels: { [ROUTER_LOG_ENV_PREFIX]: 'debug' },
};

const router = await createRouter({ logging, verbose: true });

Bulk env for this package (loaded by createRouter() after .env):

LOGXER_PACKAGE_LEVELS=AI_PROVIDER_ROUTER:info
AI_PROVIDER_ROUTER_LOGS_LEVEL=error   # wins over bulk for this prefix only

Programmatic (router only):

import { createRouter, createLogger, ROUTER_LOGXER_PACKAGE } from '@x12i/ai-providers-router';

// info summaries always; payloads when verbose + log level verbose
const router = await createRouter({ logLevel: 'info', verbose: true });

// inject custom logger
const router2 = await createRouter({
  logger: createLogger({ level: 'debug', verbose: false }),
});

// stack identity for host config
console.log(ROUTER_LOGXER_PACKAGE.envPrefix); // AI_PROVIDER_ROUTER

See also: Logxer integration checklist (generic, shareable).

Verbose mode logs sanitized AI request/response payloads. Cross-cutting sinks (console, file, format) are configured in the host via @x12i/logxer — not via provider packages.


API usage

Sync call

import { createRouter, type AIRouterRequest, type AIResponse } from '@x12i/ai-providers-router';

const router = await createRouter();

const req: AIRouterRequest = {
  request: {
    inputData: 'Write 3 bullets about routers.',
    config: { model: 'gpt-4o-mini', maxTokens: 200, temperature: 0.7 },
  },
  provider: 'openai',
  mode: 'sync',
  exec: {
    timeoutMs: 60_000,
    idempotencyKey: 'optional-key',
    signal: abortController.signal,
  },
};

const res: AIResponse = await router.invoke(req);

Streaming call

const streamReq: AIRouterRequest = { ...req, mode: 'stream' };

for await (const ev of router.stream(streamReq)) {
  switch (ev.type) {
    case 'provider_raw':
      console.log('Raw:', ev.raw);
      break;
    case 'output_text_delta':
      process.stdout.write(ev.delta);
      break;
    case 'reasoning_summary_delta':
    case 'reasoning_trace_delta':
      // reasoning stream chunks
      break;
    case 'completed':
      console.log('Final:', ev.response.outputText);
      break;
    case 'error':
      console.error(ev.error);
      break;
  }
}

Batch requests

Batch is available only when provider.capabilities.modes.batch === true:

const items = [
  { request: { inputData: 'First', config: { model: 'gpt-4o-mini' } } },
  { request: { inputData: 'Second', config: { model: 'gpt-4o-mini' } } },
];

const batchResult = await router.createBatch('openai', items, {
  timeoutMs: 120_000,
  idempotencyKey: 'batch-1',
});

console.log(batchResult.items);
console.log(batchResult.rawBatch);

Request and response types

| Type | Purpose | |------|---------| | AIRouterRequest | Router input (request, provider, mode, exec) | | AIResponse | Sync output (outputText, rawResponse, usage, reasoning, metadata) | | AIStreamEvent | Streaming events (output_text_delta, completed, error, …) | | AIBatchResponse | Batch results | | RouterConfig | Router-level settings | | ProviderModelRef | { provider?, engine?, model? } for fallback chains |

Trace diagnostics

Every AIResponse includes stable, provider-agnostic diagnostics in metadata:

| Field | Description | |-------|-------------| | metadata.provider | Final provider used | | metadata.modelUsed | Actual model that served the response | | metadata.costUsd / metadata.cost | USD cost when reported (e.g. OpenRouter usage.cost) | | metadata.costStatus | 'priced' or 'unpriced' | | metadata.maxTokensRequested | Effective generation cap | | metadata.requestIds | { routerRequestId, providerRequestId?, openrouterRequestId? } | | metadata.timing | { startedAt, endedAt, durationMs } | | metadata.latencyMs | Alias for timing.durationMs | | metadata.attempts[] | Ordered retry + fallback trace | | response.output.parsed | Structured fields when outputContract is set |

Reasoning

Request unified reasoning controls via request.config.reasoning:

config: {
  reasoning: {
    effort: 'high',           // low | medium | high | xhigh (xhigh → high)
    maxTokens: 2000,          // Anthropic/Gemini max_tokens mode
    visibility: 'trace',      // none | summary | trace (best-effort)
    onUnsupported: 'downgrade', // downgrade | error | ignore
  },
}

Response fields: response.reasoning.applied, response.reasoning.artifacts, response.reasoning.warnings.

Supported models are tracked in .metadata/reasoning-support.json.

Fallback chains

On failure, the router tries the next candidate in order. Attempts are recorded in metadata.attempts[]. On exhaustion, throws FallbackExhaustedError.

Router-level default chain:

const router = await createRouter({
  fallbackChain: [
    { provider: 'openai', model: 'gpt-4o' },
    { provider: 'grok', model: 'grok-2' },
  ],
});

Per-request chain (in request.config):

request: {
  config: {
    model: 'gpt-4o',
    fallbackChain: [
      { provider: 'openai', model: 'gpt-4o-mini' },
      { engine: 'grok', model: 'grok-2' }, // engine is alias for provider
    ],
    // Legacy: provider-only fallback (same model)
    // fallbackProviders: ['grok', 'openai'],
  },
},

Precedence: request.config.fallbackChainrequest.config.fallbackEnginesrouter.fallbackChainrequest.config.fallbackProviders.

Interceptors

router.addRequestInterceptor(async (req, provider) => {
  // mutate or replace request before execution
  return req;
});

router.addResponseInterceptor(async (res, provider) => {
  // mutate or replace response after execution
  return res;
});

OpenRouter registers a request interceptor when USE_OPENROUTER=true (default) to route vendor calls through OpenRouter while preserving the original provider name for model mapping. When USE_OPENROUTER=false, the interceptor is skipped; resolveProviderName uses direct providers when registered and falls back to OpenRouter otherwise.

Health checks

const result = await router.checkHealth('openai');
// { provider: 'openai', healthy: true, latencyMs: 1234 }
// or { provider: 'openai', healthy: false, latencyMs: 5000, error: '...' }

Runs a minimal sync invoke with a 5 s timeout.


AIGateway

Thin wrapper around the router for gateway-style requests (instructions + inputData):

import { AIGateway, createRouter } from '@x12i/ai-providers-router';

const gateway = new AIGateway(await createRouter());

const response = await gateway.invoke({
  instructions: 'You are a helpful assistant.',
  inputData: 'Explain routers in one sentence.',
  config: { provider: 'openai', model: 'gpt-4o-mini' },
  mode: 'sync',
});

Also accepts full AIRouterRequest shapes ({ request, provider, mode }) and unwraps them automatically.

Optional strict provider/model pinning: set config.enforceProviderModel: true to throw on mismatch instead of silently switching.


Response normalization and cost

Exported helpers for downstream activity persistence and output contracts:

import {
  applyResponseNormalization,
  resolveCostReporting,
  extractCostUsdFromRouterResponse,
  extractCostUsdFromProviderUsage,
  enrichParsedForOutputContract,
  resolveOutputContractFieldKeys,
  parseMarkdownSectionsFromContent,
} from '@x12i/ai-providers-router';
  • Cost — Normalizes OpenRouter and provider usage into metadata.costUsd / costStatus
  • Output contract — When outputContract is on the request, markdown sections map to camelCase keys in output.parsed

See normalization field support.


Error types

| Error | When | |-------|------| | ProviderNotFoundError | Requested provider is not registered | | ProviderNotInstalledError | Provider package not installed (includes npm install hint) | | ProviderTimeoutError | Request exceeded timeoutMs (code: 'ETIMEDOUT') | | FallbackExhaustedError | All fallback candidates failed; check .attempts[] |

On partial provider failures, FallbackExhaustedError may carry a router-shaped partial payload for gateway extraction (PartialRouterPayload).


Manual setup (advanced)

For full control without createRouter():

import { LLMProviderRouter } from '@x12i/ai-providers-router';
import * as openaiModule from '@x12i/ai-provider-openai';

const router = new LLMProviderRouter({ logLevel: 'info', timeoutMs: 60_000 });

router.configureProvider('openai', { apiKey: process.env.OPENAI_API_KEY! });
router.registerProvider(openaiModule, 'initializeClient');

const providers = router.listProviders(); // ['openai']
const registry = router.getProviderRegistry();
const adapters = router.getAdapterRegistry();

Providers are auto-registered on first invoke when matching API keys are in the environment. When USE_OPENROUTER=true (default) and OPENROUTER_API_KEY is set, direct providers are skipped in favor of OpenRouter. With USE_OPENROUTER=false, both direct providers and OpenRouter can be registered simultaneously.

Legacy config file support:

import { createRouterFromConfig } from '@x12i/ai-providers-router';
const router = await createRouterFromConfig('./router-config.json');

Public API exports

// Router
export { LLMProviderRouter, createRouter, createRouterFromConfig }

// Types
export type { RouterConfig, AIRouterRequest, AIResponse, AIStreamEvent,
  AIBatchResponse, AIBatchRequestItem, NormalizedRouterOutput, ProviderModelRef,
  HealthCheckResult, ProviderId, CreateRouterConfig }

// Errors
export { ProviderNotFoundError, FallbackExhaustedError,
  ProviderNotInstalledError, ProviderTimeoutError }
export type { FallbackAttempt, PartialRouterPayload }

// Interceptors
export type { RequestInterceptor, ResponseInterceptor }

// Logger
export { Logger, getLogger, createLogger }
export type { LogLevel, LoggerConfig }

// Gateway
export { AIGateway }
export type { EnhancedLLMResponse }

// Normalization
export { applyResponseNormalization, resolveCostReporting,
  extractCostUsdFromRouterResponse, extractCostUsdFromProviderUsage,
  hasNonZeroTokenUsage, enrichParsedForOutputContract,
  resolveOutputContractFieldKeys, contractSpecToFieldKeys,
  parseMarkdownSectionsFromContent }
export type { ActivityCostStatus, ResolvedCostReporting }

// Registries and adapters (advanced)
export { ProviderRegistry, AdapterRegistry, OpenAIAdapter, GrokAdapter }

Provider packages

| Provider ID | Package | API key env | |-------------|---------|-------------| | openai | @x12i/ai-provider-openai | OPENAI_API_KEY | | grok | @x12i/ai-provider-grok | GROK_API_KEY | | anthropic | @x12i/ai-provider-anthropic | ANTHROPIC_API_KEY | | google | @x12i/ai-provider-google | GOOGLE_API_KEY | | groq | @x12i/ai-provider-groq | GROQ_API_KEY | | OpenRouter mode | @x12i/ai-provider-openai (bundled) | OPENROUTER_API_KEY |

Missing packages produce a clear ProviderNotInstalledError with install instructions.


Development and testing

npm run build          # compile TypeScript
npm test               # build + run all .tests/**/*.test.js
npm run test:openai    # live OpenAI call (requires OPENAI_API_KEY)
npm run test:openrouter
npm run test:reasoning
npm run erc:verify     # ERC manifest verification

Requires Node.js ≥ 18.


Related documentation

| Document | Topic | |----------|-------| | Configuration guide | Full request/config reference | | Environment variables | Complete env var list | | Reasoning integration | Reasoning API details | | Reasoning supported models | Model registry | | Request/response flow | Internal flow | | Debugging no-provider error | OpenRouter troubleshooting | | Normalization fields | Output contract and cost |


License

MIT