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

@scopecall/scopecall-js

v0.3.0

Published

Official TypeScript/Node.js SDK for ScopeCall AI observability

Readme

@scopecall/scopecall-js

TypeScript SDK for ScopeCall — open-source, self-hostable AI observability.

npm License


Install

npm install @scopecall/scopecall-js
# or
pnpm add @scopecall/scopecall-js
# or
yarn add @scopecall/scopecall-js

Quick start

import { init } from "@scopecall/scopecall-js";
import OpenAI from "openai";

// Initialize once at app startup. `init()` returns the SDK instance.
const sdk = init({
  apiKey: "sc_live_xxx",                       // from your ScopeCall dashboard
  endpoint: "http://localhost:8080/v1/ingest", // required: your self-hosted ingest URL
});

// Instrument your OpenAI client in place — all chat.completions.create
// calls through this instance are traced automatically.
const openai = new OpenAI();
sdk.instrument(openai);

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

// Traces appear in your dashboard within seconds

Configuration

const sdk = init({
  apiKey: "sc_live_xxx",                       // required (or set `output`/`debug`/custom `transport`)
  endpoint: "http://localhost:8080/v1/ingest", // required when using apiKey — point at your self-hosted ingest
  environment: "production",                   // optional; defaults to "production"
  captureContent: true,                        // optional; record prompts/completions (default true)
  redact: true,                                // optional; PII redaction (default true)
  batchSize: 50,                               // optional; events per HTTP batch
  maxRetries: 3,                               // optional; retry attempts on transient failure
  debug: false,                                // optional; route events to console instead of HTTP
});

No hosted-cloud default yet. A managed default endpoint will return when ScopeCall Cloud is live. Until then, init() throws ConfigError if apiKey is set without endpoint — fail-fast is safer than silently sending events to a domain that doesn't exist.


Cost attribution hierarchy (v0.3)

sdk.workflow() / sdk.agent() / sdk.step() are three nested async-callback helpers that mark different levels of the cost- attribution hierarchy. Each emits a distinct container span on exit (kind='workflow' / 'agent' / 'step'), and the ScopeCall dashboard rolls up cost from LLM calls to whichever ancestor you wrapped:

await sdk.workflow(
  "support-refund",
  async () => {
    await sdk.agent("policy_check", async () => {
      await sdk.step("lookup_policy", async () => {
        await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [...] });
      });
      await sdk.step("verify_eligibility", async () => {
        await openai.chat.completions.create({ model: "gpt-4o", messages: [...] });
      });
    });
    await sdk.agent("refund_executor", async () => {
      await sdk.step("draft_response", async () => {
        await openai.chat.completions.create({ model: "claude-3-5-sonnet", messages: [...] });
      });
    });
  },
  { customerId: "customer_acme" },          // v0.3: B2B tenant attribution
);

The dashboard's Workflow Treemap shows aggregate cost per workflow. Drilling into a workflow surfaces a per-agent and per-step breakdown so you can see, e.g., "76% of support-refund cost is in refund_executor.draft_response".

Nesting is voluntary, not enforced — wrap only the levels you want to attribute. A bare sdk.workflow() with no agent/step is fine; cost rolls up to the workflow. Agent without step is fine too (the by-agent breakdown handles both workflow → agent → step → llm and direct workflow → agent → llm shapes).

sdk.trace(name, fn, opts?) remains supported as a backward-compatible alias for sdk.workflow() — every example in this README that uses sdk.trace(...) works identically. Use sdk.workflow() in new code so the cost-attribution intent reads clearly at the call site.


Optional metadata per call

Attribute calls to a feature, user, session, or customer via the workflow() / agent() / step() helpers. Any LLM calls made inside the callback inherit this context.

await sdk.workflow(
  "customer-support",
  async () => {
    return openai.chat.completions.create({
      model: "gpt-4o",
      messages: [...],
    });
  },
  { customerId: "customer_acme" },          // v0.3: B2B tenant attribution
);

For SDK-wide defaults (applied to every call), set them on init():

const sdk = init({
  apiKey: "sc_live_xxx",
  endpoint: "http://localhost:8080/v1/ingest",
  defaultFeature: "customer-support",
  defaultUserId: "user_123",
  defaultSessionId: "session_abc",
  test: false,                              // v0.3: tag every event is_test=true; also via SCOPECALL_TEST=1
});

test: true (or SCOPECALL_TEST=1 env var) tags every event with is_test=true. The dashboard excludes test-tagged calls from production cost reports — useful for vitest/jest runners, CI eval suites, and replay tooling that shouldn't pollute the production cost dashboard.


Tracking prompt versions

Tag a trace with promptVersion so the Prompts page can show cost / latency / error-rate per iteration. When you ship a new prompt, bump the version and see the deltas immediately:

await sdk.trace("billing-agent", async () => {
  return openai.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "system", content: PROMPT_V3 }, ...],
  });
}, { promptVersion: "v3" });

Nested traces inherit the parent's promptVersion. To clear it on a child span, pass { promptVersion: null }.

For apps where every call belongs to one version (e.g. tagged by deploy/SHA), set the SDK-wide default and skip the per-trace arg:

const sdk = init({
  apiKey: "sc_live_xxx",
  endpoint: "http://localhost:8080/v1/ingest",
  defaultPromptVersion: process.env.RELEASE_SHA, // tag every call
});

Precedence: trace() opts → parent trace's value → defaultPromptVersion → null.


Streaming

Streaming chat.completions.create({ stream: true, ... }) is captured the same way as non-streaming calls. TTFT (time to first token) is recorded automatically, content is aggregated across chunks, and one event is emitted when the stream completes (or is aborted by the consumer).

const stream = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "tell me a story" }],
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
// One trace appears with: ttft_ms, latency_ms, output_text, input/output_tokens

The SDK automatically adds stream_options.include_usage: true to your request so token counts arrive in the final chunk. If you explicitly set include_usage: false, that choice is honored and token counts will be 0.


What gets captured

Every chat.completions.create call captures:

| Field | Description | |-------|-------------| | model | Canonical model name (e.g. gpt-4o, claude-3-5-sonnet) | | provider | Detected provider (openai | anthropic) | | input_tokens | Prompt token count | | output_tokens | Completion token count | | cost_usd | Calculated cost in USD | | latency_ms | End-to-end latency | | ttft_ms | Time to first token (streaming) | | finish_reason | stop, length, tool_calls, etc. | | cache_read_tokens | OpenAI prompt cache hits | | status | success or error | | error_message | Error detail on failure | | input_text | Full prompt (redacted per your PII config) | | output_text | Full completion | | prompt_version | Prompt iteration label from trace() or config — powers the Prompts page |


Providers

| Provider | Status | |----------|--------| | OpenAI | ✅ since v0.1.0 — chat.completions.create (streaming + non-streaming) | | Anthropic | ✅ since v0.1.1 — messages.create (streaming + non-streaming) | | Vercel AI SDK | ✅ since v0.1.1 — generateText, streamText, generateObject, streamObject (every entry point that hits doGenerate / doStream) | | Google Gemini | 🔜 v0.3.1 | | LangChain (via manual sdk.trace() today; native bridge planned) | 🔜 v0.5.0 | | LlamaIndex (via manual sdk.trace() today; native bridge planned) | 🔜 v0.5.0 | | CrewAI / AutoGen / DSPy (via the OpenTelemetry GenAI bridge) | 🔜 v0.4.x |

OpenAI, Anthropic, and Vercel AI SDK instrumentation continue to work in v0.3.0; the release added the cost-attribution hierarchy (sdk.workflow() / sdk.agent() / sdk.step()) and customerId / test config on top of the existing provider instrumentation.

Anthropic usage

import { init } from "@scopecall/scopecall-js";
import Anthropic from "@anthropic-ai/sdk";

const sdk = init({
  apiKey: "sc_live_xxx",
  endpoint: "http://localhost:8080/v1/ingest",
});
const anthropic = new Anthropic();
sdk.instrument(anthropic, "anthropic");

const msg = await anthropic.messages.create({
  model: "claude-3-5-sonnet-20241022",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello" }],
});

Streaming works the same way — pass stream: true and iterate the result. TTFT and content are captured automatically; final token counts come from the message_delta event Anthropic emits near end-of-stream.

Vercel AI SDK usage

Instrument the model object that providers like @ai-sdk/openai return. All higher-level entry points (generateText, streamText, generateObject, streamObject) bottom out in this model's doGenerate / doStream, so a single instrument call captures everything:

import { init } from "@scopecall/scopecall-js";
import { generateText, streamText } from "ai";
import { openai } from "@ai-sdk/openai";

const sdk = init({
  apiKey: "sc_live_xxx",
  endpoint: "http://localhost:8080/v1/ingest",
});
const model = openai("gpt-4o");
sdk.instrument(model, "vercel-ai");

// Non-streaming
const { text } = await generateText({
  model,
  prompt: "Hello",
});

// Streaming
const result = streamText({ model, prompt: "Tell me a story" });
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

Works the same with @ai-sdk/anthropic, @ai-sdk/google, etc. — the bridge reads the provider field off the model object so the trace's provider field is set correctly per call.


Self-hosted setup

See the main repo README for the full Docker Compose quickstart.


License

Apache 2.0. Free for any use — self-hosted, modified, redistributed, or built on top of for commercial products.