Migrate from LangSmith · 2026
LangSmith is tightly bound to the LangChain ecosystem. Spanlens speaks the same graph-aware tracing shape but is provider-neutral: you can run it on plain OpenAI calls, LangChain, LangGraph, Vercel AI SDK, or raw HTTP from any language. This page shows how to swap the two without rewriting your chains.
Why teams switch
- No framework lock-in. The Spanlens proxy works whether or not you use LangChain. Most teams end up with a mix of LangChain chains and direct OpenAI calls; LangSmith only sees the LangChain half by default.
- Drop-in proxy adds zero-instrumentation paths. Background workers, third-party libraries, MCP servers all get logged automatically once they share the SDK or base URL. With LangSmith you have to thread
@traceablethrough everything. - Cost and token tracking on every call. Spanlens computes cost from a versioned price table and stores cost_usd directly on the request row, no downstream aggregation step needed.
- MIT self-host. One Docker compose, your Supabase + ClickHouse.
Step 1. Install
pnpm add @spanlens/sdktsStep 2. Swap LangChain / LangGraph tracing
If you use LangChain or LangGraph today, this is the smallest change. Replace the ambient LANGSMITH_TRACING=true env-var setup with an explicit Spanlens callback handler.
Before (LangSmith, env-var driven):
# .env
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=ls__xxx
LANGSMITH_PROJECT=my-app
LANGSMITH_ENDPOINT=https://api.smith.langchain.combash// chains.ts
import { ChatOpenAI } from '@langchain/openai'
const llm = new ChatOpenAI({ model: 'gpt-4o-mini' })
// LangSmith picks up every call automatically via the env vars above.tsAfter (Spanlens callback handler):
# .env
SPANLENS_API_KEY=sl_live_xxxbash// chains.ts
import { ChatOpenAI } from '@langchain/openai'
import { SpanlensClient } from '@spanlens/sdk'
import { createSpanlensCallbackHandler } from '@spanlens/sdk/langchain'
const client = new SpanlensClient()
const handler = createSpanlensCallbackHandler({ client })
const llm = new ChatOpenAI({ model: 'gpt-4o-mini' })
// Attach the handler at invocation time:
const res = await chain.invoke({ input }, { callbacks: [handler] })
// Or for LangGraph:
const graph = workflow.compile()
const res = await graph.invoke({ input }, { callbacks: [handler] })tsThe handler captures LLM, chain (LangGraph node), tool, and retriever spans. TherunId / parentRunId pair LangChain gives every callback becomes the Spanlens span tree, so the graph topology is preserved.
Detailed walkthrough including LangGraph specifics on /docs/integrations/langgraph.
Step 3. Replace the traceable decorator / wrapper
For non-LangChain code, LangSmith uses @traceable (Python) or traceable() (JS) to wrap arbitrary functions into runs. The Spanlens equivalent is observe().
Before (LangSmith):
import { traceable } from 'langsmith/traceable'
const answer = traceable(
async (input: string) => {
const docs = await retrieve(input)
const resp = await openai.chat.completions.create({...})
return resp.choices[0].message.content
},
{ name: 'answer-question' },
)tsAfter (Spanlens):
import { SpanlensClient } from '@spanlens/sdk'
import { observe } from '@spanlens/sdk'
const client = new SpanlensClient()
async function answer(input: string) {
const trace = client.startTrace({ name: 'answer-question' })
try {
const docs = await observe(trace, { name: 'retrieve' }, () => retrieve(input))
const resp = await observe(trace, { name: 'generate', spanType: 'llm' }, () =>
openai.chat.completions.create({...})
)
return resp.choices[0].message.content
} finally {
await trace.end()
}
}tsNested observe() calls under the same parent automatically chain into a span tree. If you want the function-decorator ergonomics, wrap once at the entry point of your route handler and call ordinary functions inside; Spanlens does not require every leaf to be decorated.
Step 4. Environment variables
| LangSmith | Spanlens | Notes |
|---|---|---|
LANGSMITH_API_KEY | SPANLENS_API_KEY | Project-scoped, format sl_live_*. |
LANGSMITH_PROJECT | set at key creation | The Spanlens key is bound to a project; no per-call header needed. |
LANGSMITH_TRACING=true | not needed | Tracing is on whenever the SDK / callback handler is wired up. |
LANGSMITH_ENDPOINT | SDK baseURL option | Self-hosting? Pass baseURL to SpanlensClient. |
Step 5. Data model mapping
| LangSmith | Spanlens | Notes |
|---|---|---|
| Run (llm / chain / tool / retriever) | Span (span_type: llm / custom / tool / retrieval) | LangChain's "chain" runs map to Spanlens custom spans; LangGraph nodes map the same way. |
| Trace (collection of runs) | Trace | 1:1. Spanlens trace has span_count, total_tokens, total_cost_usd aggregated via DB trigger. |
| Project | Project | 1:1. Spanlens projects also scope API keys and provider keys. |
| Thread (session_id grouping) | x-spanlens-session header | No separate Thread table; filter /requests by session_id. |
| Dataset / Example | Dataset / Dataset Item | 1:1. Items accept either variable maps or raw chat messages. |
| Feedback (score / tag) | Eval result + human annotation | Spanlens stores LLM-judge scores in eval_results and human ratings in a separate human_evals table. |
| Annotation queue | Annotation | Sample N requests, score them in a queue UI; results land in human_evals. |
Step 6. Run both side-by-side during the cutover
LangSmith and Spanlens do not conflict. Keep LangSmith env vars in place while you add the Spanlens callback handler; you will see both dashboards populated.
// Both active for one chain
const langsmithEnabled = process.env.LANGSMITH_TRACING === 'true'
const handlers = langsmithEnabled
? [createSpanlensCallbackHandler({ client })] // LangSmith auto-attaches
: [createSpanlensCallbackHandler({ client })]
await chain.invoke({ input }, { callbacks: handlers })tsWhen the Spanlens dashboard matches LangSmith for a representative slice of traffic, drop LANGSMITH_TRACING, remove the LangSmith env vars, uninstall langsmith.
What does not migrate 1:1
- LangSmith Hub (public prompt library). Spanlens does not have a public hub. Your private prompt versions live in /prompts and are referenced by name@version through
x-spanlens-prompt-version. - RunTree low-level API. Spanlens equivalent is the
SpanlensClient.startTrace()/trace.span()/span.child()chain. Same shape, different names. - 400-day retention. Spanlens retention depends on plan: 14 days (Free), 90 days (Pro), 365 days (Team). Self-hosting removes the cap.
- LangChain auto-instrumentation across language boundaries. Within one Node process the callback handler covers LangChain JS and LangGraph JS. For Python LangChain, use the Python SDK.
Verify the cutover
- Invoke a traced chain once.
- Open /traces. The span tree should mirror what LangSmith showed, including LangGraph node hierarchy.
- Open the LLM call detail. Token counts and cost should match the LangSmith run within 1%.
- If you used datasets / eval feedback in LangSmith, re-create the dataset in /datasets and re-run the evaluator in /evals against the same dataset items.
Next: LangGraph integration for graph topology details, or data model for the full schema.