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

@lacspace/keypool

v1.4.0

Published

AI key pooling two ways. createAiChain: route-ordered multi-provider failover (Gemini, Groq, Cerebras, OpenAI, Anthropic, DeepSeek, OpenRouter, any OpenAI-compatible) that rests key+model pairs on 429 (Gemini daily quota until Pacific midnight), quarantin

Readme

@lacspace/keypool

Turn five free keys into one big quota. Provider-agnostic API-key rotation and rate-limit accounting: pool N keys per provider, track RPM/RPD/TPM/TPD per key (and per model — Groq's token-per-day is per model), round-robin among healthy keys, cool down on 429 using retry-after, and quarantine invalid keys. pick() hands you a usable key or null; report() feeds the outcome back. Persist state through a tiny adapter so every process shares one pool. Zero dependencies, isomorphic.

npm i @lacspace/keypool
import { createKeypool, kvStore } from "@lacspace/keypool";

const pool = createKeypool({
  keys: [
    { id: "gem-1", provider: "gemini", secret: process.env.GEMINI_1! },
    { id: "gem-2", provider: "gemini", secret: process.env.GEMINI_2! },
    { id: "gem-3", provider: "gemini", secret: process.env.GEMINI_3! },
  ],
  providerLimits: { gemini: { rpm: 15, rpd: 1500, tpm: 1_000_000 } },
  store: kvStore(myMongoKv),   // shared across PM2 workers
});

const picked = await pool.pick("gemini", "gemini-2.0-flash", estTokens);
if (!picked) throw new Error("all keys cooling down / quota spent");
try {
  const res = await callGemini(picked.secret, prompt);
  await pool.report(picked.id, { ok: true, tokens: res.usage.total, model: "gemini-2.0-flash" });
} catch (e) {
  await pool.report(picked.id, { ok: false, status: e.status, headers: e.headers });
}
  • Windowed accounting — RPM/RPD/TPM/TPD roll automatically; pick(provider, model, estTokens) only returns a key that can afford the call.
  • Per-model buckets — modelLimits["groq:llama-3.3-70b"] are tracked separately, because that's how the provider counts.
  • 429 aware — cools a key down for retry-after (seconds or an HTTP date), or a default window; recovers automatically.
  • Invalid quarantine — a 401/403 sidelines a key until you reinstate(id, newSecret).
  • Priority + round-robin — prefer some keys, otherwise spread load least-recently-used first, deterministically.
  • Shared state — pass a KeypoolStore (Mongo/Redis/KV) so multiple processes don't each burn the same quota; kvStore(kv) wraps any {get,set}.

Secrets live only in memory on the KeySpec; the persisted state is counters and health, never the key. Exports createKeypool, kvStore, and the state types.

AI key chain: route-ordered failover 1.1.0

createAiChain holds every key you have and tries them in a fixed route order, moving on only when one fails. It isn't a rotation. It is packaged from ShareRocketPro's production pool, built to WeNepal's rules, and calls go through @lacspace/ai.

import { createAiChain, redisStore } from "@lacspace/keypool";

const ai = createAiChain({
  keys: [
    { id: "gem-1", provider: "gemini", apiKey: process.env.GEMINI_KEY_1! },
    { id: "gem-2", provider: "gemini", apiKey: process.env.GEMINI_KEY_2! },
    { id: "groq-1", provider: "groq", apiKey: process.env.GROQ_KEY! },
  ],                                                  // or an async loader (keys from your DB)
  route: [
    { provider: "gemini", model: "gemini-3.1-flash-lite" },
    { provider: "groq", model: "openai/gpt-oss-120b" },
  ],
  routes: { wrap: [{ provider: "groq", model: "openai/gpt-oss-120b" }] }, // per purpose
  purposeCaps: { wrap: 50 },                          // per day, via takePurpose()
  store: redisStore(redisClient),                     // share rests across processes (default: memory)
  onCall: (row) => ledger.insert(row),                // every attempt; never the key or text
  onInvalid: (key, why) => markKeyInvalid(key.id, why),
});

const r = await ai.chat({ purpose: "wrap", messages, maxTokens: 600, json: true });
r.text; r.json; r.provider; r.model; r.keyId; r.usage; r.attempts;

| Situation | What happens | |---|---| | 429 per-minute limit | That key+model pair rests for the delay the provider gives (Gemini retryDelay, Groq "try again in 2m3s"), minimum 20 s. Other models on the same key keep working. | | 429 daily quota (PerDay, RPD, TPD, "daily") | Gemini rests until Pacific midnight, when Google resets quotas. Other providers rest max(retry-after, 15 min). | | 402 billing | Rests 6 h. | | 401/403, or Gemini 400 "API key not valid" | The key is marked invalid and skipped until clearRests(id). onInvalid fires. | | 5xx, timeout, network | One retry. After failThreshold (3) failures in a row, the key rests coolMinutes (10). A success resets the count. | | "overloaded" / UNAVAILABLE | That step's other keys are skipped, since they'd hit the same model. | | Groq 413 or TPM 429 | max_tokens shrinks to fit (from "Limit X, Requested Y") and retries once. | | Empty reply from a reasoning model | Retried once with a bigger budget (×2.5, 2000–8000). Providers get a minTokens floor (gpt-oss 1600). | | json: true | Adds response_format: json_object where supported. Groq's json_validate_failed retries without it. The reply is fence-stripped and repaired into r.json. | | Bad request / unknown model | That step is skipped; the next one may work. | | Everything resting | The soonest-to-recover pair is tried once before giving up. |

The default tweak gives Gemini Flash (not Flash-Lite 3.x, which rejects it) thinkingBudget: 0 and gives gpt-oss reasoning_effort: "low".

Built-in providers: gemini, groq, cerebras, openai, anthropic, deepseek, openrouter. Add any OpenAI-compatible one through providers.

Shared state without Redis (1.3.0): mongoStore(db.collection("ai_pool"), "prefix:") takes a plain MongoDB driver Collection (Mongoose: mongoose.connection.db.collection("ai_pool")), so keypool stays dependency-free.

  • Expiry: values are documents with an expiresAt field and a TTL index. Reads also check expiry, because Mongo sweeps only about once a minute.
  • Counters: atomic $inc upserts. The expiry is set on insert only, and an expired counter restarts at 1.
  • Testing: checked against a real MongoDB 8 server, including 20 concurrent increments.

Shrink floor (1.3.0): a TPM/OTPM shrink never goes below the provider's minTokens. If it would have to, a 413 moves on to the next step instead of making a retry that is certain to fail.

Panel helpers: setOptions(patch) (1.2.0) changes the route, caps or thresholds live without losing rests; rests() lists resting pairs, clearRests(id) resets a key after a re-test, isInvalid(id) checks a key. Utilities: msToPacificMidnight(), retryAfterMs(text).

Daily budgets and priority lanes (1.4.0)

Free tiers have small daily quotas that every feature competes for. A family is one quota pool, for example Gemini TTS: 4 models × 5 projects × 10 requests a day. A lane is a purpose with a priority and a reserve.

import { createBudget, createAiChain, mongoStore } from "@lacspace/keypool";

const store = mongoStore(db.collection("ai_pool"));
const budget = createBudget({
  store,
  families: {
    "gemini-tts": { match: { provider: "gemini", models: [/tts/] }, limitPerPair: 10, pairs: () => keys.length * 4, reset: "pacific" },
  },
  lanes: { pulse: { priority: 1, reserve: 40 }, video: { priority: 3 }, promo: { priority: 4, cap: 20 } },
});

// non-chat calls (TTS): ask first, fall back for free when refused
const a = await budget.acquire("video");
if (!a.ok) return edgeTts(text);          // a.reason: "reserved" | "exhausted" | "lane-cap"

// chat calls: let the chain do it
const chain = createAiChain({ keys, route, store, budget });
await chain.chat({ purpose: "explainer", lane: true, messages, maxTokens: 800 }); // AiChainError reason "reserved" when refused

await chain.reportError("gemini", model, errorBody); // a TTS 429 "per day … limit: 10" teaches the per-pair limit
await budget.remaining("video");   // { capacity, used, heldForHigher, available, lane }
await budget.snapshot();           // every family × lane, for a dashboard
  • Reserves: a lane can't take what more important lanes still have reserved, so video stops at 160 of 200 and Pulse keeps its 40. As Pulse uses its reserve, the hold shrinks. Important lanes can also use the shared pool.
  • Reset: usage resets at the family's quota reset: "pacific" (Google, the default), "utc", "npt" or a function.
  • Limits: the per-pair limit comes from limitPerPair, or is learned from the provider's 429 body (limit: 10 / "quotaValue": "10"). limit sets a whole-pool total.
  • Other calls: refund() returns a call that never reached the provider. exhaust(family) closes the pool for the day.
  • Fix: Gemini's …per_model_per_day 429s are now recognised as daily quota. Before, they were rested like per-minute limits.

Licence

Lacspace Free Licence v1.0 — free for personal and commercial use.