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

@superpenguin/js

v0.17.3

Published

SuperPenguin JavaScript SDK for AI cost attribution across native provider SDKs, Vercel AI SDK, and OpenTelemetry

Readme

SuperPenguin JavaScript SDK

TypeScript SDK for request-level AI cost attribution. It wraps native provider SDKs for OpenAI, Anthropic, Google Gemini, AWS Bedrock, Deepgram, and ElevenLabs, and still supports Vercel AI SDK helpers plus OpenTelemetry span ingestion. Use it in server code only. Do not expose SP_API_KEY to browsers.

Install

npm install @superpenguin/js
npm install openai @anthropic-ai/sdk @google/genai @deepgram/sdk @elevenlabs/elevenlabs-js

Install only the provider package(s) you actually use. Provider SDKs are optional peer dependencies so @superpenguin/js does not force all of them into your app.

Live spend reads

Read the latest spend observed for your organization before starting another AI request. The same SDK key authorizes ingestion and spend reads.

import { init, SuperPenguinSpendError } from "@superpenguin/js";

const superpenguin = init({ apiKey: process.env.SP_API_KEY });

try {
  const current = await superpenguin.spend.get({
    window: "1d",
  });
  console.log(current.spendUsd, current.asOf, current.stalenessMs);
} catch (error) {
  if (error instanceof SuperPenguinSpendError && error.retryable) {
    // Decide whether your application should retry, queue, or continue.
  }
}

Supported windows are 1d through 7d, 14d, and month. These are UTC calendar-day windows: 1d is today, numbered windows include today plus the preceding N - 1 days, and month is the current calendar month through today. They are not rolling-hour windows. Filter by feature, model, or both:

const filtered = await superpenguin.spend.get({
  window: "7d",
  feature: "document-processing",
  model: "gpt-5",
});

spendMicros is the exact USD-micro total as an integer string; spendUsd is a convenience number. window contains the resolved UTC start, exclusive end, type, and timeZone. asOf is when the reader loaded the newest snapshot used by the response, and stalenessMs conservatively estimates how far it may trail usage already accepted by SuperPenguin. There is no public freshThrough field. The result is advisory, not a budget reservation.

The SDK flushes its local queue before reading by default, but ingestion and materialization remain asynchronous. Set flush: false to skip that flush. Identical concurrent reads in the same process share one flush and HTTP request.

Production spend reads use https://spend.superpenguin.ai automatically. spendBaseUrl or SP_SPEND_BASE_URL is an optional advanced override that affects only spend reads; usage flushes continue to use baseUrl or SP_BASE_URL. An explicit custom baseUrl is the fallback for both paths unless a spend-only override is set.

SuperPenguinSpendError exposes code, retryable, and optional HTTP status. The SDK makes one bounded spend request and does not retry automatically. Codes are invalid_request, invalid_api_key, rate_limited, spend_unavailable, and request_failed.

Native Provider SDKs

import OpenAI from "openai";
import { init, wrap } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });

const openai = wrap(new OpenAI(), {
  metadata: {
    environment: "production",
    feature: "support_agent",
  },
});

await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Write a concise reply." }],
  spMetadata: {
    customer_id: "cust_acme_123",
  },
});

For AWS Bedrock, wrap a BedrockRuntimeClient (AWS SDK v3). The client uses a command-dispatch surface, so wrap() patches client.send and tracks Converse / ConverseStream commands:

import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
import { init, wrap } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });

const bedrock = wrap(new BedrockRuntimeClient({ region: "us-east-1" }), {
  metadata: { environment: "production", feature: "support_agent" },
});

await bedrock.send(
  new ConverseCommand({
    modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages: [{ role: "user", content: [{ text: "Write a concise reply." }] }],
  }),
);

wrap() detects supported clients automatically. Provider-specific exports are also available: wrapOpenAI, wrapAnthropic, wrapGoogleGenAI, wrapBedrock, wrapDeepgram, and wrapElevenLabs.

Supported methods in 0.5.0:

  • OpenAI: chat.completions.create, completions.create, embeddings.create, responses.create, including async iterable streams where the SDK returns usage.
  • Anthropic: messages.create and messages.stream().finalMessage().
  • Google GenAI: models.generateContent and models.generateContentStream.
  • AWS Bedrock: BedrockRuntimeClient.send() for ConverseCommand and ConverseStreamCommand (other commands pass straight through). Bedrock is priced server-side against SuperPenguin's aws_bedrock rate card.
  • Deepgram: listen.v1.media.transcribeUrl and transcribeFile.
  • ElevenLabs: textToSpeech.convert, textToSpeech.stream, plus sound effects convert / stream when present.

Use spMetadata or sp_metadata on a request for per-call attribution. The wrapper removes those SuperPenguin-only fields before forwarding the provider request. The provider endpoint host is also captured automatically as base_url (handy for gateways/proxies).

LiteLLM proxy

Point an OpenAI client at your LiteLLM proxy and wrap it. Pass provider: "litellm" unless the proxy's host name contains "litellm" (then it is detected automatically). SuperPenguin records the call under the litellm route and uses LiteLLM's own per-request cost: the x-litellm-response-cost header for normal calls, and usage.cost on the last chunk for streams.

import OpenAI from "openai";
import { wrapOpenAI } from "@superpenguin/js";

const client = wrapOpenAI(
  new OpenAI({ apiKey: process.env.LITELLM_API_KEY, baseURL: "http://localhost:4000/v1" }),
  { provider: "litellm" },
);

Metadata fields

Recognized first-class attribution keys (anything else is stored as custom tags):

| Field | Purpose | |-------|---------| | customer_id | End-customer or account consuming the call | | feature | Product feature name | | team | Internal team owning the feature | | environment | production, staging, dev, etc. | | prompt_key | Identifier for the prompt template | | prompt_version | Version of the prompt template | | session_id | Groups one conversation (used by content capture) | | turn_id | Groups one turn, which may span multiple model calls (used by content capture) |

Batch APIs

Keep wrap() on a shared OpenAI-compatible client used for both live and deferred requests. The wrapper skips successful non-streaming job acknowledgements that have missing or null usage, or a queued / in_progress status. It still records terminal responses with an explicit zero-valued usage object and all failed requests.

When deferred work finishes, submit each completed item with getClient().submit(...) or a track*Batch helper and a stable idempotency_key.

Provider Batch APIs return results asynchronously, so there is no live call to wrap. After a batch job completes, pass the retrieved batch to the matching tracker and one attribution row per request is recorded with batch=true:

import OpenAI from "openai";
import { init, trackOpenAIBatch } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });
const openai = new OpenAI();

const batch = await openai.batches.retrieve("batch_abc");
const count = await trackOpenAIBatch(openai, batch, {
  metadata: { feature: "nightly-eval" },
});
// Also: trackAnthropicBatch(client, batch, ...) and trackGoogleBatch(...)

Batch and flex results with prompt samples (0.13.0)

submit() and track*Batch record usage only, so batch work produces no prompt samples. If you already loop over results yourself (for example Sail batch or flex jobs), call trackBatchResult instead of submit(). It submits the usage event you already build, unchanged, and also samples the request and response the way wrap() does (when your organization has content capture on):

import { flush, init, trackBatchResult } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });

for (const line of outputLines) {              // the job's output JSONL, parsed
  const customId = line.custom_id;
  const event = buildUsageEvent(line);         // the object you pass to submit() today
  await trackBatchResult({
    request: requestLines[customId].body,      // the matching input line's body
    response: line.response.body,
    usageEvent: event,
  });
}
await flush();

Chat (messages) and Responses (instructions, input, output) bodies are both read, including the system prompt. Sampling is decided before any text is read, and nothing is read when your organization's capture setting is off. The sample is stamped with the usage event's created_at (or completedAt when the event has none), carries its feature and other attribution (a withMetadata scope cannot override them), and reporting the same result twice stores one sample. Capture errors never affect the usage event. Without usageEvent, pass provider, batchId and the line's customId (taken from whole input or output lines automatically) and the SDK builds the same event trackOpenAIBatch would.

Batch prompt samples without storing prompts (0.15.0)

Batch output files hold only responses, so the call above means keeping every prompt until its result returns. Instead, send each half when you already have it, and the server joins them on the line's idempotency key:

import { flush, init, trackBatchRequest, trackBatchResult } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });

// At submit time, once the batch id is known: the prompt half.
for (const line of inputLines) {
  await trackBatchRequest({
    key: `${batch.id}:${line.custom_id}`,      // the key the usage event will carry
    request: line,                             // the input line (its body is read)
    metadata: { feature: "ai_extractor" },
    provider: "sail_research",
  });
}

// On completion: usage, plus the output half. No request needed.
for (const line of outputLines) {
  const event = buildUsageEvent(line);         // idempotency_key = `${batch.id}:${custom_id}`
  await trackBatchResult({ usageEvent: event, response: line.response.body });
}
await flush();

Only sampled lines send anything. Both calls hash the key against your organization's stable batch rate, so they agree without shared state, and an unsampled line reads no text and makes no network call. The usage event is submitted unchanged, exactly as with submit(). The same opt-in, redaction and encryption apply as for every sample. A prompt half whose result never arrives is discarded after 14 days.

Content capture (opt-in)

Off by default. The SDK sends only cost metadata, never prompt or completion text, unless your organization opts in from Settings (Pro or Enterprise). Once enabled, a sampled subset of calls is captured automatically and encrypted at rest. A sample holds the whole call: the request exactly as you passed it (every message in order with its role, system and developer instructions, tool calls and tool results, tool definitions and model parameters) and the full response (every choice or output item, tool calls, reasoning summaries and plain thinking text, usage). Streamed responses are captured as the assembled response. Nothing is truncated. Images, audio and other binary data are replaced by their size ([STRIPPED_BINARY:<bytes>]), encrypted provider blobs (thinking signatures, encrypted_content) by their length, and transport settings (API keys, headers, timeouts, abort signals) are never captured.

Capture never slows down or changes your calls. On your call's path the SDK only decides whether to sample and copies the top-level request; redaction, serialization and gzip compression run in the background, yielding to the event loop. Usage and spend events are sent first and are identical with capture on or off. Memory is bounded: at most 32 sampled calls wait for processing and at most 32 MiB of compressed samples (and 200 samples) wait for upload; beyond that a sample is dropped and counted, never blocking. One sample is capped at 64 MiB of uncompressed content. Samples are sent gzip compressed; a sample over 3.5 MiB compressed is uploaded in parts. In a serverless function, call await flushContentCapture() (or client.flush()) before returning so sampled calls are sent.

Before a sample is sent, the SDK replaces secrets (provider API keys, tokens, private keys, Azure and Google Cloud keys and signed-link signatures, passwords in URLs and password=-style assignments, secret-named tool arguments) with [REDACTED_SECRET], and emails, phone numbers, payment cards, US SSNs, IBANs and public IP addresses with [REDACTED_EMAIL], [REDACTED_PHONE], [REDACTED_CARD], [REDACTED_SSN], [REDACTED_IBAN] and [REDACTED_IP]. This covers every string in the request and the response, including tool-call arguments, however long. Names, addresses and metadata values are not redacted. The exact list is in the content capture docs.

Group multi-turn conversations with session_id (one conversation) and turn_id (one turn). Each model call is its own captured row, so a turn with a tool call (model -> tool -> model) spans two rows; reuse the same turn_id across them to group. If turn_id is omitted, each row gets a fresh random one and will not group.

await openai.chat.completions.create({
  model: "gpt-4o",
  messages,
  spMetadata: {
    session_id: conversationId,
    turn_id: turnId, // reuse across a tool round-trip to group calls
    customer_id: "cust_acme_123",
  },
});

configureContentCapture() can only reduce capture (it never enables it, the Settings opt-in is always required):

import { configureContentCapture } from "@superpenguin/js";

configureContentCapture({
  allow: true,          // false vetoes ALL capture from this process
  recordPrompt: true,   // false drops inputs, keeps outputs
  recordOutcome: true,  // false drops outputs, keeps inputs
  sampleRateMultiplier: 1, // scale the server sample rate down (0-1)
  redactor: (text) => text, // extra rules, run on each string after the built-in redaction
  maxQueueBytes: 32 * 1024 * 1024, // compressed bytes allowed to wait for upload
});

getContentCaptureStats() returns counters: samples queued and sent, and samples dropped because a queue was full (droppedQueueFull), over the 64 MiB ceiling (droppedSampleCeiling), too large to send in a runtime without CompressionStream (droppedNoCompression), or that failed to build (serializeErrors). Drop counts are also reported to SuperPenguin with the next upload.

Upgrading to full content capture (0.16.0)

0.16.0 stores the whole sampled call (content schema 3). Earlier versions kept only the system text and the last user message and cut every field at 64 KB. To upgrade:

  1. npm install @superpenguin/js@latest. No other change is needed for calls that already go through wrap(), the Vercel AI SDK middleware or helpers, createLangChainCallback, trackPiStreamFn, submitSpan or trackBatchResult: they now send the full request and response.
  2. Set session_id (one conversation or agent run) and turn_id (one turn, reused across the model calls of a tool round-trip) from whichever source your integration uses (examples below). Samples without them cannot be grouped into conversations.
  3. In serverless handlers (Vercel, Lambda, Cloudflare Workers), call await flushContentCapture() before returning. Capture runs in the background after your call returns; a frozen or terminated function drops what has not been sent.
  4. Use exactly one reporting path per model call. Do not both wrap a client (or model) and report the same call with trackUsage: that counts its usage twice.

Each stored document also carries a blocks index (a SHA-256 fingerprint per system prompt, tool list, message and output item, computed after redaction) so repeated conversation prefixes can be deduplicated later. You do not need to do anything with it.

Content protocol 4 (0.17.0): deduplicated capture. When SuperPenguin turns on content protocol 4 for your organization, 0.17.0 uploads and stores each large piece of text in a call (any JSON string of 1 KiB or more, such as a file the agent read, a tool output or the system prompt) once per conversation per UTC day, instead of with every call. A stored call still rebuilds to exactly the document 0.16.0 would send. No code change is needed. Until protocol 4 is on, 0.17.0 captures exactly as 0.16.0 does.

  • Sampling. A whole conversation is sampled or not for the day, from its conversation key: session_id, or a field SuperPenguin sets for your organization. Every process makes the same choice.
  • Session ids. Use one stable session_id per agent run, shared by its subagents, and never a user id. A call without a conversation key is still captured, but gets no dedupe.
  • Process affinity. Dedupe works per process: a process uploads a conversation's texts the first time it sees them that day. Routing a run's calls to one process uploads the least.
  • The usage stamp. A captured call's usage event gains one field, content_sample_id, which links it to its content. No cost or usage value changes.
  • Options. forceSample and sampleRateMultiplier do not apply under protocol 4. allow: false still stops all capture. Counters for protocol 4 are in getContentCaptureStats().v4.

wrap() (OpenAI, Anthropic, Google GenAI, Bedrock and compatible clients). Defaults on the wrapper, per call with spMetadata, or for a whole run with withMetadata:

import OpenAI from "openai";
import { flushContentCapture, init, withMetadata, wrap } from "@superpenguin/js";

init({ apiKey: process.env.SP_API_KEY });
const openai = wrap(new OpenAI(), { metadata: { feature: "support_agent" } });

await withMetadata({ session_id: conversationId, customer_id: "cust_acme_123" }, async () => {
  await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools,
    spMetadata: { turn_id: turnId },
  });
});
await flushContentCapture(); // serverless only, before returning

Vercel AI SDK middleware (AI SDK 6 "v3", AI SDK 7 "v4"). The middleware cannot see generateText options, so put the ids in withMetadata (or in the middleware's metadata defaults):

import { generateText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { superpenguinMiddleware, withMetadata } from "@superpenguin/js";

const model = wrapLanguageModel({
  model: openai("gpt-4o"),
  middleware: superpenguinMiddleware({ specificationVersion: "v3", metadata: { feature: "chat" } }),
});

await withMetadata({ session_id: chatId, turn_id: messageId }, () =>
  generateText({ model, messages, tools }),
);

With trackGenerateText / trackStreamText, pass the ids in the call's metadata option; ids in the call's telemetry.metadata or experimental_telemetry.metadata are also used for samples. With submitSpan, span attributes sp.session_id / sp.turn_id or ai.telemetry.metadata.session_id / .turn_id are used.

LangChain / LangGraph and Pi. Pass the ids in the invocation's metadata (LangChain) or establish them around the run (both):

await graph.invoke(input, {
  callbacks: [createLangChainCallback({ metadata: { feature: "research" } })],
  metadata: { session_id: threadId, turn_id: stepId },
});

await withMetadata({ session_id: sessionId }, () => agent.prompt(text)); // Pi with trackPiStreamFn

Batch helpers. trackBatchResult({ request, response, usageEvent }) samples batch and flex lines; pass metadata: { session_id, turn_id } (or set them on the usage event) to group them. See "Batch and flex results" above.

Custom agent harnesses (trackUsage). If your harness calls the provider itself (raw fetch, a vendored client) and reports usage by hand, pass the exact request body you sent and the full response body you received. Usage is submitted first and is identical with or without them; the bodies then go through the same sampled, redacted, untruncated capture as wrap(), and are never read for unsampled calls:

import { flushContentCapture, trackUsage } from "@superpenguin/js";

const body = { model: "gpt-4.1", messages, tools, temperature: 0 };
const res = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify(body),
});
const json = await res.json();

await trackUsage(
  {
    provider: "openai",
    model: json.model,
    usage: { inputTokens: json.usage.prompt_tokens, outputTokens: json.usage.completion_tokens },
    statusCode: res.status,
    request: body,      // the provider request body, in the provider's JSON shape
    response: json,     // the full provider response body
  },
  { metadata: { session_id: runId, turn_id: stepId, prompt_key: "coder" } },
);
await flushContentCapture(); // serverless only, before returning

For a streamed call, pass the response you assembled from the stream (all text, tool calls and usage). request alone or response alone is accepted; only the given half is stored.

Vercel AI SDK

import { init, trackGenerateText } from "@superpenguin/js";
import { generateText } from "ai";

init({ apiKey: process.env.SP_API_KEY });

const trackedGenerateText = trackGenerateText(generateText, {
  provider: "vercel_ai_gateway",
  functionId: "support-reply",
  metadata: {
    environment: "production",
    feature: "support_agent",
  },
});

export async function POST(request: Request) {
  const { prompt, customerId } = await request.json();

  const result = await trackedGenerateText({
    model: "xai/grok-4.1-fast-non-reasoning",
    prompt,
    metadata: {
      customer_id: customerId,
    },
    telemetry: {
      functionId: "support-reply",
    },
  });

  return Response.json({ text: result.text });
}

For Vercel AI Gateway calls, set AI_GATEWAY_API_KEY where the Vercel AI SDK runs. Set SP_API_KEY alongside it so SuperPenguin can ingest attribution rows.

Streaming

import { init, trackStreamText } from "@superpenguin/js";
import { streamText } from "ai";

init({ apiKey: process.env.SP_API_KEY });

const trackedStreamText = trackStreamText(streamText, {
  provider: "vercel_ai_gateway",
  functionId: "chat-stream",
});

const result = trackedStreamText({
  model: "xai/grok-4.1-fast-non-reasoning",
  messages,
  metadata: {
    customer_id: "cust_acme_123",
    feature: "chat",
  },
});

The stream wrapper waits for usage in the background. Starting in 0.11.0, the text helpers emit one event per result step when steps are available, preserving multi-step agent usage. Prefer model middleware for agents.

OpenTelemetry

import { submitSpan } from "@superpenguin/js";

await submitSpan(span, {
  metadata: {
    environment: "production",
  },
});

Vercel AI SDK telemetry fields such as functionId and OpenTelemetry trace IDs are stored under custom_tags unless they map to a first-class attribution column.

With content capture on, sampled spans that carry the prompt and response (ai.prompt* / ai.response.* or gen_ai.* attributes) are captured too. Samples take session_id / turn_id from metadata, else from the span attributes sp.session_id / sp.turn_id or ai.telemetry.metadata.session_id / .turn_id. Span export often runs outside your request's withMetadata scope, so set the ids on the span (for example through the Vercel AI SDK's experimental_telemetry.metadata) rather than relying on the scope.

Runtime Behavior

  • Tracking never delays your model call. A wrapped call returns, and a wrapped stream ends, as soon as the provider is done: the event is queued and sent in the background, and nothing on the call's path waits for SuperPenguin's ingest or for content capture (since 0.17.3; earlier versions waited for the event's request). submitWaitMs is deprecated and has no effect.
  • Events start sending at once by default (flushMode: "immediate").
  • Short-lived runtimes: on Vercel (including Next.js on Vercel) the SDK registers each pending send with the request's waitUntil, so the function stays alive until its events are sent (through retries, for at most 30 s), without a dependency on @vercel/functions. On other platforms that offer waitUntil (for example a Cloudflare Worker's ctx.waitUntil), pass it as init({ waitUntil: (p) => ctx.waitUntil(p) }). In Node, pending events are flushed on beforeExit. In a script, a CLI, an AWS Lambda handler or any runtime that may freeze or exit right after your code returns, call await flush() (or await client.flush()) before returning or exiting. With content capture on, flush() also uploads sampled content.
  • Memory is bounded: queued and in-flight usage events hold at most 16 MiB of event JSON (maxQueueBytes), roughly 10,000 to 30,000 events. When the queue is full, the new event is dropped and counted; events already waiting keep their place.
  • A backlog is sent in batches of up to 500 events, with up to 4 requests at once (maxConcurrentRequests); otherwise one request at a time.
  • Each request times out after 15 s (requestTimeoutMs) and is tried 3 times. If it still fails with a network error, timeout, 408, 429 or 5xx, its events go back to the front of the queue and are retried in the background (1 s to 30 s backoff). A resend keeps each event's idempotency_key, so the server counts it once.
  • Every dropped usage event is counted by reason (queue_full, rejected for a permanent 4xx, too_large for an event over the request limit) and reported to SuperPenguin with the next successful request. client.stats() shows the queue and the counters.
  • Attribution failures never fail or delay your model call. With throwOnError: true, send errors are raised from the next flush() instead; onError is called for each one either way.
  • For long-lived workers, pass flushMode: "interval" to batch events and call flush() before shutdown.

Cohere

Available in the published package since JavaScript SDK 0.11.0.

Wrap CohereClientV2 from the optional cohere-ai dependency (version 8.1 or later in the 8.x series). The wrapper captures usage for text Embed, Rerank, Chat and streaming Chat.

import { CohereClientV2 } from "cohere-ai";
import { init, wrap, flush } from "@superpenguin/js";

init({ apiKey: "YOUR_SUPERPENGUIN_KEY" });
const cohere = wrap(new CohereClientV2({ token: "YOUR_COHERE_KEY" }));
await cohere.embed({
  model: "embed-v5.0-fast",
  texts: ["A small penguin."],
  inputType: "search_document",
  embeddingTypes: ["float"],
});
await flush();

You can also wrap an OpenAI client pointed at https://api.cohere.ai/compatibility/v1. Use a Cohere API key and omit input_type for Embed on that endpoint. Use the native Cohere client when you need to specify the input type or call Rerank.

For AWS Bedrock, wrap an AWS SDK v3 client and use promise-based send. Callback-style send does not capture usage. AWS calls are attributed to Bedrock and retain their region.

Cost requires provider-reported billing quantities and a matching model price. Bedrock Rerank responses without billed search units leave per-request cost unknown. Unknown cost does not mean free usage.

Usage tracking for batch jobs, image or multimodal Embed, Parse, Transcribe and SageMaker is not supported. The provider guide lists endpoint restrictions and retired Bedrock Chat models separately from requests with unknown cost.

See the Cohere provider guide for verified models, both API examples and supported operations.

Agent SDKs and dynamic metadata (0.11.0)

Use withMetadata({ customer_id, custom_tags: { ticket_id } }, () => agentRun()) to attach dynamic metadata to model calls in a Node.js execution. Static defaults belong on the wrapped client, superpenguinMiddleware or createLangChainCallback. Nested scopes merge custom tags; explicit per-call metadata overrides scope and defaults. Concurrent runs are isolated.

  • Vercel AI SDK 6/7: pass superpenguinMiddleware() to wrapLanguageModel, using specificationVersion: "v4" for AI SDK 7 and "v3" for AI SDK 6.
  • OpenAI Agents: inject wrap(new OpenAI(), { metadata }) into new OpenAIProvider({ openAIClient: client }), then use that provider in a Runner.
  • LangGraph/LangChain: use createLangChainCallback({ metadata }) in invocation callbacks; invocation metadata supplies dynamic overrides.
  • Pi: wrap the model stream function with trackPiStreamFn; establish dynamic metadata around agent.prompt() or the embedded session invocation.
  • OpenCode stable V1: install createOpenCodePlugin in the server and resolve dynamic metadata with metadataForSession(sessionID). The remote HTTP SDK alone cannot install provider instrumentation.
  • Custom harnesses: inject wrapped clients/models, or report each model call explicitly using trackUsage({ provider, model, usage, request, response }) (request / response are optional and feed content capture; see "Upgrading to full content capture" above).

Each invocation must have exactly one usage capture path. Consume agent streams inside the metadata scope, and reestablish the scope after queue/process boundaries or checkpoint resumes. Missing usage cannot be reconstructed, and opaque SDKs need an integration hook. The HTTP OpenAI Agents path does not cover Responses WebSocket mode or Realtime.

Opt-in content capture records each model call's request as the framework received it (messages or prompt, system, tools, settings) and its full result, including tool calls, tool results and plain reasoning text the provider returns; encrypted reasoning is replaced by its length. Positive provider-reported reasoning counts are stored as custom_tags.reasoning_tokens, already included in output totals.

Run the bounded local analytics example from sdk/js with npx tsx examples/analytics-agent.ts; it uses fake model and ingest transports.

Complete examples and support limits are included in the npm package's agent guide, generated from the canonical public documentation during packaging.

Decision APIs (available since 0.12.0)

This release is published. Production readiness is recorded in PR #815, and package publication is recorded in PR #819.

wrap(client) detects OpenAI and TypeSafe clients. For OpenAI, the same wrapped client captures generation through responses.create / chat.completions.create and decisions through decisions.create; no separate decision wrapper or per-call tracking helper is needed. The explicit wrapOpenAI(client) and wrapTypeSafe(client) helpers are also available. Each decision request emits one event. OpenAI decisions and TypeSafe System One preserve returned model identity and reported usage. Locally qualified with OpenAI 7.30.0 and @typesafe-ai/sdk 0.6.0 using mocked HTTP. Older OpenAI clients without a decisions resource still work. Decision output usage is retained even when the reviewed rate is zero; missing usage remains unknown. Decision content capture is disabled.

Microsoft Foundry decisions can be captured automatically with a wrapped fetch:

import { wrapMicrosoftDecision } from "@superpenguin/js";

// Initialize SuperPenguin normally, then configure once.
const decisionFetch = wrapMicrosoftDecision(fetch, {
  region: "eastus", deploymentType: "global",
  metadata: { feature: "routing" },
});
const response = await decisionFetch(
  `${process.env.AZURE_FOUNDRY_ENDPOINT}/providers/microsoft/v1/systemone`,
  {
    method: "POST",
    headers: {
      "api-key": process.env.AZURE_FOUNDRY_API_KEY!,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: process.env.AZURE_DECISION_DEPLOYMENT,
      state: "The parcel arrived intact.",
      questions: { intact: { type: "noul", instructions: "Is the parcel intact?" } },
    }),
  },
);
const decision = await response.json(); // Original body remains readable.

Only POSTs to the exact Microsoft decision path on *.services.ai.azure.com are captured. JSON response usage is read from a clone. The returned model wins; a JSON-string request body supplies the deployment fallback when usage is absent or the request fails. Request objects/streams are never consumed for that fallback; use the model option when a fallback is needed with those request-body forms. The wrapper does not mutate global fetch or add retries. Non-JSON responses retain unknown usage. Region and deployment type must match the reviewed serving route. Other HTTP calls pass through untouched, including Azure generation requests; continue using wrapOpenAI/wrap for their normal instrumentation. Do not also call trackDecision for the same request. Explicit reporting remains available for other HTTP stacks. No prompt, answers, headers or key are captured.

These APIs are not yet published. Production pricing/schema and provider qualification must pass the repository release runbook before publication.