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

convex-evalbench

v0.3.0

Published

Reactive LLM eval, tracing, and regression layer as a Convex component.

Readme

convex-evalbench

Reactive LLM eval, tracing, and regression layer as a Convex Component.

Capture spans from your agents, run datasets against them, score with built-in scorers and LLM-as-judge, and watch results stream into a reactive dashboard live, all inside your own Convex deployment. No extra infrastructure, no polling: Convex subscriptions push every new span to your UI as it lands.

Convex Component tests npm license

Status: tracing (with the @convex-dev/agent and Vercel AI SDK adapters plus an OTLP/JSON receiver), versioned datasets, the eval runner, deterministic scorers, LLM-as-judge with multi-judge consensus, embeddingSimilarity, custom scorers, run comparison with a CI regression gate, host-invoked trace retention, and a live reactive dashboard are shipped. See the roadmap. References: docs/tracing.md, docs/evals.md, docs/dashboard.md.

Tracing

Record every LLM call as a span inside your own Convex deployment. Because Convex is reactive, a trace renders as a live span tree that fills in while it is in flight, no polling.

Install and register

npm install convex-evalbench
// convex/convex.config.ts
import { defineApp } from "convex/server";
import evalbench from "convex-evalbench/convex.config";

const app = defineApp();
app.use(evalbench);
export default app;

Record spans (source-agnostic API)

Construct an Evalbench from the generated component handle and record spans. Recording is best-effort: a failure is logged and swallowed, never thrown back into your LLM call.

import { Evalbench } from "convex-evalbench";
import { components } from "./_generated/api.js";
import { action } from "./_generated/server.js";

const evalbench = new Evalbench(components.evalbench);

export const runStep = action({
  args: {},
  handler: async (ctx) => {
    const traceId = crypto.randomUUID();
    const rootSpanId = crypto.randomUUID();
    await evalbench.recordSpan(ctx, {
      traceId,
      spanId: rootSpanId,
      kind: "agent_step",
      operationName: "my operation",
      status: "success",
      startedAt: Date.now(),
    });
    await evalbench.recordSpan(ctx, {
      traceId,
      spanId: crypto.randomUUID(),
      parentSpanId: rootSpanId,
      kind: "llm",
      operationName: "llm call",
      model: "...",
      provider: "...",
      inputTokens: 10,
      outputTokens: 20,
      totalTokens: 30,
      status: "success",
      startedAt: Date.now(),
      // input/output are recorded only when you pass them (opt-in).
      input: "the prompt",
      output: "the completion",
    });
  },
});

Read it back (reactive)

Expose the queries from your host so a client can subscribe; the span tree fills in live as spans are recorded.

import { query } from "./_generated/server.js";
import { v } from "convex/values";

export const listSpans = query({
  args: { traceId: v.string() },
  handler: (ctx, args) => evalbench.spansByTrace(ctx, args.traceId),
});

export const listRecentTraces = query({
  args: { limit: v.optional(v.number()) },
  handler: (ctx, args) => evalbench.recentTraces(ctx, args),
});

// Resolve a span's content on demand (inline strings plus signed URLs for
// content held in File Storage). `spanId` is the document id from listSpans.
export const getSpanContent = query({
  args: { spanId: v.string() },
  handler: (ctx, args) => evalbench.spanContent(ctx, args.spanId),
});

The tree and recent-traces queries return metadata only (no raw content), so reactive updates stay small. Raw content is opt-in (you record it by passing input/output); content at or below 4 KB is stored inline, larger content is offloaded to Convex File Storage and resolved on demand.

Wrap a @convex-dev/agent agent (optional)

The withEvalbench adapter is one optional ingestion source. It wraps a @convex-dev/agent agent so each LLM call is recorded as a span, composing with (not replacing) any handlers you already set. @convex-dev/agent is an optional peer dependency; the tracing core does not import it.

import { Agent } from "@convex-dev/agent";
import { Evalbench } from "convex-evalbench";
import { withEvalbench } from "convex-evalbench/agent";
import { anthropic } from "@ai-sdk/anthropic";
import { components } from "./_generated/api.js";

const evalbench = new Evalbench(components.evalbench);

const agent = withEvalbench(
  new Agent(components.agent, {
    name: "my-agent",
    languageModel: anthropic("claude-haiku-4-5"),
  }),
  { evalbench, recordContent: true }, // recordContent defaults to false
);

LLM calls made inside one generateText / generateObject operation share a traceId and link to a root agent_step span, so the operation renders as one trace tree. See docs/tracing.md for details and limitations.

Other ingestion sources

Two more optional sources feed the same tracing core:

  • Vercel AI SDK (convex-evalbench/ai): evalbenchMiddleware wraps a model with wrapLanguageModel, recording one span per call with measured latency. ai is an optional peer dependency.
  • OpenTelemetry (convex-evalbench/otlp): otlpTraceHandler is an httpAction you mount (e.g. at /v1/traces) so any OTLP/JSON exporter streams spans in, mapped via GenAI semantic conventions.

See docs/tracing.md for both.

Evals: datasets, runs, scorers

Keep a versioned dataset of inputs with expected outputs, run your system under test against every item with bounded parallelism, and watch the pass rate fill in live.

// Seed a dataset.
const datasetId = await evalbench.createDataset(ctx, {
  name: "greetings",
  items: [
    { input: "hello", expectedOutput: "HELLO" },
    { input: "world", expectedOutput: "WORLD" },
  ],
});

// The system under test: an action taking { input, runId, itemId } and
// returning { output, traceId? }. Stamp your spans with runId so the
// run's traces correlate.
export const myTarget = action({
  args: { input: v.any(), runId: v.string(), itemId: v.string() },
  handler: async (ctx, args) => {
    const output = await runMyAgent(args.input);
    return { output };
  },
});

// Run it: one idempotent, scored result per item.
const runId = await evalbench.startRun(ctx, {
  datasetId,
  target: api.evals.myTarget,
  config: { scorers: [{ type: "exactMatch" }], concurrency: 4 },
});

Subscribe to evalbench.runSummary(ctx, runId) for the live counters (completed / passed / aggregate score, maintained in the same mutation that writes each result) and evalbench.listResults(ctx, runId) for the per-item rows with scores and trace links.

Scorers: deterministic built-ins exactMatch and jsonSchema (eval-free JSON Schema validation that runs in Convex's V8 runtime), plus host-extensible scoring with no provider SDK in the component: defineScorer for custom scorer actions, llmAsJudge for rubric-based judging with your own LLM call (each verdict traced as a judge span), multi-judge consensus with a quorum, and embeddingSimilarity against a host embedder action. A wedged run is recovered with evalbench.redriveRun(ctx, runId) (attempts-capped). See docs/evals.md for the target and scorer contracts, versioning, and the runner's execution model.

Local development

The repo bundles a pinned convex-local-backend, so the example app and tests run without a cloud Convex project. See CONTRIBUTING.md for the dev loop and release flow.

Roadmap

Tracing, datasets, the runner with managed retries (retryable target failures retried with exponential backoff; see docs/evals.md), judges (LLM-as-judge with consensus), embeddingSimilarity, custom scorers, regression / A-B (run comparison with a CI gate), trace retention (host-invoked pruneTraces), the Vercel AI SDK adapter (evalbenchMiddleware), the OTLP/JSON receiver (otlpTraceHandler for any OpenTelemetry-instrumented app; see docs/tracing.md), and the live dashboard (a companion React app over the reactive queries; see docs/dashboard.md) are shipped. The roadmap items are all delivered; feature requests are welcome via issues.

Security

Raw prompt/completion content is only stored when you opt in; span metadata is always recorded. See SECURITY.md for the threat model and how to report vulnerabilities.

License

Apache-2.0