Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.tracepilotai.com/llms.txt

Use this file to discover all available pages before exploring further.

TracePilot traces the OpenAI calls that LangChain makes under the hood. You don’t wrap LangChain itself — you intercept the openai.chat.completions.create call at the point where LangChain invokes it, using a custom LangChain callback or a thin wrapper around the OpenAI client. This gives you full span visibility without touching your chain or agent logic.

How the pattern works

LangChain calls OpenAI internally. TracePilot’s wrapOpenAI intercepts that call, captures inputs, outputs, tokens, and latency as a span, then returns the original result untouched. LangChain never knows TracePilot is there.
TracePilot wraps OpenAI calls — it does not wrap LangChain itself. Your chains, agents, and tools continue to work exactly as before.

Setup

1

Install dependencies

npm install tracepilot-sdk langchain @langchain/openai openai
2

Create a traced OpenAI client

Extend the LangChain ChatOpenAI class to intercept completions calls before they reach OpenAI. Pass tp.wrapOpenAI as the call executor inside the _generate override.
import { TracePilot } from 'tracepilot-sdk';
import { ChatOpenAI } from '@langchain/openai';
import OpenAI from 'openai';
import { BaseMessage, HumanMessage } from '@langchain/core/messages';

const tp = new TracePilot(process.env.TRACEPILOT_API_KEY!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

class TracedChatOpenAI extends ChatOpenAI {
  private parentSpanId?: string;
  private stepOrder?: number;

  constructor(opts: ConstructorParameters<typeof ChatOpenAI>[0] & {
    parentSpanId?: string;
    stepOrder?: number;
  }) {
    super(opts);
    this.parentSpanId = opts.parentSpanId;
    this.stepOrder = opts.stepOrder;
  }

  async _generate(messages: BaseMessage[], options: this['ParsedCallOptions']) {
    const openAIMessages = messages.map((m) => ({
      role: m._getType() === 'human' ? 'user' : 'assistant',
      content: m.content as string,
    }));

    const { result, spanId } = await tp.wrapOpenAI(
      () =>
        openai.chat.completions.create({
          model: this.modelName,
          messages: openAIMessages,
        }),
      openAIMessages,
      this.parentSpanId,
      this.stepOrder,
    );

    // Store the spanId for downstream steps to use as parentSpanId
    (this as any)._lastSpanId = spanId;

    // Re-use the parent class to convert the OpenAI response to LangChain format
    return super._generate(messages, options);
  }
}
3

Build your chain with the traced model

Use TracedChatOpenAI anywhere you’d use ChatOpenAI. Pass stepOrder to match LangChain’s chain step numbering so spans appear in the correct order in the dashboard.
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate } from '@langchain/core/prompts';

async function runSupportChain(userQuestion: string) {
  await tp.startTrace('support-chain');

  const prompt = ChatPromptTemplate.fromMessages([
    ['system', 'You are a helpful customer support agent.'],
    ['human', '{question}'],
  ]);

  const model = new TracedChatOpenAI({
    modelName: 'gpt-4o-mini',
    stepOrder: 1,
  });

  const chain = prompt.pipe(model).pipe(new StringOutputParser());

  const answer = await chain.invoke({ question: userQuestion });

  console.log(answer);
  return answer;
}

runSupportChain('How do I export my data?');

Multi-step chains

When your chain has multiple LLM steps, pass stepOrder to each TracedChatOpenAI instance and use parentSpanId to link them into a tree.
async function runResearchChain(topic: string) {
  await tp.startTrace('research-chain');

  const messages = [{ role: 'user' as const, content: `Summarize: ${topic}` }];

  // Step 1 — Generate a search plan
  const { result: plan, spanId: planSpanId } = await tp.wrapOpenAI(
    () =>
      openai.chat.completions.create({
        model: 'gpt-4o',
        messages,
      }),
    messages,
    undefined, // no parent — this is the root span
    1,
  );

  const followUp = [
    ...messages,
    { role: 'assistant' as const, content: plan.choices[0].message.content! },
    { role: 'user' as const, content: 'Now write a detailed report based on that plan.' },
  ];

  // Step 2 — Write the report (child of Step 1)
  const { result: report } = await tp.wrapOpenAI(
    () =>
      openai.chat.completions.create({
        model: 'gpt-4o',
        messages: followUp,
      }),
    followUp,
    planSpanId, // link to the planning span
    2,
  );

  return report.choices[0].message.content;
}
Set stepOrder to match LangChain’s chain step index. This keeps spans in the correct sequence in the dashboard, making it easy to spot which step produced a bad output.

What you’ll see in the dashboard

Each wrapOpenAI call creates one span. For a two-step chain, you get a parent span for Step 1 and a child span for Step 2 nested beneath it. Click any span to inspect the exact messages sent to OpenAI, the response, token usage, latency, and cost.