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

@agentproto/batch

v0.2.2

Published

One batch-inference contract over provider Batch APIs (Anthropic Message Batches, OpenRouter Batch) plus a local-queue emulation for providers without one — submit N Messages-shaped requests, poll, and collect results keyed by custom_id, all driver-agnost

Readme

@agentproto/batch

One contract over provider batch-inference APIs — submit N Anthropic Messages-shaped requests, poll, and collect results keyed by custom_id, without caring which provider runs them. No SDK deps: raw fetch + zod.

Why

Anthropic and OpenRouter both expose an async Batch API at 50% of token price: submit up to 100k requests, get a batch id back, poll it, collect results within a 24h window. Batch is not a model — it's a delivery mode wrapped around the same Messages body (model, system, messages, max_tokens, tools…). This package gives callers (distillation, eval judging, dataset generation, proxy fan-out) one BatchDriver contract so they don't have to special-case each provider's envelope.

The contract

interface BatchDriver {
  readonly id: string
  submit(requests: readonly BatchRequest[], opts?: BatchSubmitOptions): Promise<BatchHandle>
  status(handle: BatchHandle): Promise<BatchStatus>
  results(handle: BatchHandle): AsyncIterable<BatchResult>
  cancel(handle: BatchHandle): Promise<void>
}
  • BatchRequest = { customId, body: MessagesBody }body is the same Anthropic Messages shape you'd send synchronously.
  • BatchHandle.id is ours (b_<ulid>), stable across process restarts; provider.batchIds is whatever the underlying API assigned (a driver may fan out to several — see OpenRouter below).
  • Results are keyed by customId, never by position — batch APIs return them in arbitrary order.
  • expired is not a failure. It means the item never ran inside the 24h window and should be resubmitted; use expiredCustomIds(results) to find which ones.
import {
  anthropicBatchDriver,
  pollUntilEnded,
  collectResults,
  expiredCustomIds,
} from "@agentproto/batch"

const driver = anthropicBatchDriver({ apiKey: process.env.ANTHROPIC_API_KEY! })
const handle = await driver.submit([
  { customId: "doc-1", body: { model: "claude-sonnet-5", max_tokens: 1024, messages: [...] } },
])
await pollUntilEnded(driver, handle, { onTick: s => console.error(s.counts) })
const results = await collectResults(driver, handle)
const toResubmit = expiredCustomIds(results.values())

Pre-submit validation

validateForBatch(request) rejects fields a batch envelope can't carry — stream, speed, fallbacks, max_tokens < 1, and a forced tool_choice of any/tool — naming the offending customId. validateBatchRequests also rejects duplicate customIds across a submit (results would be ambiguous otherwise). Every driver's submit() runs this before talking to a provider.

Durable store

BatchStore persists a batch to <stateDir>/batches/<id>/{manifest.json, requests.jsonl, results.jsonl} so it outlives the process — anyone with the id can re-attach via store.load(id). stateDir is an explicit constructor option; this package never defaults to a real home directory.

import { BatchStore } from "@agentproto/batch"
const store = new BatchStore({ stateDir: "/path/to/workspace/.batch" })

Drivers

anthropicBatchDriver({ apiKey, baseUrl?, fetch? })

One provider batch per submit. results() streams results_url's JSONL line by line. cancel() posts the documented cancel endpoint.

openrouterBatchDriver({ apiKey, baseUrl?, fetch? })

OpenRouter's batch API (beta) scopes one provider batch to one model, so a submit with mixed models fans out to N provider batches — grouped by body.model, one POST per group with endpoint: "/v1/messages" (Anthropic body shape, passed through as-is). The request body's key order (endpoint, model, requests) is intentional — OpenRouter stream-parses it. cancel() throws BatchUnsupportedError rather than pretend it works — OpenRouter's beta docs don't document one.

Status/count aggregation across a submit's provider batches is exact when the driver instance that submitted is still resident (its per-group customId lists are cached in memory); across a process restart with no cache, status() still sums to the handle's total request count but can't attribute partial progress to a specific still-running group.

localQueueDriver({ complete, concurrency?, store, retry? })

Emulation for providers with no batch API of their own (Requesty, Moonshot, Groq, xAI, a local llm-endpoint…). Runs every request through the injected complete(body) with bounded concurrency, retrying a thrown RetryableCompletionError (the signal a complete implementation uses for a 429/5xx from its underlying provider) with backoff up to retry.max attempts before recording a permanent errored result. Every result is appended to store as it lands. Full price — there is no provider-side batch to get the 50% discount from.

submit() runs every pending item to completion before resolving (there's no external service to poll). If the process crashes mid-run, a fresh localQueueDriver pointed at the same store can call the extra resume(handle) method — not part of the shared BatchDriver contract — to reload the original requests and re-run only the ones still missing a result.

Helpers

  • pollUntilEnded(driver, handle, { intervalMs?, timeoutMs?, onTick? }) — polls status() until ended/failed (default 24h timeout, matching every provider's batch window).
  • collectResults(driver, handle) — drains results() into a Map<customId, BatchResult>.

What this package deliberately doesn't do

No OpenAI/Gemini/Mistral native batch drivers, no daemon MCP/CLI tools, no cron polling, no model-catalog batch-pricing fields. MessagesBody covers the Anthropic Messages request shape only — the batch envelope, not a provider-agnostic request format.

License

Apache-2.0