What a tool writes
Set it up once and share the store. Each call names a task, not a model; Lodestone picks the order, the time slices and the fallbacks.
// set up once, and share the store const ai = createLodestone({ openrouter: { apiKey: OPENROUTER_KEY, allowPaid: true, paidFirst: true, paidModels: ['anthropic/claude-haiku-5.5'], reasoning: (m) => m.startsWith('anthropic/') ? { enabled: false } : { effort: 'low', exclude: true } }, fallbacks: [ { provider: requesty(), apiKey: REQUESTY_KEY, models: ['bedrock/claude-haiku-5-5'], discover: { fill: { maxPromptUsdPerMTok: 1, maxCompletionUsdPerMTok: 5 } } }, { provider: groq(), apiKey: GROQ_KEY, models: ['openai/gpt-oss-20b'], free: true }, ], store, health: createHealth({ store }), spend: createSpendLimit({ store, dailyUsd: 2 }), rateLimit: { limiter: createRateLimiter({ store, max: 20 }), whenOver: 'free-only' }, trust: { requireNoTraining: true }, }); // each call names a task, not a model try { const { data } = await ai.json({ prompt, validate: isJob, signal: req.signal, caller: userId, private: true }); // a CV: do not keep or train on it } catch (err) { return explain(err, { product: 'MyApp' }); }
- Tiers, not model ids. A task maps to
fast,strongorvision(picked automatically when a message has an image). Or pass a fixedrouteto name provider, model and key yourself. - Customer keys stay separate. A caller's own key replaces yours and unlocks the paid model, but your fallback providers and your spend ledger are never touched by that call.
- One person cannot empty the budget. Name the
callerand each one gets their own allowance (stored as a hash). Over it, they are refused or served from free models only. The spend limit takes a hold per attempt, so a burst cannot overshoot it. How, and what is approximate. - Daily upkeep.
checkModels()refreshes every provider's catalogue and reports what has gone or is retiring.cacheKeyandpeek()reuse answers for repeatable tasks. - Cancelling. Pass a
signal; when the person leaves, the call and every model after it stop. - Private by design.
onAttemptreports provider, model, outcome and timing only. Prompts and answers are never logged, and aprivatecall is never cached.
Shared state
Stores for where you deploy
Netlify Blobs, Cloudflare KV, Upstash (and Vercel KV) and Deno KV adapters, so the circuit breaker and rate limits are shared between functions.
Checked, not assumedNode, Workers, Deno, Bun
The same smoke checks run on all four in CI, Workers without the Node compatibility flag.
AlphaA Python port
Failover, time budget, breaker, spend holds, per-caller limits and explain(), with the standard library only. What it lacks is listed.