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

promptver

v0.1.1

Published

AI prompt versioning toolkit with diffing, semantic version tags, evaluation runs, and rollbacks for LLM applications.

Downloads

262

Readme

promptver

npm version bundle size license TypeScript

Treat your LLM prompts like database migrations. promptver versions every prompt, lets you roll back instantly when v3 turns out to hallucinate, and runs deterministic A/B experiments across any LLM provider — OpenAI, Anthropic, Vercel AI SDK, or raw fetch.

Strings embedded in your codebase get edited without review, lose attribution, and silently break production. With promptver every prompt is a numbered, immutable file on disk (or any pluggable backend). Switching the active version is a one-line CLI call. Experiments record per-variant cost, latency, and any custom metric you care about, and experiment.getWinner() tells you which one to ship.


Installation

npm install promptver zod
pnpm add promptver zod
yarn add promptver zod

Quick Start

import { PromptForge } from "promptver";
import OpenAI from "openai";

const forge = new PromptForge();
await forge.init();
await forge.create("summarize", "Summarize this text in one sentence:\n\n{{text}}", {
  model: "gpt-4o-mini",
});

const openai = new OpenAI();
const rendered = await forge.render("summarize", { text: "Hello world" });
const res = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: rendered }],
});

Core Usage Examples

1. Create and load a versioned prompt

import { PromptForge } from "promptver";

const forge = new PromptForge();
await forge.init();

await forge.create(
  "extract_emails",
  "Extract every email address from the text:\n\n{{text}}",
  { model: "gpt-4o-mini" },
);

const prompt = await forge.load("extract_emails");
console.log(prompt.version, prompt.template);

2. Render a template with variables

import { PromptForge } from "promptver";

const forge = new PromptForge();
const out = await forge.render("extract_emails", {
  text: "Contact [email protected] and [email protected].",
});
console.log(out);
// Extract every email address from the text:
//
// Contact [email protected] and [email protected].

3. Roll back to a previous version

import { PromptForge } from "promptver";

const forge = new PromptForge();
await forge.create("summarize", "Summarize: {{text}}");
await forge.create("summarize", "Summarize concisely as bullet points: {{text}}");

// v2 is hallucinating? Roll back.
await forge.rollback("summarize", 1);
console.log((await forge.load("summarize")).version); // 1

4. Set up an A/B experiment

import { PromptExperiment } from "promptver";

const experiment = new PromptExperiment({
  id: "summarize_2025_q1",
  variants: [
    { name: "A", promptName: "summarize", version: 1, weight: 70 },
    { name: "B", promptName: "summarize", version: 2, weight: 30 },
  ],
});

const variant = experiment.assign("user_123");
console.log(variant.name); // deterministic: same user => same variant

5. Read experiment results

experiment.record({
  variantName: "A",
  latencyMs: 420,
  costUsd: 0.0024,
  inputTokens: 312,
  outputTokens: 88,
});
experiment.record({
  variantName: "B",
  latencyMs: 510,
  costUsd: 0.0018,
  inputTokens: 312,
  outputTokens: 60,
});

console.log(experiment.getResult().byVariant);
console.log(experiment.getWinner("cost").name); // "B"

6. Wrap an OpenAI client

import OpenAI from "openai";
import { PromptForge, wrapProvider } from "promptver";

const forge = new PromptForge();
const openai = wrapProvider(new OpenAI(), forge, { provider: "openai" });

await openai.chat.completions.create({
  promptName: "summarize",
  variables: { text: "Hello world" },
} as unknown as Parameters<OpenAI["chat"]["completions"]["create"]>[0]);

Framework Integration Examples

OpenAI SDK

import OpenAI from "openai";
import { PromptForge, wrapProvider } from "promptver";

const forge = new PromptForge();
await forge.create("answer", "Answer briefly: {{question}}", { model: "gpt-4o-mini" });

const openai = wrapProvider(new OpenAI(), forge, { provider: "openai" });
const res = await openai.chat.completions.create({
  promptName: "answer",
  variables: { question: "What is 2 + 2?" },
} as any);

Anthropic SDK

import Anthropic from "@anthropic-ai/sdk";
import { PromptForge, wrapProvider } from "promptver";

const forge = new PromptForge();
await forge.create("translate", "Translate to French: {{text}}", { model: "claude-sonnet-4" });

const anthropic = wrapProvider(new Anthropic(), forge, { provider: "anthropic" });
await anthropic.messages.create({
  max_tokens: 1024,
  promptName: "translate",
  variables: { text: "Hello" },
} as any);

Vercel AI SDK

import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { PromptForge, buildRequest } from "promptver";

const forge = new PromptForge();
const built = await buildRequest(forge, "summarize", { text: "Long input here" });

const result = streamText({
  model: openai(built.model ?? "gpt-4o-mini"),
  prompt: built.rendered,
});

for await (const chunk of result.textStream) process.stdout.write(chunk);

Next.js API route

// app/api/summarize/route.ts
import { NextResponse } from "next/server";
import OpenAI from "openai";
import { PromptForge, wrapProvider } from "promptver";

const forge = new PromptForge();
const openai = wrapProvider(new OpenAI(), forge, { provider: "openai" });

export async function POST(req: Request) {
  const { text } = await req.json();
  const res = await openai.chat.completions.create({
    promptName: "summarize",
    variables: { text },
  } as any);
  return NextResponse.json(res);
}

Configuration Reference

new PromptForge(options)

| Option | Type | Default | Description | | --------- | ---------------- | ---------------- | --------------------------------- | | storage | StorageAdapter | new FileStorage() | Backend for prompt persistence |

new PromptExperiment(config)

| Field | Type | Description | | ---------- | --------------------- | ------------------------------------------------ | | id | string | Stable identifier (used to seed assignment hash) | | variants | ExperimentVariant[] | Each with name, promptName, version?, weight |

new FileStorage(options)

| Option | Type | Default | Description | | ------ | -------- | ----------- | -------------------------- | | dir | string | "prompts" | Directory for prompt files |

wrapProvider(client, forge, options)

| Option | Type | Default | Description | | ------------ | ------------------------ | ------- | ---------------------------------------------- | | provider | "openai" \| "anthropic"| — | Wire shape used to inject messages | | applyModel | boolean | true | Override request model with the prompt's model |


Error Handling

import { PromptForge, PromptNotFoundError, VersionConflictError, StorageError } from "promptver";

try {
  await new PromptForge().load("does_not_exist");
} catch (err) {
  if (err instanceof PromptNotFoundError) {
    console.error(err.promptName, err.version);
  } else if (err instanceof VersionConflictError) {
    console.error("dup version", err.version);
  } else if (err instanceof StorageError) {
    console.error("storage failed", err.cause);
  }
}

TypeScript Types

import type {
  PromptDefinition,
  ExperimentConfig,
  ExperimentResult,
  ExperimentVariant,
  StorageAdapter,
  CallRecord,
} from "promptver";

class PostgresStorage implements StorageAdapter {
  async loadRegistry() { /* ... */ return { version: 1 as const, prompts: {} }; }
  async saveRegistry() {}
  async loadPrompt() { throw new Error("not yet"); }
  async savePrompt() {}
  async listPrompts() { return []; }
  async listVersions() { return []; }
}

CLI Reference

promptver init
promptver create summarize --template "Summarize: {{text}}" --model gpt-4o-mini
promptver rollback summarize 1
promptver list

| Command | Flags | Description | | ------------------------- | -------------------------------------------------- | -------------------------------------------- | | init | -d, --dir <path> | Create the prompts directory | | create <name> | -t, --template <text>, -m, --model <model> | Create a new version of a prompt | | rollback <name> <ver> | | Mark a previous version as active | | list | | Print every prompt with its active version |

Sample output of promptver list:

summarize	active=v1	versions=[1, 2]
extract_emails	active=v2	versions=[1, 2]

Real-World Recipe — Multi-Variant Summarization Service

// scripts/seed.ts
import { PromptForge } from "promptver";

const forge = new PromptForge();
await forge.init();
await forge.create("summarize", "Summarize: {{text}}", { model: "gpt-4o-mini" });
await forge.create("summarize", "Summarize the following as 3-5 bullet points:\n\n{{text}}", { model: "gpt-4o-mini" });
// src/experiment.ts
import { PromptExperiment } from "promptver";

export const summarizationExperiment = new PromptExperiment({
  id: "summarize_2025_q1",
  variants: [
    { name: "v1_paragraph", promptName: "summarize", version: 1, weight: 50 },
    { name: "v2_bullets", promptName: "summarize", version: 2, weight: 50 },
  ],
});
// src/server.ts
import express from "express";
import OpenAI from "openai";
import { PromptForge, buildRequest } from "promptver";
import { summarizationExperiment } from "./experiment.js";

const app = express();
app.use(express.json());

const forge = new PromptForge();
const openai = new OpenAI();

app.post("/summarize", async (req, res) => {
  const { userId, text } = req.body as { userId: string; text: string };
  const variant = summarizationExperiment.assign(userId);
  const built = await buildRequest(forge, variant.promptName, { text }, variant.version);

  const start = Date.now();
  const completion = await openai.chat.completions.create({
    model: built.model ?? "gpt-4o-mini",
    messages: [{ role: "user", content: built.rendered }],
  });
  const latencyMs = Date.now() - start;

  const inputTokens = completion.usage?.prompt_tokens ?? 0;
  const outputTokens = completion.usage?.completion_tokens ?? 0;
  const costUsd = (inputTokens * 0.15 + outputTokens * 0.60) / 1_000_000;

  summarizationExperiment.record({
    variantName: variant.name,
    latencyMs,
    costUsd,
    inputTokens,
    outputTokens,
  });

  res.json({ variant: variant.name, summary: completion.choices[0]?.message?.content });
});

app.get("/results", (_req, res) => {
  res.json({
    result: summarizationExperiment.getResult(),
    winner_by_cost: summarizationExperiment.getWinner("cost").name,
  });
});

app.listen(3000);
# Discovered v2 is more expensive — roll back
promptver rollback summarize 1

Prompt File Format Reference

prompts/<name>/<version>.json:

{
  "id": "summarize",
  "version": 2,
  "template": "Summarize the following as 3-5 bullet points:\n\n{{text}}",
  "variables": ["text"],
  "model": "gpt-4o-mini",
  "metadata": {
    "author": "@ada",
    "createdAt": "2026-01-12T09:32:11.000Z"
  }
}

| Field | Type | Description | | ------------ | -------- | ---------------------------------------------------- | | id | string | Prompt name (== folder name) | | version | number | Monotonically increasing integer | | template | string | Mustache-style {{variable}} template (dot notation supported) | | variables | string[] | Auto-extracted from template unless provided | | model | string?| Optional default model | | metadata | object | Free-form annotations |

Registry prompts/_registry.json:

{
  "version": 1,
  "prompts": {
    "summarize": { "name": "summarize", "activeVersion": 1, "versions": [1, 2] }
  }
}

Comparison Table

| Feature | Hardcoded strings | LangChain hub | promptver | | ----------------- | :---------------: | :-----------: | :--------------: | | Versioning | ❌ | ✅ | ✅ | | Rollback | ❌ | ✅ | ✅ | | A/B testing | ❌ | ❌ | ✅ | | Provider agnostic | ✅ | ❌ | ✅ | | Self-hosted | ✅ | ❌ | ✅ | | TypeScript types | ⚠️ | ⚠️ | ✅ | | CLI tooling | ❌ | ❌ | ✅ |


License

MIT