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-proxy

v1.10.1

Published

Loopback HTTP sidecar exposing @polycode-projects/bedrock-meter's pre-flight cost cap and tmct fallback to any Lambda runtime, packaged as a Lambda layer/extension.

Downloads

2,110

Readme

@polycode-projects/bedrock-meter-proxy

A loopback HTTP sidecar wrapping @polycode-projects/bedrock-meter's pre-flight cost cap and tmct fallback, packaged as an AWS Lambda layer/extension. Any Lambda runtime — Java, Python, .NET, whatever — gets metering, capping, and the tmct fallback over http://127.0.0.1:<port>, without touching the Bedrock SDK or a JS/JVM interop layer directly.

Why a sidecar, not a per-language SDK

@polycode-projects/bedrock-meter is a Node library. A Lambda function written in another language can't import it. Rather than hand-porting the cap-check and fallback logic into every runtime bedrock-meter should support, this package runs the existing Node runtime as a small, always-local HTTP server inside the same execution environment, reachable only from the function that owns it.

The HTTP contract

Deliberately a native JSON API, not a byte-faithful mimic of Bedrock's wire format. SigV4 verification on a loopback, single-tenant socket buys nothing — there is no other principal on that socket to authenticate against — and a plain JSON contract is callable from any language's stdlib HTTP client with zero SDK dependency, which is the actual goal.

POST /v1/converse
  body: {
    modelId: string,
    messages: [...],        // mirrors Bedrock's ConverseCommandInput.messages
    system?: [...],         // mirrors ConverseCommandInput.system
    inferenceConfig?: {...},
    toolConfig?: {...},
    guardrailConfig?: {...},  // mirrors ConverseCommandInput.guardrailConfig;
                              // ConverseStreamCommandInput's extra field
                              // (streamProcessingMode) is accepted too, used
                              // only when stream: true
    scope?: { graphId?, sessionUuid?, origin? },
    needs?: { contextTokens?, reasoning?, tools?, structured? },
    stream?: boolean         // see "Streaming" below
  }

  responses (stream absent or false):
    200 { path: "bedrock", output: <real ConverseCommand response>,
          cost: { usd_micros, gbp_micros, model_id } }
    200 { path: "tmct-fallback", reason: "in-envelope",
          output: { answer, narration, goal, canonicalised, dialogueAct,
                     factsUsed, factsLearned, digest? },
          cost: { usd_micros: 0, gbp_micros: 0, model_id: "tmct" } }

      factsUsed    the sources the answer leaned on, one entry per citation:
                   { provenance, title?, url? }. `provenance` is the verbatim
                   citation the answer carried; `title`/`url` are added
                   whenever it names a reference article, so a repeat ask
                   citing the bare tag reference:simplewiki:Benchmark@7557838
                   resolves to the same openable revision the first ask
                   spelled out in full.
      factsLearned the facts the turn WROTE: each tmct fact row
                   ({ id, subject, predicate, object, provenance, … }) plus
                   `text`, the triple rendered as a sentence, and
                   `title`/`url` on the same terms as above. Empty when the
                   turn wrote nothing — what a repeat ask answered out of
                   memory looks like.

    402 { path: "refused", reason: "refused-over-cap", receipt: <cap decision> }
    5xx { path: "error", reason: "bedrock-error" | "sidecar-timeout", message }

GET /v1/budget
  query: graphId?, sessionUuid?, scopeKey?, scopeId?, projectedTokens?
  200 { as_of, remaining: { day, global_remaining_p, global_cap_p,
                             session_remaining_p, session_cap_p,
                             month, monthly_remaining_p, monthly_cap_p },
        today: <CounterRow>, month: <CounterRow>,
        attribution: <AttributionRow> | null,
        forecast: { projected_tokens, projected_cost_p,
                    calls_remaining_global, calls_remaining_monthly,
                    calls_remaining_session, limiting_budget, calls_remaining } }
  400 { path: "error", reason: "bad-request", message }   # e.g. scopeKey without scopeId

  Read-only: this endpoint never writes to the ledger and never reserves
  budget — it reads the same ledger this sidecar's own /v1/converse calls
  write, over @polycode-projects/bedrock-meter's makeBudgetTool().

GET /healthz -> 200 { ok: true, uptime_s }     # liveness only, no store/AWS check
GET /        -> 200 { name, version, mode, caps, fallback: { envelope, target },
                       guardrail: { request_field, applied_on_fallback },
                       endpoints, tools, skills }
                # tools/skills are the budget query's MCP tool descriptor and
                # A2A skill descriptor, for discovery — the query itself is
                # still GET /v1/budget above, not this route.

Streaming

stream: true reuses the same route and body shape. A bad request or a cap-breach refusal is decided before any header is written, so those stay the same one-shot 400/402 JSON as above — never ndjson.

Once the request is actually going to stream (real Bedrock or the tmct-fallback path), the response is 200 with Content-Type: application/x-ndjson, chunked, one JSON object per line:

{ type: "delta", text: string }                              // one per completed text chunk
{ type: "tool-use", id: string, name: string, input: object } // one per completed tool-use block
{ type: "usage", inputTokens: number, outputTokens: number }  // once
{ type: "stop", reason: string }                              // terminal, always last

On the real Bedrock path these come off ConverseStreamCommand's own event stream. On the tmct-fallback path (tmct is not itself token-streaming) they're synthesized as a single burst: one delta with the full answer, a usage frame at {inputTokens: 0, outputTokens: 0} (tmct's $0 guarantee), and a stop frame — no tool-use frame, since tmct doesn't call tools.

A failure once the response is already committed to 200/ndjson can't retroactively become a 5xx — it ends the stream with a final {type: "stop", reason: "error"} frame instead. Only a failure before the first frame (the real dispatch call itself throwing) still produces the one-shot 502 { path: "error", reason: "bedrock-error" } above.

SSE framing was considered and rejected here for the same reason it was rejected on the non-streaming contract: on a loopback, single-tenant socket it buys nothing over plain ndjson.

Guardrails

Send guardrailConfig in Bedrock's own shape and it goes straight through to both the buffered and streaming Bedrock call, unchanged.

If you omit trace, the sidecar sets it to "enabled". Bedrock only reports billable guardrail units inside the trace, so a request with no trace would meter its guardrail spend at zero. Send trace: "disabled" yourself if you want that, and accept the cost: the guardrail's spend won't be priced.

The sidecar has no guardrail of its own and never injects one. If your IAM conditions Converse on bedrock:GuardrailIdentifier, the sidecar runs under that same role, so your request must carry the identifier the grant names.

The tmct fallback path applies no guardrail. It answers from a local deterministic graph with no Bedrock call to attach a guardrail to. GET / reports this as guardrail.applied_on_fallback: false.

guardrail_id lands on the ledger row for any turn that carried a guardrailConfig, so a reconcile can tell a screened turn from an unscreened one.

needs is passed straight through to the tmct fallback envelope gate. If omitted, the proxy derives a conservative estimate from the request body itself (a rough characters-per-token heuristic over the message/system text) rather than defaulting to "always in envelope."

The 402 for a cap-breach refusal is deliberate: a caller can branch on the status code alone — retryable 5xx vs. not-retryable 402 — without parsing the response body.

Security: loopback only, no auth, by design

The server binds 127.0.0.1 only. This is hardcoded, not an option or an env var — there is no legitimate reason to widen it, since this is a single-tenant socket inside one Lambda execution environment, and doing so "for flexibility" would be a real widening of the attack surface for zero benefit. There is no authentication for the same reason: on a loopback, single-tenant socket, no other principal exists to authenticate against.

GET /v1/budget carries the same property for the same reason: a spend readout is operational intelligence, not a secret, and the thing that makes skipping auth safe here is the socket, not the sensitivity of what it returns.

The caller's responsibility: timeout and fallback posture

This is intra-namespace loopback traffic. A slow response means the sidecar itself is broken (or booting), not that Bedrock is slow — so the calling code should set its own short timeout on requests to this proxy, much shorter than it would tolerate for a direct Bedrock call.

The proxy does not decide what happens if it is unreachable (connection refused, timeout) — that is a policy choice for the consumer, and different consumers will want different answers:

  • Fail loud: treat an unreachable proxy as a hard error. Correct when cost control is a compliance requirement, not a nicety.
  • Degrade to direct, unmetered Bedrock: treat the proxy as best-effort observability, and fall through to calling Bedrock directly (unmetered, uncapped) if the sidecar can't be reached. Correct when availability matters more than metering that one call.

Document whichever choice your calling code makes; the proxy has no opinion.

Packaging: a wrapper script, not Node SEA

Node's Single Executable Application (SEA) feature requires the whole application to be pre-bundled into one file via esbuild. tmct cannot be esbuild-bundled — it reads its reference corpus off the filesystem, uses createRequire, and lazily requires node:sqlite — so SEA is not an option here. Instead, build/build-layer.mjs assembles a layer zip containing a real linux-x64 Node binary plus the unbundled .mjs source tree (src/, node_modules/, including the workspace-linked @polycode-projects/bedrock-meter, tmct, and the optional @aws-sdk/client-bedrock-runtime):

layer.zip
  extensions/bedrock-meter-proxy    # shim; the only file the Extensions
                                     # loader execs directly
  bedrock-meter-proxy/
    node                             # real linux-x64 node binary
    src/... + node_modules/...       # proxy + runtime + tmct, unbundled

build/build-layer.mjs requires npm, tar, and zip on PATH, and network access to download the pinned Node build. Its output (packages/proxy/build/) is gitignored — the git repo does not commit a prebuilt layer. The published npm tarball does, though: CI runs build:layer before npm publish, so node_modules/@polycode-projects/bedrock-meter-proxy/build/layer.zip is a real, built zip once you npm install the package — a non-Node CDK app's Code.fromAsset (or any other asset-loading step) can point straight at that path without running the build script itself. Actually deploying and smoke-testing the layer against a real Lambda is a separate, operator-run step (not exercised by this package's own test suite, which runs the HTTP contract as a plain node:http server with no AWS involved).

Metrics

createServer({ metrics, metricsScopeKeys }) accepts the same neutral metrics exporter the runtime's makeAccountant does — wrap an OpenTelemetry Meter with the runtime's otelMetricsExporter(), or pass your own callback. This is programmatic-only today: the standalone bedrock-meter-proxy entrypoint (src/cli.mjs, src/extension/register.mjs) has no environment-variable wiring for it, since a JS callback cannot be passed in through an env var — that would need a config-module-path mechanism this package has not designed.

Environment variables

Read by both src/cli.mjs (a standalone process) and src/extension/register.mjs (the Lambda Extension):

| Variable | Default | Meaning | |----------------------------------|---------------------------------------|---------| | BEDROCK_METER_PROXY_PORT | 8790 | Port the server listens on (always on 127.0.0.1) | | BEDROCK_METER_TMCT_REPO_PATH | a fresh temp dir / /tmp/bedrock-meter-proxy-tmct | Where the embedded tmct session's sqlite memory + session log live | | DDB_TABLE_NAME | unset (uses memoryStore()) | If set, uses dynamoStore() for the ledger instead | | AWS_REGION | SDK default resolution | Region for dynamoStore() and the real Bedrock client |

Cap overrides (DAILY_GLOBAL_CAP_P etc.) are read directly by @polycode-projects/bedrock-meter's cap-check — this package does not re-surface them under different names.

Lambda Extensions API registration

src/extension/register.mjs registers for the SHUTDOWN event only, not INVOKE. This extension does no per-invocation work — it just needs the HTTP server up before the function's first invoke and closed cleanly at shutdown — and subscribing to INVOKE would force the Lambda runtime to wait on this extension's own Next call after every single invocation for a sidecar with nothing invoke-scoped to do. See the comments in register.mjs for the exact registration payload and the AWS documentation it was verified against.

Two things this package cannot self-verify without a real deployed Lambda: the packaged Node binary's actual glibc/kernel compatibility inside a live execution environment, and CPU-throttling behavior during Init for an extension that starts a long-lived server rather than returning quickly. Both are call-outs for the operator-run smoke test against a real test stack, not something the unit test suite here can cover.