Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.inference.net/llms.txt

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

Catalyst instruments @mariozechner/pi-ai in TypeScript. PI AI is a unified LLM API and provider registry. Catalyst patches the registered PI AI providers so each model turn through stream, streamSimple, complete, or completeSimple emits an OpenInference LLM span. Use this guide for Node applications that use PI AI for model calls, tool calling, cross-provider handoffs, or agent workflows. PI AI is a TypeScript package, so there is no Python equivalent for this integration.

What Is Captured

  • One LLM span per PI AI model turn, named like pi-ai.<provider>.turn
  • stream and streamSimple calls, plus complete and completeSimple calls because PI AI completes by resolving the underlying stream
  • System prompts, input messages, assistant output, model name, provider, and invocation parameters
  • Tool call IDs, names, and JSON arguments from assistant messages
  • Token usage, prompt cache read/write counts, and total cost when PI AI returns them
  • Finish reason, errors, aborts, and exception details
  • Active agentSpan() identity, including agent.id, agent.name, agent.role, and session.id

Install

TypeScript
bun add @inference/tracing @mariozechner/pi-ai

Configure Export

Set the Catalyst endpoint and token before your app starts. Generate a token at API Keys.
export CATALYST_OTLP_ENDPOINT="https://telemetry.inference.net"
export CATALYST_OTLP_TOKEN="<your-token>"
export CATALYST_SERVICE_NAME="pi-ai-agent"

export OPENAI_API_KEY="<your-openai-api-key>"

Initialize Tracing

Initialize Catalyst before making PI AI calls. Import the PI AI namespace and pass it to setup() so Catalyst patches the same provider registry your app uses.
TypeScript
import * as piAi from "@mariozechner/pi-ai";
import { setup } from "@inference/tracing";

const tracing = await setup({
  serviceName: process.env.CATALYST_SERVICE_NAME ?? "pi-ai-agent",
  modules: { piAi },
});
For explicit manual initialization, disable auto-instrumentation and use the PI AI subpath helper.
TypeScript
import * as piAi from "@mariozechner/pi-ai";
import { setup } from "@inference/tracing";
import { instrumentPiAi } from "@inference/tracing/pi-ai";

const tracing = await setup({ autoInstrument: false });
instrumentPiAi(piAi, tracing);

Complete Call

completeSimple() is the smallest path. PI AI resolves it through a provider stream, so Catalyst emits the LLM span when the call finishes. The examples below assume you initialized tracing with the setup block above.
TypeScript
import { completeSimple, getModel, type Context } from "@mariozechner/pi-ai";

const model = getModel("openai", "gpt-4o-mini");
const context: Context = {
  systemPrompt: "You answer in one concise sentence.",
  messages: [{ role: "user", content: "Why are trace trees useful?" }],
};

const response = await completeSimple(model, context);
console.log(response.content);
Expected span:
  • pi-ai.openai.turn
Expected promoted fields include llm.model_name, input.value, output.value, llm.token_count.prompt, llm.token_count.completion, llm.token_count.total, and llm.invocation_parameters when PI AI returns the corresponding data.

Streaming

For streaming calls, consume the stream and call await stream.result() before process shutdown. Catalyst finishes the span when PI AI emits a done/error event or when .result() resolves.
TypeScript
import { getModel, streamSimple, type Context } from "@mariozechner/pi-ai";

const model = getModel("openai", "gpt-4o-mini");
const context: Context = {
  messages: [{ role: "user", content: "Stream a sentence about observability." }],
};

const stream = streamSimple(model, context);

for await (const event of stream) {
  if (event.type === "text_delta") {
    process.stdout.write(event.delta);
  }
}

const finalMessage = await stream.result();
console.log("\nFinished:", finalMessage.stopReason);
For short-lived scripts, call await tracing.shutdown() after the stream is fully consumed.

Tool Loop

PI AI returns tool calls in assistant message content. Catalyst records the tool call name, ID, and arguments on the PI AI LLM span. Your local tool execution is application code, so wrap it with manualSpan() if you also want a TOOL child span for the work your app performs.
TypeScript
import {
  Type,
  completeSimple,
  getModel,
  type Context,
  type Tool,
} from "@mariozechner/pi-ai";
import { manualSpan, SpanKindValues } from "@inference/tracing";

const model = getModel("openai", "gpt-4o-mini");

const tools: Tool[] = [
  {
    name: "lookup_order",
    description: "Look up an order by ID.",
    parameters: Type.Object({
      orderId: Type.String({ description: "Customer order ID." }),
    }),
  },
];

const context: Context = {
  systemPrompt: "Use tools when you need order data.",
  messages: [{ role: "user", content: "Where is order ABC-123?" }],
  tools,
};

const firstTurn = await completeSimple(model, context);
context.messages.push(firstTurn);

for (const block of firstTurn.content) {
  if (block.type !== "toolCall" || block.name !== "lookup_order") continue;
  const args = block.arguments as { orderId: string };

  const order = await manualSpan(
    tracing.tracer,
    {
      spanName: "lookup_order",
      spanKind: SpanKindValues.TOOL,
      toolName: block.name,
      toolCallId: block.id,
      input: args,
    },
    async (span) => {
      const result = {
        orderId: args.orderId,
        status: "shipped",
        eta: "Friday",
      };
      span.setOutput(result);
      return result;
    },
  );

  context.messages.push({
    role: "toolResult",
    toolCallId: block.id,
    toolName: block.name,
    content: [{ type: "text", text: JSON.stringify(order) }],
    isError: false,
    timestamp: Date.now(),
  });
}

const finalTurn = await completeSimple(model, context);
console.log(finalTurn.content);
Expected spans:
  • pi-ai.openai.turn for the first model turn with the tool call
  • lookup_order TOOL span when you wrap local execution with manualSpan()
  • another pi-ai.openai.turn for the continuation after the tool result

Stable Agent Identity

Wrap the full PI AI run in agentSpan() when one user request can produce multiple model turns or tool executions. PI AI LLM spans inherit the active agent identity and nest under the AGENT span.
TypeScript
import { agentSpan } from "@inference/tracing";
import { completeSimple, getModel, type Context } from "@mariozechner/pi-ai";

const model = getModel("openai", "gpt-4o-mini");

await agentSpan(
  tracing.tracer,
  {
    agentId: "pi-ai-support-agent",
    agentName: "PI AI Support Agent",
    spanName: "pi-ai-support-agent.run",
    sessionId: "conversation-order-abc-123",
    role: "support",
    system: "pi-ai",
  },
  async (span) => {
    const input = "Summarize order ABC-123 for the customer.";
    const context: Context = {
      messages: [{ role: "user", content: input }],
    };

    span.setInput(input);
    const response = await completeSimple(model, context);
    span.setOutput(response.content);
  },
);

Verify In Catalyst

Filter traces by your service.name, for example pi-ai-agent. A successful run should show PI AI LLM spans named by provider, such as pi-ai.openai.turn, with captured input/output, model metadata, usage, finish reason, and tool call attributes. If you wrap the run in agentSpan(), the same LLM spans should show the inherited agent.id, agent.name, and agent.role. If no PI AI spans appear:
  • Initialize Catalyst before making PI AI calls.
  • Pass the PI AI namespace directly with modules: { piAi } or instrumentPiAi(piAi, tracing).
  • Consume streaming results or call .result() so PI AI can finish the turn.
  • Call await tracing.shutdown() before process exit in short-lived scripts.