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

@polycode-projects/bedrock-meter

v1.10.1

Published

Real-time, in-process, pre-flight AWS Bedrock spend metering and capping.

Readme

@polycode-projects/bedrock-meter

Know the cost of every AWS Bedrock call — before it returns. Then cap, degrade, or alarm on it.

bedrock-meter is a tiny, in-process library you wrap your BedrockRuntimeClient with. It prices every Converse / InvokeModel call pre-flight, meters spend into a pluggable store, and enforces GBP spend caps before the call is allowed to run. No sidecar, no agent, no AWS round-trip on the hot path.

Why

  1. Finest granularity, immediately, and before the spend. It prices every Bedrock call per-request, in-process, before the call returns — versus AWS's own actuals, which trail: CloudWatch token metrics arrive ~13–40s later, and Cost Explorer / CUR dollar data only settles next-day (~24–48h). You get the number pre-flight. This is the core differentiator.
  2. Built on (1): throttle, fall back, alarm. Because the cost is known before the call, you can refuse or cap it (onCapExceeded) and alarm — enforcement, not just observation. You pick the model; the meter meters it and never swaps it.
  3. A grounded $0 fallback instead of an outage. (Roadmap.) When the cap trips, calls can fall back to an embedded deterministic no-LLM engine (tmct) that answers from grounded memory or refuses honestly — the dispatchFallback/TMCT_ENVELOPE exports are the seam.

A metered battery (Nova Lite + Nova Micro + Titan embed) reconciled to 0% token drift versus CloudWatch in our evidence harness.

Install

npm install @polycode-projects/bedrock-meter

The package is published to the GitLab project npm registry under the @polycode-projects scope. Point the scope at the registry with a project-local .npmrc (public npm is planned):

@polycode-projects:registry=https://gitlab.com/api/v4/projects/<project-id>/packages/npm/

Node >=20. TypeScript types are bundled (index.d.ts). The AWS SDK packages are optionalDependencies — install them only for dynamoStore() / the live pricing tier; the default in-memory + pinned setup needs no AWS deps.

Quickstart

Wrap your client, get pre-flight capping plus per-call pricing:

import { meter } from "@polycode-projects/bedrock-meter";
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";

const client = meter(new BedrockRuntimeClient({ region: "eu-west-2" }), {
  caps: { dailyGbp: 3 },        // refuse calls once today's £3 cap is hit
  onCapExceeded: "throw",       // 'throw' | 'degrade' | (decision, command) => …
  scope: { tenant: "acme" },    // attribute spend to arbitrary keys
});

await client.send(new ConverseCommand({ /* … */ }));   // metered + capped
const today = await client.meter.spentToday();          // { gbp_micros, requests, … }

No AWS needed — memoryStore()

For tests, demos, or local dev, swap in the in-memory store and the pinned price table. Nothing touches AWS:

import { meter, memoryStore, pinned } from "@polycode-projects/bedrock-meter";

// A fake client that returns a Converse-shaped usage block.
const fake = {
  async send() {
    return { usage: { inputTokens: 1000, outputTokens: 500 } };
  },
};

const client = meter(fake, {
  caps: { dailyGbp: 1 },
  store: memoryStore(),
  priceFeed: pinned(),
});

// estimate() projects GBP pence for a hypothetical call, pre-flight:
console.log(client.meter.estimate({ inputTokens: 1000, outputTokens: 500 }));

The explain CLI

Inspect pricing from the shell without writing any code:

npx bedrock-meter explain amazon.nova-lite-v1:0 --in 1000 --out 500
# → { model, usd, gbp, fx_gbp_usd, … }

# Prompt-cache tokens are billed on TOP of --in, so they get their own flags.
# Add --cache-write-5m / --cache-write-1h when you know which cache was written.
npx bedrock-meter explain amazon.nova-pro-v1:0 --in 1000 --cache-read 20000 --cache-write 2000

# Explaining a call routed through an inference profile: pass the underlying
# foundation model id, not the profile ARN. The ARN is opaque, so it prices at
# the unknown-model fallback shape and reports known: false.

npx bedrock-meter prices    # dump the pinned price table
npx bedrock-meter caps      # show the default caps

Forecasting a batch

estimate() and forecast() answer different questions and both stay available. estimate() is synchronous, flat-rate and model-agnostic — the same projection checkBeforeTurn uses, so it always agrees with the cap. forecast() is async and model-specific, priced through the same feed the ledger records with, so it agrees with the receipts.

const { totals, headroom } = await client.meter.forecast([
  { modelId: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", inputTokens: 12_000, outputTokens: 2_000 },
  { modelId: "amazon.nova-pro-v1:0", inputTokens: 4_000, outputTokens: 1_000, cacheReadInputTokens: 30_000 },
]);
totals.gbp;                 // priced cost of the batch, in GBP
headroom.fits_global_day;   // does it fit in what's left of today's cap?

The headroom block reports both figures rather than picking one: batch_priced_p is the model-specific cost the ledger would record, and batch_cap_p is what the cap counters would actually accumulate at cap-check's flat rates. The fits_* flags use batch_cap_p, since that is what moves against those caps. Token counts only — the library has no tokenizer and never takes prompt text. Nothing is written and no decision is taken; the caller decides.

API

| Export | Kind | Purpose | | --- | --- | --- | | meter(client, opts) | fn | Wrap a client; returns the same client with .send patched and .meter attached. | | client.meter.spentToday() | fn | Today's counters (requests, tokens, usd_micros, gbp_micros). | | client.meter.estimate({inputTokens, outputTokens}) | fn | Project the GBP pence cost of a call, pre-flight. | | client.meter.forecast(items, scope?) | fn | Price a batch of hypothetical calls and report it against the remaining budget. | | makeForecast({priceFeed, fx, capCheck}) | fn | The same batch forecaster without wrapping a client. | | CapExceededError | class | Thrown by .send() when a cap is hit and onCapExceeded: 'throw'. | | memoryStore() / dynamoStore(opts) | fn | Store seam — in-memory (no AWS) vs DynamoDB. | | pinned() / awsPricingApi(opts) / layered(feeds) | fn | Price-feed seam — committed table, live AWS Pricing API, fall-through stack. | | scope(obj) / scopeKey(obj) | fn | Normalise an attribution object / make a stable cache key. | | computePrice({priceFeed, fx, modelId, inputTokens, outputTokens, cacheReadInputTokens, cacheWriteInputTokens, guardrailTextUnits}) | fn | Price one invocation in integer micros. Never throws. | | capBasisInputTokens(usage) | fn | A turn's cache tokens as the plain input tokens they cost at cap-check's flat rate. | | guardrailUnitsFromResponse(resp, text?) | fn | Billable guardrail text units from an ApplyGuardrail response. | | guardrailTextUnitsOf(response) | fn | Billable guardrail text units across a Converse response's inline trace assessments as well as a standalone ApplyGuardrail usage block. | | makeAccountant / makeCounters / makeAttribution / makeCapCheck / makeTurnBudget | fn | The injectable building blocks behind meter(). | | dispatchFallback / makeTmctTarget / httpDispatch / TMCT_ENVELOPE / inEnvelope | fn/const | The tmct $0 fallback: envelope gate + metered dispatch to a tmct endpoint (priced £0). | | readTmctEnvelopeStamp() | fn | Which tmct backs the fallback, and which AGENTBENCH run stamped its published capability figures — read from the installed package. Lazy, memoized, never rejects. | | runtimeFlags | namespace | Kill-switch / degraded / high mode flag resolvers. | | makeBudgetTool | fn | Read-only budget/usage/forecast query over the metered ledger, plus MCP/A2A descriptors (see below). |

Budget as an agent tool

The budget query is exposed as data plus mountable descriptors, so any MCP server or A2A agent can serve it. bedrock-meter depends on no MCP or A2A SDK.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { makeBudgetTool, toMcpToolResult, dynamoStore } from "@polycode-projects/bedrock-meter";

const budget = makeBudgetTool({ store: dynamoStore({ tableName: "…" }) });
const server = new McpServer({ name: "bedrock-meter", version: "1.0.0" });

server.registerTool(
  budget.descriptor.name,
  { description: budget.descriptor.description, inputSchema: budget.descriptor.inputSchema },
  async (input) => toMcpToolResult(await budget.handle(input)),
);

For A2A, budget.skill is an AgentSkill and budgetAgentCard({ url, version }) builds a minimal AgentCard advertising it.

The handler is read-only. It reads the same ledger meter() writes and never writes to it — the store it holds is wrapped in a guard that throws on any write.

Subpath exports

| Subpath | Purpose | | --- | --- | | @polycode-projects/bedrock-meter/schema | Ledger key shapes and currency helpers, for a reconcile/extract job that needs the same pk/sk a live meter() call writes. | | @polycode-projects/bedrock-meter/cap | The daily/monthly cap resolver, for an ops Lambda reading the same resolved caps meter() sees. | | @polycode-projects/bedrock-meter/budget-tool | The read-only budget/usage/forecast handler and its MCP/A2A descriptors, standalone. | | @polycode-projects/bedrock-meter/runtime-flags | The kill-switch / degraded / high mode flag resolvers standalone. | | @polycode-projects/bedrock-meter/fallback-http | makeTmctTarget / httpDispatch / TmctDispatchError with zero dependency on the-mechanical-code-talker. Importing the package root also reaches makeEmbeddedTmctTarget, and a single-file bundler (esbuild --bundle) statically inlines its whole tmct dependency tree — tens of megabytes of unpacked tmct in your bundle, even when the embedded target is never called. If you only dispatch to a remote tmct serve process, import this subpath instead of the root. |

meter() options

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | caps.dailyGbp | number | — | Global daily cap, in GBP. | | caps.monthlyGbp | number | — | Monthly cap, in GBP. | | caps.perScopeDailyGbp | number | — | Per-scope (e.g. per-session) daily cap, in GBP. | | store | Store | memoryStore() | Where counters live. | | priceFeed | PriceFeed | pinned() | Price source. | | priceModelIds | map | fn | — | Price a call made against an inference-profile ARN as the underlying model. | | fx | number | fn | {rate} | pinned rate | GBP↔USD override. | | scope | object | {} | Attribution keys applied to every call. | | onCapExceeded | 'throw' | 'degrade' | fn | 'throw' | Cap-breach policy (see below). | | modes.degraded | boolean | false | Run in degraded mode. |

Caps are always in GBP. onCapExceeded:

  • 'throw' — throw CapExceededError and make the call (default).
  • 'degrade' — let the call through, surfacing the decision on client.meter.lastCapDecision for you to inspect.
  • a function (decision, command) => any — your handler; its return value is returned from .send() (e.g. swap to a cheaper model, return a cached reply).

Seams

  • StorememoryStore() (no AWS) or dynamoStore({ tableName }). Both implement the same single-table interface with an atomic counter add.
  • Price feedpinned() (committed JSON table, offline fallback), awsPricingApi({ store }) (live us-east-1 Pricing API, cached), and layered([live, pinned()]) to try live first and fall back. No feed ever throws on a price miss.

Prompt-cache pricing

Bedrock reports cache tokens separately, and its inputTokens count excludes them: the total input you are billed for is inputTokens + cacheReadInputTokens + cacheWriteInputTokens. The meter reads all three, prices them separately, records them as their own columns, and charges them against the spend cap.

The pinned table carries cache_read_per_1k_usd and cache_write_per_1k_usd per model, plus cache_write_1h_per_1k_usd for the models that offer a 1-hour cache alongside the default 5-minute one. Two rules make sure a cache token is never priced too low, since too low is a cap bypass while too high is only a false alarm:

  • A model entry with no pinned cache price is charged a bound rather than a guess: the input rate for a read (every measured family bills a read at 0.1x input, so the input rate can only over-state) and twice the input rate for a write.
  • A cache write whose cache lifetime the response never reported is charged at the highest write rate that model has. On a model with only a 5-minute cache that is its ordinary write rate; on one that also offers a 1-hour cache it is the 1-hour rate, because there is no way to tell which was written. Send and read Converse's cacheDetails breakdown to be charged the real split.

The live Pricing API feed answers null for every cache unit — its inferenceType filter cannot express one — so layered([live, pinned()]) falls through to the pinned cache prices rather than silently reusing the input price.

  • Scope — any { key: value } set ({ tenant, user, feature }); the accountant keeps one lifetime attribution counter per non-empty key.

Cost-allocation tags and price identity

Routing a call through an Application Inference Profile is the only way AWS attributes the spend to that profile's cost-allocation tags. To do it you send the profile ARN as the modelId. The ARN is opaque: nothing in it names the foundation model it copies from, so only you know what it should price as. Left unmapped, the ARN misses the price table and prices at the documented unknown-model fallback shape.

priceModelIds tells the meter what an ARN prices as. Give it a map, or a function if you resolve the id at call time:

const client = meter(new BedrockRuntimeClient({}), {
  priceModelIds: {
    "arn:aws:bedrock:eu-west-2:123456789012:application-inference-profile/abc123":
      "amazon.nova-micro-v1:0",
  },
});

The call itself is untouched. The command still carries the ARN, so the spend still lands under the profile's tags. Each ledger row then records three more columns:

  • model_id — unchanged: the id the call was made with, ARN and all.
  • price_model_id — the id the price was looked up under.
  • price_model_source"command" or "override".
  • price_model_known — did the feed's own table carry price_model_id? false is the row to alert on: the price came from the unknown-model fallback, not from that model's real price. null means the feed exposes no model table and cannot say.

A billing reconcile joins Cost Explorer's tagged dollars on model_id and groups the meter's own rows on price_model_id, with no mapping table on the side. client.meter.forecast() prices through the same map, so a forecast by ARN matches the row that call later writes.

Mapping an ARN can move a price down, since an unmapped ARN often prices as a dearer model than the real one. That is a correction to the priced ledger, not a weakening of the cap: the cap counters run on model-agnostic flat rates and this option does not touch them.

Metrics

Spend and usage metrics are emitted through an injectable exporter. Nothing is emitted, and no event is even built, unless you inject one. The EMF DailyGbpMicros line to stdout is unaffected either way.

const client = meter(new BedrockRuntimeClient({}), {
  metrics: (event) => console.log(JSON.stringify(event)),
});

Two event types arrive: "request", once per metered invocation, carrying the priced cost, model, operation, outcome and latency; and "window", once per counter increment, carrying the running day total. Money is always integer micros.

For OpenTelemetry, bridge your own Meter — bedrock-meter depends on no OpenTelemetry package:

import { metrics } from "@opentelemetry/api";
import { meter, otelMetricsExporter } from "@polycode-projects/bedrock-meter";

const client = meter(new BedrockRuntimeClient({}), {
  metrics: otelMetricsExporter(metrics.getMeter("my-service")),
  metricsScopeKeys: ["tenant"],
});

metricsScopeKeys is an allowlist, and it defaults to empty. scope is an arbitrary key/value set; flattening it onto metric attributes wholesale would mint an unbounded time series per session, so keys are opted in by name.

Where the prices come from

Every price this library uses can be traced to where it came from and when it was fetched. No tier below ever makes a network call at runtime — refreshing prices is something you run yourself, at build time or by hand.

Three tiers, checked in order, per model row — overriding one model's price never orphans the rest:

| Tier | File (in your working directory) | Env override | Written by | | --- | --- | --- | --- | | user override | bedrock-meter.config.json | BEDROCK_METER_CONFIG | you, by hand | | generated update | bedrock-meter.prices.json | BEDROCK_METER_PRICES | update-prices | | shipped default | the committed data/pricing-pinned.json | — | our CI |

bedrock-meter update-prices

Fetches AWS's public bulk price list and the ECB's daily exchange rates, and writes a provenance-stamped price file:

# Fetch fresh prices and rates, write them next to your project (the generated tier).
npx bedrock-meter update-prices

# See what would change without writing anything.
npx bedrock-meter update-prices --dry-run

# Refresh this package's own shipped default (CI does this on a schedule).
npx bedrock-meter update-prices --pinned

| Flag | Default | Meaning | | --- | --- | --- | | --region <r> | AWS_REGION or eu-west-2 | Which region's price list to read. | | --out-file <path> | ./bedrock-meter.prices.json | Where to write the generated file. | | --pinned | off | Write the shipped default instead of the generated file — refuses outside a checkout of this repo. | | --dry-run | off | Print the drift report; write nothing. | | --max-drift <pct> | 25 | Refuse to write if any row moves further than this. | | --currencies a,b,c | every ECB currency | Restrict which FX rates to fetch. | | --models id,id | every model already priced | Add new models to price, without dropping any. | | --no-fx / --no-prices | off | Skip one side of the refresh. |

Both sources are credential-free:

  • AWS's public bulk price list — no key or @aws-sdk/client-pricing needed, just AWS's own published data.
  • The ECB's daily euro foreign exchange reference rates — updated on TARGET working days, reused under the ECB's free-of-charge terms. Every generated file cites the ECB as source in its sources[] block.

Some models have no machine-readable AWS list price (recent Anthropic models, for instance), or a measured price that genuinely disagrees with the list. Those rows carry a hold, so a refresh reports the list price beside the held one instead of overwriting a real measurement:

"amazon.nova-lite-v1:0": {
  "input_per_1k_usd": 0.00006,
  "hold": { "reason": "measured from Cost Explorer spend; list price is 40% higher", "measured_at": "2026-05-30" }
}

Per-row provenance

bedrock-meter prices --provenance shows which tier answered each model's price and when it was fetched — the one-command answer to "why is this call priced this cheap":

npx bedrock-meter prices --provenance
# → { "amazon.nova-lite-v1:0": { …, "provenance": { "tier": "pinned", "source": "aws-price-list", "fetched_at": "…" } } }

bedrock-meter fx prints the resolved rate table with its own source and fetch date.

Currency display

The ledger and the cap engine still work in GBP and USD internally — nothing about enforcement changes. Everything else can display in any currency the ECB publishes a rate for:

npx bedrock-meter explain amazon.nova-lite-v1:0 --in 1000 --out 500 --currency EUR
npx bedrock-meter caps --currency JPY

Set BEDROCK_METER_CURRENCY to pick a default display currency without passing --currency every time.

Caps also take a currency-neutral form beside the existing GBP fields — supply one or the other, not both:

caps: { daily: { amount: 12, currency: "USD" } }   // instead of caps.dailyGbp

makeCounters() can emit an extra DailySpendMicros EMF gauge in your chosen currency, opt-in and alongside the existing DailyGbpMicros line, never instead of it:

makeCounters(store, { displayCurrency: "EUR" });

Links

Licence

Apache-2.0 © 2026 Polycode Limited.