Lodestone

Docs

Scorecards and candidates

Two small JSON formats let a model evaluator and Lodestone feed each other without knowing about each other's code. Naija Eval is the first evaluator to use them.

   evaluator (Naija Eval)                         Lodestone (in your tool)
   ──────────────────────                         ────────────────────────
   measures answer quality  ── model-scorecard ──▶  orders models by it, per task
                                                      │
   benchmarks what is in use ◀─ model-candidates ──   lists the models it can call
                                                      │
   (reads live reliability) ◀── model-scorecard ───   records success rate and speed

Nothing here is automatic or hidden. A scorecard changes the order models are tried in, for tasks you name, and nothing else. A model nobody measured is never dropped, added or moved.

model-scorecard/1

Measured results about models.

{
  "schema": "model-scorecard/1",
  "source": "naija-eval",
  "generatedAt": "2026-10-28T09:00:00Z",
  "scores": [
    {
      "model": "anthropic/claude-sonnet-5.5",
      "task": "translate",
      "metric": "chrf++",
      "value": 32.3,
      "higherIsBetter": true,
      "n": 100,
      "measuredAt": "2026-10-08",
      "language": "yo",
      "slice": "en->yo"
    }
  ]
}
FieldMeaning
modelAn OpenRouter model id. A model that can't be routed (run locally) uses local/<name>; Lodestone ignores those. A :free or other :variant suffix is ignored when matching, so a score for a model applies to its free version.
taskWhat was measured: translate, transcribe, availability, latency.
metricHow: chrf++, wer, success_rate, latency_p50_ms.
value, higherIsBetterThe number, and which way is good. Both are stated, never assumed.
nItems the score rests on.
measuredAtThe day it was measured.
languageISO 639-1 code, when the task is about a language.
sliceA finer cut: en->yo, avg, or for live stats the provider.

parseScorecard() checks all of this and lists every problem. A scorecard with a different schema version is refused, not guessed at.

Using one

import { createLodestone, fetchScorecard } from '@extrafemi-ai/lodestone';

let card; let at = 0;
const ai = createLodestone({
  openrouter: { apiKey: process.env.OPENROUTER_API_KEY },
  // Called per request; keep your own cache. If it fails or takes over 1.5 s, the order is left alone.
  scorecards: async () => {
    if (!card || Date.now() - at > 6 * 3_600_000) { card = await fetchScorecard('https://naija-eval.extrafemi.com/data/scorecard.json'); at = Date.now(); }
    return [card];
  },
  profiles: {
    'translate-yoruba': { tier: 'strong', quality: { task: 'translate', metric: 'chrf++', language: 'yo', slice: 'avg' } },
  },
});

await ai.complete({ task: 'translate-yoruba', prompt });

The rules, so there are no surprises:

Live reliability

An evaluator says how good an answer is. It can't say whether you get an answer at all. Lodestone records that itself:

import { createStats } from '@extrafemi-ai/lodestone';

const stats = createStats();                 // last 200 attempts per model and provider
const ai = createLodestone({ /* ... */, stats });

stats.scorecard();   // success_rate, latency_p50_ms, latency_p95_ms per model, as a model-scorecard/1
stats.toJSON();      // save in your store; stats.load(saved) after a restart

The result is an ordinary scorecard (source: "lodestone-live"), so it can be served, shared, or used to order models: quality: { task: 'availability', metric: 'success_rate' } puts the model that answers most often first. Stats never see prompts or answers.

model-candidates/1

The models a tool can actually call, so an evaluator benchmarks those and not a guess.

const candidates = await ai.candidates();
{
  "schema": "model-candidates/1",
  "source": "lodestone",
  "generatedAt": "2026-10-28T09:00:00Z",
  "models": [
    { "id": "google/gemma-4-31b-it:free", "provider": "openrouter", "roles": ["free:strong", "free:vision"] },
    { "id": "qwen/qwen3.7-flash", "provider": "openrouter", "roles": ["paid"] }
  ]
}

Serve it from an endpoint or write it to a file. Naija Eval's eval/candidates.py reads it and prints the exact commands for the models it hasn't measured yet.