cd /news/ai-tools/show-hn-llmbrake-per-customer-dollar… · home › topics › ai-tools › article
[ARTICLE · art-164981] src=github.com ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Show HN: LLMBrake – Per-customer dollar caps for OpenAI/Anthropic API calls

LLMBrake, a TypeScript library published to npm as version 0.1.0 under the MIT license, wraps OpenAI-compatible chat.completions.create calls to enforce a per-customer monthly dollar budget, pricing each call in USD from the token usage the provider returns. When a customer reaches 80% of their budget (configurable via downgradeAt), LLMBrake routes requests to a cheaper model listed in downgradeMap; at 100% it throws a typed BudgetExceededError before the request reaches the provider. The library runs inside the host app with the developer's own API keys, has zero runtime dependencies, is ESM-only on Node 18+, and stores only metadata such as customer ID, model, token counts, cost and timestamps, never prompts or completions.

read8 min views2 publishedNov 5, 2026
Show HN: LLMBrake – Per-customer dollar caps for OpenAI/Anthropic API calls
Image: Michielbdejong (auto-discovered)

Per-customer cost caps for AI apps: put a monthly dollar budget on every customer, switch them to a cheaper model near the cap, and block them at the cap.

llmbrake is a small TypeScript library that wraps your OpenAI-compatible chat.completions.create call. It prices every call in USD from the token usage the provider returns and keeps a running monthly total per customer. Near a customer's budget it swaps in a cheaper model; at the budget it throws a typed BudgetExceededError before the request ever reaches the provider.

  • Not a proxy. It runs inside your app and calls your provider directly with your own keys.
  • Metadata only. It never reads or stores prompts or completions, only customer ID, plan, feature tag, model, token counts, cost and timestamps.
  • Zero runtime dependencies. ESM, Node 18+, MIT licensed.

Status: 0.1.0, early. Website: llmbrake.com

npm install llmbrake

Published on npm as llmbrake. To try the latest main instead, run npm install github:llmbrake/llmbrake (it builds on install).

llmbrake is ESM-only (import, not require).

import OpenAI from "openai";
import { createGuard, isBudgetExceededError } from "llmbrake";

const openai = new OpenAI();

const guard = createGuard({
  budgetUsd: (ctx) => (ctx.plan === "pro" ? 20 : 2), // USD per customer per calendar month (UTC)
  downgradeAt: 0.8,                                   // at 80% of the budget...
  downgradeMap: { "gpt-5.5": "gpt-5.4-mini" },        // ...send gpt-5.5 requests to gpt-5.4-mini
});

const chat = openai.chat.completions;
const create = guard.wrap(chat.create.bind(chat));

try {
  const res = await create(
    { model: "gpt-5.5", messages: [{ role: "user", content: "Hello" }] },
    { customerId: user.id, plan: user.plan, feature: "chat" },
  );
} catch (e) {
  if (isBudgetExceededError(e)) {
    // e.customerId, e.spentUsd, e.budgetUsd, e.period
    return showUpgradePrompt();
  }
  throw e;
}

await guard.status({ customerId: user.id, plan: user.plan });
// → { period: "2026-10", spentUsd: …, budgetUsd: 20, remainingUsd: … }

Every wrapped call goes through three steps:

  1. Check (before the call).llmbrake resolves the customer's budget (a number, or your(ctx) => number function, e.g. by plan) and reads their spend for the current period, a calendar month in UTC such as2026-10 .
  2. Decide. - Block: ifspent >= budget , it throwsBudgetExceededError (code: "LLMBRAKE_BUDGET_EXCEEDED" ) andthe provider is never called . Catch it to show an upgrade prompt.
  • Downgrade: ifspent >= budget × downgradeAt and the requested model has an entry indowngradeMap , the request is sent with the cheaper model instead. The usage event reportsdowngraded: true and the originalrequestedModel .
  • Otherwise the request goes through unchanged.
  1. Record (after the call). The cost is computed from the response'susage (uncached input, cached input and output tokens) and your price table, then added to the customer's total. TheonUsage hook receives a usage event:
{
  customerId: "cus_123", plan: "pro", feature: "chat", period: "2026-10",
  requestedModel: "gpt-5.5", model: "gpt-5.4-mini", downgraded: true,
  promptTokens: 1102, completionTokens: 182, cachedTokens: 640,
  costUsd: 0.0012, spentUsd: 16.41, budgetUsd: 20, timestamp: "2026-10-11T20:00:00.000Z"
}

guard.check(ctx, model) runs steps 1–2 without making a call, which is handy for disabling a button before the user clicks it.

With stream: true, llmbrake sets stream_options.include_usage = true and records the cost when the stream ends, including when your code stops reading early (whatever usage was received by then is recorded).

Any create(params) function that accepts { model, ... } and returns { model, usage: { prompt_tokens, completion_tokens } } works, for example OpenRouter, Anthropic's OpenAI-compatible endpoint and local gateways. Add their model IDs to the price table.

Tested integrations live in examples/:

  • Vercel AI SDK :guard.check() beforegenerateText /streamText , then charge fromresult.usage (cached tokens included). Also has a Next.js route handler sketch.
  • OpenRouter :guard.wrap() on the OpenAI SDK pointed at OpenRouter, with OpenRouter model IDs in the price table.
  • LangChain.js :guard.check() beforeinvoke() , then charge fromAIMessage.usage_metadata .

CI runs them against mock providers (no API keys).

The default MemoryStore keeps totals in process memory: fine for one server, tests and demos, but lost on restart and not shared between instances. For multiple instances or serverless, implement the two-method UsageStore interface. With ioredis:

import Redis from "ioredis";
import { createGuard, type UsageStore } from "llmbrake";

const redis = new Redis(process.env.REDIS_URL!);
const key = (customerId: string, period: string) => `llmbrake:${period}:${customerId}`;

const redisStore: UsageStore = {
  async getSpend(customerId, period) {
    return Number((await redis.get(key(customerId, period))) ?? 0);
  },
  async addSpend(customerId, period, usd) {
    const k = key(customerId, period);
    const res = await redis
      .multi()
      .incrbyfloat(k, usd)
      .expire(k, 60 * 60 * 24 * 40) // keep ~40 days, then let old months expire
      .exec();
    const [err, total] = res?.[0] ?? [new Error("Redis transaction failed"), null];
    if (err) throw err;
    return Number(total);
  },
};

const guard = createGuard({ budgetUsd: 5, store: redisStore });

INCRBYFLOAT is atomic, so totals stay correct across instances. If the store throws (or returns a non-numeric value), calls fail open by default so your product keeps working; set failOpen: false to fail closed instead.

HostedStore keeps the counters in LLMBrake Cloud, so every server and serverless instance using your project's API key shares one counter per customer, and budgets you edit in the dashboard apply on the next call, with no Redis to run:

import { createGuard, HostedStore } from "llmbrake";

const guard = createGuard({
  budgetUsd: 5, // default; a budget set in the dashboard for a customer wins
  downgradeMap: { "gpt-5.5": "gpt-5.4-mini" },
  store: new HostedStore({ apiKey: process.env.LLMBRAKE_API_KEY! }),
});

It also closes the concurrent-overshoot gap: before each call the guard reserves an estimate of the call's cost (prompt size plus max_tokens, or estimateUsd), and the reservation only succeeds if spend plus other in-flight reservations is under the budget, in one atomic step on the server. After the call it settles the real cost and releases the hold (failed provider calls release it at $0). Parallel calls can then overshoot by at most about one call, instead of by everything in flight. If the API is unreachable the guard follows failOpen (default: let the call through).

Any store can opt in to this by implementing reserve and settle (the ReservingStore interface).

createGuard(options) returns { wrap, check, status }.

Option Default Description
budgetUsd required Monthly USD budget: a number, or (ctx) => number | Promise<number> . Must be>= 0 ; useInfinity for unlimited.
store new MemoryStore() Where spend is kept. Any UsageStore (getSpend ,addSpend ).
prices DEFAULT_PRICES Model → { inputPerMTok, outputPerMTok, cachedInputPerMTok?, source? } , USD per 1M tokens.
downgradeAt 0.8 Fraction of the budget (0–1) at which downgradeMap applies.1 disables downgrading.
downgradeMap {} { "expensive-model": "cheaper-model" } .
onUsage none (event) => void | Promise<void> , called after each metered call. Errors in it are swallowed.
unknownModel "throw" Model missing from prices :"throw" anUnknownModelPriceError before the call, or"allow" it and recordcostUsd: null .
failOpen true If the store errors: true lets calls through,false rethrows.
now () => new Date() Clock override, mainly for tests.
estimateUsd estimated Reserving stores only ( HostedStore ): USD to hold while a call is in flight, a number or(ctx, params) => number . Default: prompt length / 4 tokens plusmax_completion_tokens /max_tokens (or 1,000) at the requested model's price.

The call context passed as the second argument to the wrapped function is { customerId: string; plan?: string; feature?: string }.

DEFAULT_PRICES covers a few common OpenAI and Anthropic models in USD per 1M tokens (standard tier), read on 2026-10-09 from:

Provider prices change often, and these values are not guaranteed current. Check them against the pages above before using them for billing decisions. Each entry carries a source field. Override or extend the table:

import { createGuard, DEFAULT_PRICES } from "llmbrake";

createGuard({
  budgetUsd: 5,
  prices: { ...DEFAULT_PRICES, "my-model": { inputPerMTok: 1, outputPerMTok: 3 } },
});

Dated snapshot IDs such as gpt-5.4-mini-2026-03-17 fall back to the undated entry.

  • Concurrent overshoot. WithMemoryStore or a plainUsageStore , the budget is checked before a call and the cost is recorded after it, so several in-flight calls for the same customer can each pass the check and overshoot the cap by roughly their combined cost.HostedStore (or anyReservingStore ) reserves before each call, which bounds the overshoot to about one call.
  • Prices need verification. See above. Long-context tiers, batch discounts, regional or data-residency uplifts and priority tiers are not modeled, and the Anthropic IDs in the default table may differ from what your endpoint accepts.
  • Chat completions only. Only chat-completions-style token usage is metered: no images, audio, embeddings or tool fees yet.
  • Streaming needs usage. If a provider ignoresinclude_usage , or a stream is abandoned before the usage chunk arrives, that call is recorded with zero cost.
  • UTC calendar months. Budget periods reset at 00:00 UTC on the 1st.

A Python port with the same behavior lives in python/: sync and async openai clients, streaming, pluggable stores (in-memory, Redis), zero required dependencies, Python 3.9+.

pip install llmbrake is coming soon to PyPI. For now, install from GitHub:

pip install "git+https://github.com/llmbrake/llmbrake#subdirectory=python"

See python/README.md for the quickstart.

The library is free and stays MIT licensed. LLMBrake Cloud is the optional paid layer on top: shared counters (HostedStore), a dashboard with spend, budget and status per customer (edit budgets live), Slack/webhook alerts at 80% and 100%, and AI cost vs. Stripe revenue per customer. See llmbrake.com.

Issues and pull requests are welcome. See CONTRIBUTING.md. Security issues: see SECURITY.md.

── more in #ai-tools 4 stories · sorted by recency
── more on @llmbrake 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-llmbrake-per…] indexed:0 read:8min 2026-11-05 · —