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

@proxon/agent-observer

v0.1.0

Published

Drop-in agent-cost telemetry SDK: auto-instruments the Anthropic + OpenAI SDKs (ESM, via import-in-the-middle), maps per-call token usage to the Proxon-native agent-cost event, and fire-and-forget POSTs it to the ingest tier (POST /v1/agent-cost/events).

Readme

@proxon/agent-observer

Drop-in agent-cost telemetry for your own bot or agent. Install it, launch with one flag, and every LLM call your process makes reports per-agent / per-task / per-owner cost + token usage into Proxon — with zero call-site changes.

It emits the Proxon-native agent-cost schema (the same contract Proxon dogfoods). Providers return tokens, not dollars; Proxon recomputes cost server-side from a maintained price book, so you never ship pricing in your agent. It is fire-and-forget: it never throws into — or slows — your request path, and if it isn't configured it emits nothing (a clean no-op).

  • Runs anywhere Node does — CI, containers, servers, a laptop. Node ≥ 18.19.
  • Instruments the Anthropic + OpenAI SDKs (and OpenAI-compatible providers like DeepSeek via OPENAI_BASE_URL). A Python SDK is planned; until then, non-Node apps use OpenTelemetry.
  • Never sees prompt or response content — only token counts + metadata.

Quickstart (60 seconds)

  1. Get a service credential. In Proxon, go to Admin → Service Credentials and generate one. You get a prx_svc_… bearer, shown once — copy it.

  2. Install:

    npm i @proxon/agent-observer
  3. Point the SDK at Proxon (via env — see Configuration):

    export PROXON_OBSERVER_INGEST_URL="https://ingest.proxon.dev"   # your ingest host
    export PROXON_OBSERVER_SERVICE_CREDENTIAL="prx_svc_<prefix>_<secret>"
    export PROXON_OBSERVER_AGENT="my-agent"                          # what this process reports as
  4. Launch your app with the register flag — no code changes:

    node --import @proxon/agent-observer/register app.js

That's it. Every Anthropic/OpenAI call your app makes now reports its cost to Proxon. You'll see a one-line confirmation banner in your logs at startup:

[proxon-agent-observer] active — reporting agent-cost to https://ingest.proxon.dev as agent "my-agent" (incl. spawned claude CLI subprocesses)

What do you want to do?


Instrument a Node app I run

The recommended path. Launch your process with the register entry — it patches the Anthropic + OpenAI SDKs at module load via the OpenTelemetry-standard import-in-the-middle loader hook:

node --import @proxon/agent-observer/register app.js

No source edits. @anthropic-ai/sdk and openai are optional peer dependencies — the SDK instruments whichever you already depend on and never adds the other to your runtime graph.

ESM only. The loader hook is ESM-native. The package itself is ESM ("type": "module"). A CommonJS app can't use --import auto-instrumentation (native CJS support via require-in-the-middle is a planned follow-up) — use wrap() instead.

OpenAI chat streaming: pass stream_options: { include_usage: true } to chat.completions.create — without it the API returns no token counts and streamed calls are silently skipped (no cost row). The SDK warns once per process when it detects a usage-less stream. (OpenAI Responses streaming always includes usage; Anthropic streaming is unaffected.)

Instrument a client I construct myself

If you can't use --import (a CommonJS app, a bundled runtime, or you just prefer an explicit call), wrap the client instance in place:

import Anthropic from '@anthropic-ai/sdk';
import { wrap } from '@proxon/agent-observer';

const anthropic = wrap(new Anthropic()); // also works for `new OpenAI()`

wrap() returns the same client, now instrumented; calling it twice on the same client is a no-op. Everything else (config, attribution, flushing) is identical to the auto-instrument path.

Attribute cost to an agent / task / customer

The instrumentation can't know what a given messages.create is for — that's business context you supply. Wrap a unit of work in withContext and every event emitted inside it (across await boundaries) inherits the attribution:

import { withContext, flush } from '@proxon/agent-observer';

await withContext(
  {
    agent: 'support-bot',
    task_type: 'triage',
    linear_ticket: 'PRX-42',
    owner: 'octocat',
    // Open-ended custom dimensions — the Proxon rollup auto-discovers and
    // breaks down by whatever keys you supply. No schema change needed.
    attributes: { tenant: 'acme', feature: 'search' },
  },
  async () => {
    await anthropic.messages.create({
      /* ... */
    }); // auto-attributed with everything above
  }
);

await flush(); // ship the final batch on graceful shutdown / after a one-shot batch

Fixed attribution fields (first-class dashboard dimensions): agent, task_type, run_type, linear_ticket (string), pr_number (number), owner, outcome, plus runId (auto-generated if you don't set one) and querySource. Anything else goes in the generic attributes bag (string → string|number), which the rollup discovers as attr:<key> breakdowns automatically.

runWith is an alias for withContext if that name reads better in your code.

Track a multi-process agent fleet

For an orchestrator that spawns child agents in separate processes, carry the active context across the process boundary so the child's cost rolls up under the parent. It's two explicit touchpoints — propagateContext() on the parent, contextFromEnv() on the child.

Parent — serialize the active context into the child's env:

import { propagateContext } from '@proxon/agent-observer';
import { spawn } from 'node:child_process';

// Inside the parent's withContext(...) run:
spawn('node', ['--import', '@proxon/agent-observer/register', 'child.js'], {
  env: { ...process.env, ...propagateContext() },
});

Child — rehydrate it and run your work under it. The register entry does not do this for you; the child's contextFromEnv() is the one SDK touchpoint at the spawn boundary (run standalone, it returns undefined and the work emits as a normal main run):

import { contextFromEnv, withContext } from '@proxon/agent-observer';

const inherited = contextFromEnv();
if (inherited) {
  await withContext(inherited, run);
} else {
  await run();
}

The child becomes a sub-agent: it gets a fresh run_id, its query_source is forced to subagent, and the parent's run_id is carried as the child's parent_run_id — so orchestrator → sub-agent cost links end-to-end. The generic attributes bag propagates too (so a child inherits tenant/feature/…); outcome does not (a run's outcome isn't known until after its children were spawned).

Black-box / OpenTelemetry-only parents: if your parent never calls withContext but does set OTel Baggage, the SDK reads that baggage at emit time and maps it to attribution for you (see Attribution precedence).

Capture a spawned claude CLI

When you launch with --import @proxon/agent-observer/register, the SDK also captures the LLM spend of claude CLI subprocesses your process spawns (e.g. claude --print …) — zero code, through the same emitter, config, and attribution as in-process calls. On by default.

The --import hooks only see in-process SDK calls, and a spawned claude is a separate process. So the SDK instruments child_process.spawn/execFile: when it detects a claude spawn it injects Claude Code's OpenTelemetry metrics export into the child (pointed at a loopback 127.0.0.1 receiver inside your process), receives the OTLP metrics in-process, and emits one agent-cost event per spawn. The spawned claude is attributed as a sub-agent (query_source = subagent, its own run_id, parent_run_id + agent / task_type / linear_ticket / … carried from the active withContext at spawn time). provider = anthropic, endpoint = claude-code-cli.

It only injects telemetry env the child didn't already have — if you run your own Claude Code OpenTelemetry (OTEL_EXPORTER_OTLP_ENDPOINT or CLAUDE_CODE_ENABLE_TELEMETRY set in the environment, any value including 0), the SDK defers to your setup and stays out of the way. No credential or non-OTEL secret is ever passed to the child.

Opt out with PROXON_OBSERVER_CAPTURE_SUBPROCESSES=0.

  • Limitation: this covers the claude CLI specifically (it exports OTLP metrics natively). Any other subprocess that makes its own LLM calls needs its own --import @proxon/agent-observer/register (or your own OpenTelemetry pipeline) to be captured.
  • One-tick startup race: the loopback receiver binds asynchronously at import — register never blocks your app's startup to wait for it. A claude spawn in your process's very first synchronous tick (before the bind completes) may be missed. In practice this never matters: event-driven agents spawn claude in response to a request/event, i.e. after their first async turn, by which point the receiver is long since bound. This is the deliberate safe trade-off (a telemetry SDK must never gate or slow the host's startup).

Not a Node app?

The SDK and OpenTelemetry do the same job two ways. For a Node app you control, the SDK auto-instruments it with one line. For anything else — another language, or a tool that already emits OpenTelemetry (Claude Code, GitHub Copilot, your own OTLP exporter) — point that exporter at Proxon's OTLP endpoint instead; no new agent, no proxy, no certificate. Either is enough; running both adds detail, not new coverage.


Configuration

Configure via PROXON_OBSERVER_* environment variables, or programmatically with configure({ … }) / init({ … }) (an alias) at startup. Programmatic options take precedence over env.

| Variable | Required | Description | | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PROXON_OBSERVER_INGEST_URL | yes | Base URL of your Proxon ingest host (e.g. https://ingest.proxon.dev); /v1/agent-cost/events is appended. | | PROXON_OBSERVER_SERVICE_CREDENTIAL | yes | The prx_svc_<prefix>_<secret> service credential (Admin → Service Credentials). | | PROXON_OBSERVER_AGENT | yes* | The agent name this process reports as. *Required unless you supply agent per-run via withContext({ agent }) / configure({ agent }); an event with no resolvable agent is dropped. | | PROXON_OBSERVER_BATCH_SIZE | no | Events buffered before an automatic flush (default 20). | | PROXON_OBSERVER_FLUSH_INTERVAL_MS | no | Max ms an event waits before an automatic flush (default 5000). | | PROXON_OBSERVER_ATTRIBUTES | no | Process-wide default attributes, as a JSON object (e.g. {"env":"prod"}). Merged into every event, below per-run withContext attributes. Malformed JSON is ignored, never fatal. | | PROXON_OBSERVER_DISABLED | no | Kill-switch. Only the exact value 1 disables — true/yes/on are NOT recognised (deliberate, so an ambient boolean env var can't silently turn telemetry off). | | PROXON_OBSERVER_CAPTURE_SUBPROCESSES | no | Spawned claude CLI capture (above). On by default; only the exact value 0 opts out (same exact-match discipline as PROXON_OBSERVER_DISABLED). | | PROXON_OBSERVER_AUTO_ATTRIBUTES | no | Auto-detect ambient attribution (env/host/runtime/CI) into the attribute bag. On by default; 0 opts out. | | PROXON_OBSERVER_BAGGAGE | no | Read OpenTelemetry Baggage at emit time and map it into attribution. On by default; 0 opts out. No-op if @opentelemetry/api isn't installed. | | PROXON_OBSERVER_INFER_FROM_PROMPT | no | Let the wrappers infer attribution from request content (e.g. a Linear ticket id in the prompt). Opt-in for privacy — off by default; 1 enables. Gap-fills only. |

Every knob above is also an ObserverOptions field on configure() (camelCase: ingestUrl, serviceCredential, agent, batchSize, flushIntervalMs, attributes, enabled, captureSubprocesses, autoAttributes, readBaggage, inferFromPrompt).

Note: the credential's secret is sensitive — keep it in your secret manager, not in code. Rotate/revoke it any time from Admin → Service Credentials.


How attribution is resolved

Attribution is layered, lowest → highest precedence — a higher layer only gap-fills or overrides what's below it, so you can lean on the free ambient signal and override exactly what you care about:

  1. Prompt inference (inferFromPrompt, opt-in) — a fuzzy gap-fill, e.g. a Linear ticket id in the request content; the lowest layer, overridden by everything below.
  2. Ambient auto-detect (autoAttributes) — env / host / runtime / CI, for free.
  3. Process defaults — configure({ attributes }) / PROXON_OBSERVER_ATTRIBUTES.
  4. OpenTelemetry Baggage (readBaggage) — active baggage at emit time: enduser.id → owner, every other key → an attr:<key>. Beats config and ambient — only withContext overrides it.
  5. Per-run context — withContext({ … }). Always wins.

So a customer already on OpenTelemetry gets meaningful attribution with no Proxon-specific code at all (baggage + ambient), while a customer who wants exact control uses withContext.


What Proxon receives (the wire event)

Each captured call becomes one agent-cost event. The SDK reports tokens, not dollars — Proxon recomputes cost_usd server-side from its price book (the SDK never ships pricing). Every SDK event is stamped billing_mode: api_metered (it instruments an API-key client, so the tokens are real cash spend). The public AgentCostEvent / QuerySource / BusinessContext / ObserverConfig / ObserverOptions / WrapDeps types are exported for reference.

Token classes are reported separately, never summed (and an explicit 0 is kept as a real datum): tokens_input, tokens_output, tokens_cache_read, tokens_cache_write, tokens_reasoning, tokens_audio_input/output, tokens_image_input/output. Content (prompts, responses) is never sent.


API reference

The package export is intentionally narrow — only what a customer calls:

| Export | What it does | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | wrap(client) | Instrument an Anthropic/OpenAI client instance in place (returns the same client). Idempotent. | | withContext(ctx, fn) / runWith | Run fn with ctx as the active attribution; every event emitted inside inherits it. | | getContext() | The active BusinessContext, or undefined outside any withContext. | | propagateContext(ctx?, querySource?) | Serialize the active (or given) context into env vars for a spawned child (defaults to subagent). | | contextFromEnv(env?) | Rehydrate a BusinessContext from the env vars propagateContext wrote — call it in the spawned child, then run under withContext(inherited, …). | | configure(opts) / init(opts) | Set config programmatically (alias). Overrides env. | | getConfig() / loadConfigFromEnv() | Read the resolved config / re-read it from env. | | flush() | Ship the buffered batch now — call on graceful shutdown or after a one-shot run. | | CONTEXT_ENV | The env-var names propagateContext/contextFromEnv use (for advanced/manual wiring). |

The mappers, event builder, transport, and per-provider instrument* functions are not exported — reach the wrappers through wrap(). This keeps the public surface stable.


Runnable examples

Two complete, copy-paste programs (they mirror the SDK's own CI e2e fixtures).

Zero-edit auto-instrumentation — nothing Proxon-specific in the file; the telemetry is bolted on at launch via --import:

// app.mjs — run: node --import @proxon/agent-observer/register app.mjs
// env: PROXON_OBSERVER_INGEST_URL, PROXON_OBSERVER_SERVICE_CREDENTIAL,
//      PROXON_OBSERVER_AGENT   (+ ANTHROPIC_API_KEY for the SDK client)
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

const message = await client.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 64,
  messages: [{ role: 'user', content: 'Say ok.' }],
});

console.log('usage:', JSON.stringify(message.usage));

Custom attribution dimensions — stamp every call inside the callback with your own tenant / feature / owner; Proxon auto-discovers the breakdown:

// run: node --import @proxon/agent-observer/register app.mjs
import Anthropic from '@anthropic-ai/sdk';
import { withContext } from '@proxon/agent-observer';

const client = new Anthropic();

await withContext({ attributes: { tenant: 'acme', feature: 'checkout' } }, async () => {
  const message = await client.messages.create({
    model: 'claude-sonnet-4-5',
    max_tokens: 64,
    messages: [{ role: 'user', content: 'Say ok.' }],
  });
  console.log('usage:', JSON.stringify(message.usage));
});

The OpenAI SDK auto-instruments the exact same way (swap the import). The multi-process and spawned-claude-CLI shapes are covered in Track a multi-process agent fleet and Capture a spawned claude CLI above.


Confirm it's working / troubleshoot

Telemetry is optional infra, but the SDK is not silent about its state. At startup the --import entry logs one [proxon-agent-observer] banner so you can confirm it from your process logs:

[proxon-agent-observer] active — reporting agent-cost to https://ingest.proxon.dev as agent "my-agent" (incl. spawned claude CLI subprocesses)
[proxon-agent-observer] loaded but INACTIVE — set PROXON_OBSERVER_INGEST_URL and PROXON_OBSERVER_SERVICE_CREDENTIAL to emit telemetry (currently a no-op)

| Symptom | Likely cause / fix | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | loaded but INACTIVE banner | PROXON_OBSERVER_INGEST_URL and/or PROXON_OBSERVER_SERVICE_CREDENTIAL unset. Set both. | | No banner at all | The register entry didn't load — check you're launching with node --import @proxon/agent-observer/register … (ESM), not a bare node app.js. | | Banner is active but no rows in Proxon | An event with no resolvable agent is dropped — set PROXON_OBSERVER_AGENT or pass agent via withContext/configure. Also confirm the credential is live (Admin → Service Credentials). | | OpenAI streamed calls missing | Pass stream_options: { include_usage: true } to chat.completions.create (see above). | | CommonJS app, --import does nothing | Auto-instrumentation is ESM-only — use wrap(). | | Spawned claude spend missing | Ensure PROXON_OBSERVER_CAPTURE_SUBPROCESSES isn't 0, and that you launched with --import (the capture rides the register entry). |

Runtime failures are swallowed with at most a single warn per failure streak — the SDK never throws into your request path. If you need to hard-disable it, set PROXON_OBSERVER_DISABLED=1.