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. 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 https://llmbrake.com npm install llmbrake Published on npm as llmbrake https://www.npmjs.com/package/llmbrake . To try the latest main instead, run npm install github:llmbrake/llmbrake it builds on install . llmbrake is ESM-only import , not require . python 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 as 2026-10 . 2. Decide. - Block: if spent = budget , it throws BudgetExceededError code: "LLMBRAKE BUDGET EXCEEDED" and the provider is never called . Catch it to show an upgrade prompt. - Downgrade: if spent = budget × downgradeAt and the requested model has an entry in downgradeMap , the request is sent with the cheaper model instead. The usage event reports downgraded: true and the original requestedModel . - Otherwise the request goes through unchanged. 3. Record after the call . The cost is computed from the response's usage uncached input, cached input and output tokens and your price table, then added to the customer's total. The onUsage 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/ https://github.com/llmbrake/llmbrake/blob/main/examples : - Vercel AI SDK https://github.com/llmbrake/llmbrake/blob/main/examples/vercel-ai-sdk : guard.check before generateText / streamText , then charge from result.usage cached tokens included . Also has a Next.js route handler sketch. - OpenRouter https://github.com/llmbrake/llmbrake/blob/main/examples/openrouter : guard.wrap on the OpenAI SDK pointed at OpenRouter, with OpenRouter model IDs in the price table. - LangChain.js https://github.com/llmbrake/llmbrake/blob/main/examples/langchain : guard.check before invoke , then charge from AIMessage.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 https://github.com/redis/ioredis : python 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: js 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