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

@spendgraph/evals

v0.5.0

Published

Score LLM output instead of asserting equality. Deterministic metrics, an optional LLM judge, and the bill for both.

Readme

@spendgraph/evals

Score LLM output instead of asserting equality. Deterministic metrics, an optional LLM judge, and the bill for both. Zero dependencies.

npm install @spendgraph/evals
import { evaluate, formatReport } from "@spendgraph/evals";
import { jsonCorrectness, wordLimit } from "@spendgraph/evals/metrics";

const report = await evaluate(
  [{ input: "price as JSON", actualOutput: '{"input": 3, "output": 15}' }],
  [jsonCorrectness({ requiredKeys: ["input", "output"] }), wordLimit({ limit: 60 })]
);

console.log(formatReport(report));

A score, not a pass

Every metric returns a number between 0 and 1 and a reason for it. The threshold turns that into a verdict, and it is per metric and overridable per use:

const metric = report.results[0].metrics[0];

metric.name;       // "json_correctness"
metric.score;      // 1
metric.reason;     // "valid JSON with all required keys"
metric.threshold;  // 1
metric.passed;     // true
metric.kind;       // "deterministic" | "judge"

The report totals that across every case, and says what the judging cost:

report.passed;            // cases where every metric cleared its bar
report.failed;
report.meanScore;
report.byMetric;          // mean, passed and failed per metric name
report.skipped;           // metrics that never ran, and why
report.judgeCostMicros;   // what the judge charged, in micro-USD
report.judgeSavedMicros;  // what the response cache avoided

Equality assertions do not survive contact with a model: the same answer, reworded, is still the answer. A score says how far off it was, which is the thing you can set a bar against and watch move between versions.

Two fields exist so a report cannot lie about what happened:

  • error is set when the metric threw. A broken metric must never read as a pass.
  • skipped is set when it never ran — no judge configured, or the case is missing a field the metric needs.

A case

input and actualOutput are all most metrics read. The rest are there for the metrics that check retrieval or tool use.

{
  input: "Why was I charged twice?",
  actualOutput: "A proration for the mid-cycle upgrade.",
  expectedOutput: "You upgraded mid-cycle.",  // judged for meaning, never string equality
  retrievalContext: ["MSA 2.4 …"],            // for RAG groundedness
  toolsCalled: ["lookup_contract"],
  expectedTools: ["lookup_contract"],
}

The nine

Import a metric from /metrics and it costs nothing to have around. Only geval makes a model call.

| | | | | --- | --- | --- | | jsonCorrectness | output parses, and carries the keys you named | free | | containsAll | every required phrase is present | free | | wordLimit | the answer stayed inside its budget | free | | absence | a phrase that must not appear did not | free | | noLeakedPlaceholders | no {placeholder} survived rendering | free | | toolCorrectness | the tools called are the tools expected | free | | toolOrder | and in the order that made sense | free | | stepValidity | each step in a rollout was a legal move | free | | geval | a model grades it against criteria you write | paid |

kind is the field that answers "did this cost money". The runner uses it to skip judge metrics when no key is configured and to say so in the report, rather than quietly reporting a run where half the metrics never executed.

That split is the point of having both. The free eight run on every commit; the judge runs where a rubric is genuinely the only way to score the thing, and you can see what it charged.

Docs

| | | | --- | --- | | Getting started | Your first eval, and how to read the report | | Metrics | All nine, with what each one is for | | The judge | Wiring a model, sampling, logprobs, caching, cost | | Testing and CI | assertTest, regression gates, free-vs-paid runs | | Datasets and reliability | Repeat runs, pass^k, feedback and held-out splits | | Custom metrics | defineMetric, the registry, evals defined as data | | Assay | The prompt optimizer |

Where this is going: PLAN.md — the five phases from scorer to prompt optimizer, and which schema decisions cannot be backfilled.

Layout

src/
  core/       types, scoring maths, defineMetric, the registry
  judge/      Judge contract, logprob weighting, response cache
  metrics/    one file per metric, grouped by family
  runner/     evaluate, assertTest, reporters

Nothing in core/ or runner/ knows which metrics exist. Adding one touches two files: the metric, and the registration list.

Adding a metric

export const yamlCorrectness = defineMetric<YamlOptions>({
  id: "yaml_correctness",          // registry key — stable, snake_case
  kind: "deterministic",           // or "judge" if it costs money
  defaultThreshold: 1,
  requires: ["actualOutput"],      // fields the case must carry
  description: "Output parses as YAML.",
  build: (opts) => (testCase) => ({ score: 1, reason: "…" }),
});

Then registerMetric(yamlCorrectness) in src/metrics/index.ts and export it. That is the whole change: every metric gets threshold and name overrides for free, appears in listMetrics(), and becomes constructible from config. The full walkthrough is in docs/custom-metrics.md.

requires is what produces a skipped rather than a crash when a case does not carry the field — a tool metric run over a dataset with no tool calls says so instead of scoring zero.

What is exported where

The main entry is twelve names — enough to run an evaluation and read the result. Importing it registers the built-in metrics, which is what lets createMetric({ metric: "word_limit" }) resolve one by name.

import { evaluate, formatReport, createMetric, assay } from "@spendgraph/evals";

The rest is grouped by what it is for:

| | | | --- | --- | | /metrics | the built-in metric functions — wordLimit · geval · jsonCorrectness · … | | /core | scoring primitives, the metric registry, newSpend | | /dataset | loading and splitting cases | | /judge | judge plumbing — caching, the default judge, logprobs | | /report | score matrices and candidate comparison | | /rollout | repeat runs and reliability | | /runner | assertTest, measureOne, mapLimit | | /taxonomy | MAST failure codes |

Importing a metric from /metrics directly does not register it, which is the point: take the scoring function without the registry if that is all you want.

License

MIT