Skip to main content
Use @uselemma/tracing with the Anthropic Node SDK to get a top-level run trace from agent plus a child span for every messages.create call — with prompts, completions, model, token usage, and timing.

How It Works

registerOTel() sets up the OTel transport. The AnthropicInstrumentation from OpenInference patches the anthropic package so every API call emits a child span inside whatever span is currently active — including inside agent.

Getting Started

Install

npm install @uselemma/tracing anthropic @opentelemetry/instrumentation @arizeai/openinference-instrumentation-anthropic

Register at startup

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { registerOTel } = await import("@uselemma/tracing");
    const { registerInstrumentations } = await import("@opentelemetry/instrumentation");
    const { AnthropicInstrumentation } = await import(
      "@arizeai/openinference-instrumentation-anthropic"
    );

    const provider = registerOTel();
    registerInstrumentations({
      instrumentations: [new AnthropicInstrumentation()],
      tracerProvider: provider,
    });
  }
}
Set LEMMA_API_KEY and LEMMA_PROJECT_ID environment variables. Find them in your Lemma project settings.

Example

Single-turn completion

import { agent } from "@uselemma/tracing";
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const supportAgent = agent("support-agent", async (input: { message: string }) => {
  const response = await anthropic.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: input.message }],
  });
  return response.content[0].type === "text" ? response.content[0].text : "";
});

const { result, runId } = await supportAgent({ message: "Explain async/await in one sentence." });

Tool-calling agent

import { agent, tool } from "@uselemma/tracing";
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const getWeather = tool("get-weather", async (city: string) => {
  return { city, temperature: "72°F", condition: "sunny" };
});

const weatherAgent = agent("weather-agent", async (input: { message: string }, ctx) => {
  const toolSpec: Anthropic.Tool[] = [
    {
      name: "get_weather",
      description: "Get current weather for a city",
      input_schema: {
        type: "object",
        properties: { city: { type: "string" } },
        required: ["city"],
      },
    },
  ];

  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: input.message },
  ];

  let response = await anthropic.messages.create({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    tools: toolSpec,
    messages,
  });

  while (response.stop_reason === "tool_use") {
    const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
    messages.push({ role: "assistant", content: response.content });

    const toolResults: Anthropic.ToolResultBlockParam[] = [];
    for (const block of toolUseBlocks) {
      if (block.type !== "tool_use") continue;
      const weatherData = await getWeather(block.input.city as string);
      toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(weatherData) });
    }

    messages.push({ role: "user", content: toolResults });
    response = await anthropic.messages.create({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      tools: toolSpec,
      messages,
    });
  }

  const textBlock = response.content.find((b) => b.type === "text");
  const text = textBlock?.type === "text" ? textBlock.text : "";
  return text;
});

const { result, runId } = await weatherAgent({ message: "What's the weather in Paris?" });

Streaming with ctx.complete()

For streaming, use messages.create({ stream: true }) rather than the higher-level messages.stream() helper — the latter can break with AnthropicInstrumentation in some versions. Consume the async iterable inside the wrapper and call ctx.complete() once the full text is assembled:
import { agent } from "@uselemma/tracing";
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const streamingAgent = agent("streaming-agent", async (input: string, ctx) => {
  let fullText = "";

  const stream = await anthropic.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 512,
    messages: [{ role: "user", content: input }],
    stream: true,
  });

  for await (const event of stream) {
    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      fullText += event.delta.text;
    }
  }

  ctx.complete(fullText); // store assembled text as output, close span
  return fullText;
}, { streaming: true });

What You’ll See in Lemma

SpanSourceContains
ai.agent.runagentFull run input, output, timing, run ID
gen_ai.chatOpenInferenceModel name, prompt messages, completion, token usage
tool.get-weathertool() helperTool input and return value

Next Steps