Sign In

@spanlens/sdk

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@spanlens/sdk

Spanlens SDK — agent tracing, LLM usage capture, and cost observability for TypeScript.

Source
npmnpm
Version
0.2.7
Version published
Weekly downloads
44
-48.24%
Maintainers
1
Weekly downloads
 
Created
Source

@spanlens/sdk

LLM observability SDK for Spanlens. Record agent traces, LLM calls, tool invocations, and retrievals — with a single line change.

Zero-instrumentation mode — just swap your baseURL to Spanlens proxy and you get request logging + cost tracking automatically. Use this SDK when you also want agent tracing (multi-step workflows, parallel fan-out, nested spans).

💡 Next.js user? Run npx @spanlens/cli init — the wizard installs this SDK, writes your env var, and auto-rewrites new OpenAI({...}) into createOpenAI() for you (30 seconds).

Install

npm install @spanlens/sdk
# or
pnpm add @spanlens/sdk

1-line setup (v0.2.0+) ⚡

For the common case — just route your LLM calls through Spanlens for logging + cost tracking — use the pre-configured client helpers. No baseURL to remember:

// Before
import OpenAI from 'openai'
const openai = new OpenAI({
  apiKey: process.env.SPANLENS_API_KEY,
  baseURL: 'https://spanlens-server.vercel.app/proxy/openai/v1',
})

// After ⚡
import { createOpenAI } from '@spanlens/sdk/openai'
const openai = createOpenAI()  // reads SPANLENS_API_KEY + baseURL automatically

All three providers supported:

import { createOpenAI } from '@spanlens/sdk/openai'
import { createAnthropic } from '@spanlens/sdk/anthropic'
import { createGemini } from '@spanlens/sdk/gemini'

const openai    = createOpenAI()
const anthropic = createAnthropic()
const gemini    = createGemini()
// gemini.getGenerativeModel() auto-routes through Spanlens proxy

The returned clients are identical to new OpenAI(...) etc — all options (timeout, headers, organization, etc.) forward through. Peer dependencies (openai, @anthropic-ai/sdk, @google/generative-ai) are optional — install only the ones you use.

Prompt A/B tagging (v0.2.2+)

Link a call to a specific Spanlens Prompts version so it shows up in the A/B metrics table:

import { createOpenAI, withPromptVersion } from '@spanlens/sdk/openai'
const openai = createOpenAI()

const res = await openai.chat.completions.create(
  { model: 'gpt-4o-mini', messages: [...] },
  withPromptVersion('chatbot-system@3'),   // or '@latest' / raw UUID
)

Same helper on @spanlens/sdk/anthropic. For observeOpenAI/Anthropic/Gemini, pass promptVersion in options.

For multi-step agent tracing (Gantt view, parent/child spans, RAG pipelines), continue to the Quick start below.

Quick start

import { SpanlensClient, observe } from '@spanlens/sdk'

const client = new SpanlensClient({ apiKey: process.env.SPANLENS_API_KEY! })

const trace = client.startTrace({
  name: 'support_chat',
  metadata: { user_id: 'u_42', session_id: 'sess_abc' },
})

try {
  // Manual span
  const retrievalSpan = trace.span({ name: 'kb_search', spanType: 'retrieval' })
  const docs = await vectorStore.query('...')
  await retrievalSpan.end({ output: { doc_count: docs.length } })

  // Auto-end via observe helper — handles errors, always closes the span
  const answer = await observe(trace, { name: 'gpt4o_answer', spanType: 'llm' }, async (span) => {
    const res = await openai.chat.completions.create({ ... })
    span.end({
      totalTokens: res.usage!.total_tokens,
      costUsd: computeCost(res.usage!),
    })
    return res.choices[0].message.content
  })

  await trace.end({ status: 'completed' })
} catch (err) {
  await trace.end({ status: 'error', errorMessage: String(err) })
  throw err
}

API

new SpanlensClient(config)

OptionTypeDefaultDescription
apiKeystringrequired Spanlens API key (sl_live_...).
baseUrlstringhttps://spanlens-server.vercel.appAPI base URL.
timeoutMsnumber3000Request timeout for ingest calls.
silentbooleantrueSwallow network errors so instrumentation never crashes user code.
onError(err, ctx) => voidCalled on every ingest failure (even when silent).

client.startTrace({ name, metadata? })TraceHandle

Starts a new trace. Returns immediately — the backend ingest POST runs in the background.

TraceHandle

  • .traceId: string — client-generated UUID.
  • .span(options) → SpanHandle — create a root span under this trace.
  • .end({ status?, errorMessage?, metadata? }) — mark trace complete (idempotent).

SpanHandle

  • .spanId: string
  • .child(options) → SpanHandle — nested span (auto-sets parent_span_id).
  • .end({ status?, output?, errorMessage?, promptTokens?, completionTokens?, totalTokens?, costUsd?, requestId?, metadata? }) — idempotent.

spanType: 'llm' | 'tool' | 'retrieval' | 'embedding' | 'custom' (default 'custom').

observe(parent, options, fn)

Wraps an async function in a span. Auto-ends the span on success or failure (rethrows the error).

const result = await observe(traceOrSpan, { name: 'work' }, async (span) => {
  // span is open here
  return doWork()
  // span automatically closes — .end() is idempotent so you can still
  // call span.end({ totalTokens, costUsd }) inside to capture metrics.
})

Framework examples

OpenAI (auto-instrumentation)

import OpenAI from 'openai'
import { SpanlensClient, observeOpenAI } from '@spanlens/sdk'

const spanlens = new SpanlensClient({ apiKey: process.env.SPANLENS_API_KEY! })

// Route OpenAI calls through the Spanlens proxy. The SDK injects
// x-trace-id/x-span-id headers so the proxy's request log is linked
// back to your spans.
const openai = new OpenAI({
  apiKey: process.env.SPANLENS_API_KEY!,
  baseURL: 'https://spanlens-server.vercel.app/proxy/openai/v1',
})

const trace = spanlens.startTrace({ name: 'support_chat' })

const res = await observeOpenAI(trace, 'answer', (headers) =>
  openai.chat.completions.create(
    { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },
    { headers },
  ),
)

await trace.end({ status: 'completed' })

Anthropic

import Anthropic from '@anthropic-ai/sdk'
import { SpanlensClient, observeAnthropic } from '@spanlens/sdk'

const spanlens = new SpanlensClient({ apiKey: process.env.SPANLENS_API_KEY! })
const anthropic = new Anthropic({
  apiKey: process.env.SPANLENS_API_KEY!,
  baseURL: 'https://spanlens-server.vercel.app/proxy/anthropic',
})

const trace = spanlens.startTrace({ name: 'agent_run' })
const res = await observeAnthropic(trace, 'reason', (headers) =>
  anthropic.messages.create(
    { model: 'claude-haiku-4-5', max_tokens: 1024, messages: [...] },
    { headers },
  ),
)
await trace.end()

LangChain (JS)

LangChain calls go through the underlying OpenAI/Anthropic client — point that client at the Spanlens proxy and wrap the chain invocation in observe():

import { ChatOpenAI } from '@langchain/openai'
import { SpanlensClient, observe } from '@spanlens/sdk'

const spanlens = new SpanlensClient({ apiKey: process.env.SPANLENS_API_KEY! })

const llm = new ChatOpenAI({
  apiKey: process.env.SPANLENS_API_KEY!,
  configuration: {
    baseURL: 'https://spanlens-server.vercel.app/proxy/openai/v1',
  },
})

const trace = spanlens.startTrace({ name: 'langchain_qa' })

// LangChain's internal fetch won't carry our trace headers, so we group
// the whole chain under one span for dashboard visibility.
const answer = await observe(trace, { name: 'chain.invoke', spanType: 'llm' }, async () => {
  return llm.invoke('What is Spanlens?')
})

await trace.end()

LlamaIndex (TS)

import { OpenAI, VectorStoreIndex, SimpleDirectoryReader } from 'llamaindex'
import { SpanlensClient, observe } from '@spanlens/sdk'

const spanlens = new SpanlensClient({ apiKey: process.env.SPANLENS_API_KEY! })

const llm = new OpenAI({
  apiKey: process.env.SPANLENS_API_KEY!,
  additionalSessionOptions: {
    baseURL: 'https://spanlens-server.vercel.app/proxy/openai/v1',
  },
})

const trace = spanlens.startTrace({ name: 'rag_query' })

const retrieval = await observe(trace, { name: 'retrieve', spanType: 'retrieval' }, async () => {
  const docs = await new SimpleDirectoryReader().loadData({ directoryPath: './docs' })
  return VectorStoreIndex.fromDocuments(docs)
})

const answer = await observe(trace, { name: 'generate', spanType: 'llm' }, async () => {
  const engine = retrieval.asQueryEngine({ llm })
  return engine.query({ query: 'What is Spanlens?' })
})

await trace.end()

Design notes

  • Fire-and-forget ingest: startTrace() and trace.span() return synchronously. Network writes run in the background so your hot path never waits on observability.
  • Client-side UUIDs: idempotent retries are safe — same UUID twice is a no-op on the server.
  • No unhandled rejections: background POST failures are silently swallowed; use the onError hook for visibility.
  • No auto-instrumentation yet: OpenAI/Anthropic wrappers ship in v0.2 — for now, wrap LLM calls manually inside observe() (or wrap via the proxy baseURL + manual span for tracing metadata).

License

MIT

Keywords

llm

FAQs

Package last updated on 13 May 2026

Related posts