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

ai-resilient

v0.2.0

Published

Smart model fallback for the Vercel AI SDK: reactive fallback on rate-limit/transient errors and proactive switching before limits are hit.

Readme

ai-resilient

Smart model fallback for the Vercel AI SDK (v5 and v6).

ai-resilient wraps a chain of language models in a single LanguageModelV2 that:

  • Reactively falls back to the next model on rate-limit (429/quota) and transient (5xx/network) errors — but not on fatal errors (400/401/403), which throw immediately.
  • Proactively switches away from models that are near a known rate limit, using provider rate-limit response headers (OpenAI, Anthropic, Groq, Google, Mistral) and/or self-counted usage against limits you declare.

Works transparently with generateText, streamText, generateObject, and streamObject, using your own API keys — no gateway required.

Install

npm install ai-resilient ai

ai (v5 or v6) and @ai-sdk/provider are peer dependencies (@ai-sdk/provider ships with ai, so most package managers install it automatically). ai-resilient has zero runtime dependencies.

The returned model mirrors the specification version of the models it wraps — LanguageModelV2 on ai v5, LanguageModelV3 on ai v6 — so it plugs into generateText/streamText/generateObject/streamObject on either major. All models in one chain must come from the same SDK major; mixing throws at construction.

Usage

import { generateText } from 'ai';
import { createResilient } from 'ai-resilient';
import { groq } from '@ai-sdk/groq';
import { google } from '@ai-sdk/google';

const model = createResilient({
  models: [
    {
      model: groq('llama-3.3-70b-versatile'),
      limits: { requestsPerMinute: 30 },
    },
    { model: google('gemini-2.0-flash') },
  ],
});

const { text } = await generateText({ model, prompt: 'Hello!' });

Models are tried in the order you configure them. The first model is the primary; the rest are fallbacks.

Options

createResilient({
  models, // required: [{ model, limits? }, ...]
  store: memoryStore(), // default; pluggable (Redis/KV)
  threshold: 0.1, // skip a model when <10% of a known limit remains
  cooldown: 60_000, // ms to bench a model after a rate-limit error
  onFallback: (info) => {}, // { from, to, reason: 'rate-limit' | 'transient' | 'proactive' }
  onError: (error, modelId) => {},
});

Per-model limits (requestsPerMinute, requestsPerDay, tokensPerMinute) enable self-counted sliding-window tracking for providers that expose no rate-limit headers.

Behavior

| Situation | Behavior | | --------------------------------------- | --------------------------------------------------- | | 429 / quota exceeded | Bench model (retry-after or cooldown), try next | | 5xx / overloaded / network error | Try next model (no bench) | | 400 / 401 / 403 | Throw immediately | | Stream error before first content chunk | Fall back, fresh stream on next model | | Stream error after first content chunk | Propagate to caller | | All models fail | AllModelsExhaustedError with per-model attempts | | All models near a limit | Try all in order anyway | | Store unavailable | Assume available, skip recording |

Error handling

import { AllModelsExhaustedError } from 'ai-resilient';

try {
  await generateText({ model, prompt: '...' });
} catch (error) {
  if (AllModelsExhaustedError.isInstance(error)) {
    for (const { modelId, classification, error: cause } of error.attempts) {
      console.error(`${modelId} failed (${classification})`, cause);
    }
  }
}

Custom stores

The default memoryStore() keeps state in-process, which suits long-running servers. For serverless deployments, plug in any store implementing:

interface Store {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttlMs?: number): Promise<void>;
}

Example Redis adapter (using ioredis):

import Redis from 'ioredis';
import type { Store } from 'ai-resilient';

function redisStore(redis: Redis): Store {
  return {
    async get(key) {
      return redis.get(key);
    },
    async set(key, value, ttlMs) {
      if (ttlMs !== undefined) await redis.set(key, value, 'PX', ttlMs);
      else await redis.set(key, value);
    },
  };
}

Store failures never break your calls: if the store throws, models are assumed available and recording is skipped.

API

Beyond createResilient, these building blocks are exported:

| Export | Kind | Purpose | | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | memoryStore() | function | Default in-process Store with TTL eviction. | | AllModelsExhaustedError | class | Thrown when every model fails; has attempts: ModelAttempt[] and static isInstance(error). | | classifyError(error) | function | Classify any error as 'rate-limit' \| 'transient' \| 'fatal' — the same logic the fallback loop uses. | | getRetryAfterMs(error) | function | Parse a retry-after header (delta-seconds or HTTP-date) from an error's responseHeaders into milliseconds. | | parseRateLimitHeaders(provider, headers) | function | Parse provider rate-limit headers (OpenAI, Anthropic, Groq, Google, Mistral, IETF draft) into a normalized ParsedRateLimit. | | LimitTracker | class | The tracker behind proactive switching: bench state, header snapshots, sliding-window counters. Useful for custom orchestration on top of the same Store. | | Store, Limits, ModelConfig, ResilientOptions, FallbackInfo, FallbackReason, ErrorClassification, ModelAttempt, ParsedRateLimit | types | Public types for the options and callbacks above. |

Scope (v1)

Language models only. Not included: embeddings/image/speech models, mid-stream fallback (restarting after tokens were emitted), multi-key rotation, cost/latency-based routing.

License

MIT