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

@ctx0/sdk

v0.3.1

Published

Context0 LLM observability SDK for TypeScript/Node.js

Downloads

59

Readme

@ctx0/sdk

Drop-in LLM observability for TypeScript/Node.js. One import change, all calls logged automatically.

Install

npm install @ctx0/sdk

Usage

import { configure, OpenAI } from "@ctx0/sdk";

configure({ apiKey: "your-api-key" });

const client = new OpenAI({ apiKey: "sk-..." });
const response = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello" }],
});

Supports OpenAI, AzureOpenAI, and Anthropic.

You can also set the LLM_OBSERVATORY_API_KEY environment variable instead of passing apiKey to configure() — the SDK picks it up automatically.

Langfuse integration

Already using Langfuse? wrapLangfuse() captures all Langfuse traces, generations, and spans — including metadata like userId, sessionId, and tags — and forwards them to Context0 automatically. Zero code changes to your existing Langfuse setup.

Requires Langfuse TS SDK v4+ (@langfuse/tracing, @langfuse/otel).

Setup

import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";
import { configure, wrapLangfuse } from "@ctx0/sdk";

// 1. Configure Context0
configure({ apiKey: "your-api-key" });

// 2. Initialize Langfuse with OTel (standard Langfuse v4 setup)
const sdk = new NodeSDK({
  spanProcessors: [
    new LangfuseSpanProcessor({
      publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
      secretKey: process.env.LANGFUSE_SECRET_KEY!,
    }),
  ],
});
sdk.start();

// 3. Call once — adds Context0 processor to the TracerProvider
wrapLangfuse();

What gets captured

After calling wrapLangfuse(), every Langfuse observation is automatically forwarded to Context0:

import { startActiveObservation } from "@langfuse/tracing";
import OpenAI from "openai";

const client = new OpenAI({ apiKey: "sk-..." });

// Per-request metadata (userId, sessionId, tags) — captured automatically
const reply = await startActiveObservation(
  {
    name: "user-question",
    userId: "alice",
    sessionId: "sess-1",
    tags: ["production", "chat"],
    asType: "generation",
    model: "gpt-4o",
  },
  async (span) => {
    const messages = [{ role: "user" as const, content: "What is observability?" }];
    span.update({ input: messages });

    const response = await client.chat.completions.create({
      model: "gpt-4o",
      messages,
    });

    const content = response.choices[0].message.content ?? "";
    span.update({
      output: { role: "assistant", content },
      usageDetails: {
        input: response.usage?.prompt_tokens ?? 0,
        output: response.usage?.completion_tokens ?? 0,
      },
    });
    return content;
  },
);

// Multi-step traces (RAG pipeline) — captured automatically
await startActiveObservation(
  { name: "rag-pipeline", userId: "alice", sessionId: "sess-1" },
  async () => {
    // Nested spans create parent-child relationships in Context0
    const docs = await startActiveObservation(
      { name: "retrieval", asType: "span" },
      async (span) => {
        const results = await vectorDb.search(query);
        span.update({ input: query, output: results });
        return results;
      },
    );

    return await startActiveObservation(
      { name: "completion", asType: "generation", model: "gpt-4o" },
      async (span) => {
        // LLM call — input, output, usage all captured
        span.update({ input: messages });
        const response = await client.chat.completions.create({ model: "gpt-4o", messages });
        span.update({ output: response.choices[0].message, usageDetails: { ... } });
        return response.choices[0].message.content;
      },
    );
  },
);

Works with startActiveObservation(), observe(), and all Langfuse v4 patterns.

wrap() — per-client alternative

If you only want to observe a specific client instance (rather than all Langfuse observations globally), use wrap() instead:

import { observeOpenAI } from "langfuse";
import { configure, wrap } from "@ctx0/sdk";

configure({ apiKey: "your-api-key" });

const client = wrap(observeOpenAI(new OpenAI({ apiKey: "sk-..." })));

// Both Langfuse AND Context0 capture this call
const response = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello" }],
});

wrap() works with any client that has a .chat (OpenAI-shaped) or .messages (Anthropic-shaped) interface. If the client is already a Context0 wrapper, it's returned as-is — no double-wrapping.

When to use which: Use wrapLangfuse() to capture everything Langfuse sees globally (recommended). Use wrap() if you only want Context0 on specific client instances.

Tracing

Group related LLM calls into traces to see your full pipeline as a tree.

observe() wrapper

Automatically creates a trace (top-level) or span (nested). Captures function args as input and return value as output.

import { configure, OpenAI, observe } from "@ctx0/sdk";

const client = new OpenAI();

const retrieveDocs = observe(async function retrieveDocs(query: string) {
  return vectorDb.search(query);
});

const ragPipeline = observe(async function ragPipeline(question: string) {
  const docs = await retrieveDocs(question);  // child span
  const response = await client.chat.completions.create({  // auto-captured as generation
    model: "gpt-4o",
    messages: [{ role: "user", content: `${docs}\n\n${question}` }],
  });
  return response.choices[0].message.content;
});

Options:

  • observe({ name: "custom-name" }, fn) — override the span name (default: function name)
  • observe({ captureInput: false }, fn) — don't log args (for sensitive data)
  • observe({ captureOutput: false }, fn) — don't log return value

Works with both sync and async functions.

Inline tracing

Use trace() and span() for inline code blocks that aren't standalone functions:

import { trace, span } from "@ctx0/sdk";

trace("rag-pipeline", { input: { query: q } }, (t) => {
  span("vector-search", (s) => {
    const results = search(q);
    s.setOutput(results);
  });

  const response = await client.chat.completions.create({
    model: "gpt-4o",
    messages: [...],
  });
  t.setOutput(response.choices[0].message.content);
});

Mix freely — observe() and span() compose within the same trace.

Cross-service propagation

Pass trace context across services (e.g., API to SQS worker) via W3C traceparent:

import { trace, getCurrentTraceparent, emitCompletedSpan } from "@ctx0/sdk";

// Producer — get traceparent inside a trace
trace("api-request", () => {
  const traceparent = getCurrentTraceparent();
  sqs.sendMessage({ Body: JSON.stringify({ traceparent, ... }) });
});

// Consumer — restore context
const msg = JSON.parse(sqsMessage.Body);
trace("worker", { traceparent: msg.traceparent }, () => {
  await client.chat.completions.create(...);  // appears as child in same trace
});

Use emitCompletedSpan() to backfill spans with explicit start/end times (e.g., queue wait duration).

Serverless / short-lived environments

In AWS Lambda, Convex, Vercel Functions, or any environment where the process freezes or terminates after your handler returns, buffered events may be lost before the background flush fires.

Option 1 — immediate mode (recommended): Send each event as it happens, no batching:

configure({
  apiKey: "your-api-key",
  flushMode: "immediate",
});

Option 2 — explicit flush: Keep the default batch mode but flush before returning:

import { configure, OpenAI, flush } from "@ctx0/sdk";

configure({ apiKey: "your-api-key" });
const client = new OpenAI({ apiKey: "sk-..." });

export async function handler(event: any) {
  const response = await client.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: event.prompt }],
  });

  await flush(); // wait for all buffered events to send before the process freezes

  return { statusCode: 200, body: response.choices[0].message.content };
}

flushMode: "immediate" has slightly higher per-request overhead but guarantees no events are lost. Use it when you can't control when the process shuts down.

Using with Langfuse in serverless

If you're using wrapLangfuse(), Langfuse also buffers spans and needs flushing. The OTel SDK's shutdown() flushes all registered span processors — both Langfuse and Context0 — in one call:

import { NodeSDK } from "@opentelemetry/sdk-node";

// At the end of your handler:
await sdk.shutdown(); // flushes both Langfuse and Context0

If you're using flushMode: "immediate", Context0 events are sent immediately and don't need a separate flush — but you still need sdk.shutdown() for Langfuse.