> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zespan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK — @zespan/sdk

> Install and configure the Zespan TypeScript SDK to trace OpenAI, Anthropic, Google, Bedrock, Mistral, Groq, and LiteLLM calls, manage agent workflows, and flush events in serverless environments.

The TypeScript SDK wraps your existing LLM client instances and automatically captures latency, token counts, costs, and errors for every call — with no changes to your application logic beyond initialization.

## Installation

<Steps>
  <Step title="Install the package">
    ```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    npm install @zespan/sdk
    ```

    All provider integrations are optional peer dependencies. Install only what you use:

    ```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    npm install openai                    # OpenAI
    npm install @anthropic-ai/sdk         # Anthropic
    npm install @google/generative-ai     # Google Generative AI
    npm install @aws-sdk/client-bedrock-runtime  # AWS Bedrock
    npm install @mistralai/mistralai      # Mistral
    npm install groq-sdk                  # Groq
    npm install @langchain/core           # LangChain
    ```
  </Step>

  <Step title="Initialize Zespan">
    Call `zespan.init()` once at application startup, before any LLM calls are made.

    ```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    import { zespan } from "@zespan/sdk";

    zespan.init({
      apiKey: process.env.ZESPAN_API_KEY!,
      environment: "production",
    });
    ```
  </Step>

  <Step title="Wrap your LLM client">
    Pass your client instance through the appropriate wrapper function. The wrapper returns the same client — your existing code continues to work unchanged.

    ```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    import OpenAI from "openai";
    import { zespan } from "@zespan/sdk";

    const openai = zespan.wrapOpenAI(new OpenAI());

    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: "Hello!" }],
    });
    ```
  </Step>
</Steps>

## Init options

`zespan.init(options)` accepts the following configuration:

<ParamField body="apiKey" type="string" required>
  Your Zespan API key. Must start with `zsp_`. Find this in your project settings.
</ParamField>

<ParamField body="environment" type="string" default="production">
  Environment label attached to every event. Use `"staging"` or `"development"` to separate traces by environment.
</ParamField>

<ParamField body="storePrompts" type="boolean" default="true">
  When `true` (default), prompt and completion text are stored alongside traces with PII redaction applied before transmission. Set to `false` to disable prompt storage entirely.
</ParamField>

<ParamField body="sampleRate" type="number" default="1.0">
  Fraction of events to send, between `0.0` and `1.0`. Set to `0.1` to trace 10% of calls.
</ParamField>

<ParamField body="redactKeys" type="string[]" default="[&#x22;password&#x22;, &#x22;secret&#x22;, &#x22;token&#x22;, &#x22;api_key&#x22;]">
  Keys whose values are redacted before any data is stored. Passing this option replaces the default list — it is not merged. Applied regardless of `storePrompts`.
</ParamField>

<ParamField body="redactPii" type="boolean" default="false">
  When `true`, enables pattern-based PII detection (emails, phone numbers, SSNs, credit cards, and more) on top of key-based redaction. Off by default. See [PII redaction](/sdk/pii-redaction) for the full configuration options (`piiPreset`, `piiCategories`, `piiRedactionMode`, and others).
</ParamField>

<ParamField body="batchSize" type="number" default="50">
  Number of events to accumulate before flushing.
</ParamField>

<ParamField body="flushInterval" type="number" default="2000">
  Milliseconds between automatic flushes. The SDK also flushes on process exit.
</ParamField>

<ParamField body="baseURL" type="string" default="https://api.zespan.com">
  Override the API base URL. Use this only for self-hosted deployments.
</ParamField>

<ParamField body="enableOTel" type="boolean" default="false">
  When `true`, also exports spans to an OpenTelemetry-compatible backend. Requires `otelEndpoint`.
</ParamField>

<ParamField body="otelEndpoint" type="string">
  OTLP collector endpoint. Required when `enableOTel` is `true`.
</ParamField>

<ParamField body="otelServiceName" type="string">
  Service name attached to exported OpenTelemetry spans.
</ParamField>

<ParamField body="debug" type="boolean" default="false">
  When `true`, logs internal errors to the console. Enable during integration testing.
</ParamField>

<ParamField body="autopatch" type="boolean" default="true">
  When `true` (default), automatically instruments installed OpenAI, Anthropic, and Google (`@google/generative-ai` and `@google/genai`) clients as soon as they're detected — no explicit wrapper call required. Set to `false` to trace only clients you wrap explicitly. See [Automatic instrumentation](#automatic-instrumentation-autopatch).
</ParamField>

<ParamField body="projectId" type="string">
  Your Zespan project ID. Required to receive [config propagation](/sdk/config-propagation) updates from ZespanPilot or the dashboard.
</ParamField>

<ParamField body="enableZespanPilot" type="boolean" default="true">
  When `false`, disables config propagation even if `projectId` is set — the SDK will not fetch or apply remote config changes.
</ParamField>

## Provider wrappers

### OpenAI

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import OpenAI from "openai";
import { zespan } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const openai = zespan.wrapOpenAI(new OpenAI());

// Non-streaming
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Summarize this article." }],
});

// Streaming — TTFT captured automatically
const stream = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Write a poem." }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```

### Anthropic

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import Anthropic from "@anthropic-ai/sdk";
import { zespan } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const anthropic = zespan.wrapAnthropic(new Anthropic());

const message = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Explain monads in plain English." }],
});
```

### Google Generative AI

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { GoogleGenerativeAI } from "@google/generative-ai";
import { zespan } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const google = zespan.wrapGoogle(new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!));
const model = google.getGenerativeModel({ model: "gemini-2.5-flash" });
const result = await model.generateContent("What is quantum entanglement?");
```

<Note>
  This covers the legacy `@google/generative-ai` package. For Google's current SDK (`@google/genai`) — including image generation, embeddings, and Veo video generation — use `zespan.wrapGoogleGenAI()` instead. See [Google Generative AI](/sdk/integrations/google-genai) for the full reference.
</Note>

### AWS Bedrock

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
import { wrapBedrock } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const bedrock = wrapBedrock(new BedrockRuntimeClient({ region: "us-east-1" }));

const response = await bedrock.converse({
  modelId: "amazon.nova-lite-v1:0",
  messages: [{ role: "user", content: [{ text: "Summarize this document." }] }],
});
```

<Note>
  `wrapBedrock` patches both `client.converse` and `client.converseStream`, plus `client.send()` calls that dispatch a `ConverseStreamCommand` — streaming Bedrock calls are traced the same as non-streaming ones.
</Note>

### Mistral

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { Mistral } from "@mistralai/mistralai";
import { wrapMistral } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const mistral = wrapMistral(new Mistral({ apiKey: process.env.MISTRAL_API_KEY! }));

const response = await mistral.chat.complete({
  model: "mistral-small-latest",
  messages: [{ role: "user", content: "What is the capital of France?" }],
});
```

### Groq

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import Groq from "groq-sdk";
import { wrapGroq } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const groq = wrapGroq(new Groq({ apiKey: process.env.GROQ_API_KEY! }));

const response = await groq.chat.completions.create({
  model: "llama-3.3-70b-versatile",
  messages: [{ role: "user", content: "Explain gradient descent." }],
});
```

### OpenRouter

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import OpenAI from "openai";
import { zespan } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const openrouter = zespan.wrapOpenRouter(
  new OpenAI({
    baseURL: "https://openrouter.ai/api/v1",
    apiKey: process.env.OPENROUTER_API_KEY!,
  })
);

const response = await openrouter.chat.completions.create({
  model: "anthropic/claude-sonnet-4-6",
  messages: [{ role: "user", content: "Hello from OpenRouter!" }],
});
```

### LiteLLM

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { wrapLiteLLM } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const litellm = wrapLiteLLM({
  baseURL: "http://localhost:4000",
  apiKey: process.env.LITELLM_API_KEY,
});

const response = await litellm.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello from LiteLLM!" }],
});
```

## Automatic instrumentation (autopatch)

By default, Zespan also patches installed OpenAI, Anthropic, and Google (`@google/generative-ai`, `@google/genai`) SDKs directly at the module level as soon as `zespan.init()` runs and the package is detected — no `wrapOpenAI`-style call required. This is useful for code paths where you can't easily reach the client instantiation, such as third-party libraries that construct their own client internally.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { zespan } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! }); // autopatch runs here

const openai = new OpenAI();
// Traced automatically — no wrapOpenAI() call needed
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello!" }],
});
```

Set `autopatch: false` at init to disable this and trace only clients you wrap explicitly. Framework integrations (LangChain, Google ADK) are not affected by this flag — they use their own callback-based instrumentation.

If you're writing a custom integration for a framework Zespan doesn't support out of the box, call `markFrameworkActive(spanId)` before your handler's LLM call fires and `markFrameworkInactive(spanId)` after, so autopatch doesn't also enqueue an event for the same call and create a duplicate.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
markFrameworkActive(spanId); // suppress autopatch for this span
await llm.call(/* ... */);
markFrameworkInactive(spanId);
```

## Framework integrations

Beyond direct provider wrappers, Zespan integrates with common agent frameworks. Each has a dedicated reference page with full setup instructions:

* **[LangChain](/sdk/integrations/langchain)** — `ZespanCallbackHandler` traces chains, agents, tools, and retrievers automatically.
* **[Google ADK](/sdk/integrations/google-adk)** — `instrumentADK`, `wrapADKRunner`, and `wrapADKAgent` trace Agent Development Kit agents and multi-agent delegation.
* **[Vercel AI SDK](/sdk/integrations/vercel-ai)** — `instrumentVercelAI()` traces `generateText`, `streamText`, `generateObject`, and tool calls via OpenTelemetry.
* **[LlamaIndex](/sdk/integrations/llamaindex)** — `ZespanLlamaIndexHandler` attaches to LlamaIndex's callback manager to trace LLM calls, tool use, and agent steps.
* **[CrewAI](/sdk/integrations/crewai)** and **[AutoGen / AG2](/sdk/integrations/autogen)** — these frameworks are Python-native. The TypeScript SDK exports context-propagation helpers (`extractAgentContext`, `attachTraceToAutoGenMessage`, `extractTraceFromAutoGenMessage`) so a Node.js service receiving calls from a Python agent can continue the same trace.

## Context enrichment

Use `withZespanContext` to attach a `userId`, `sessionId`, or custom `tags` to all traces generated within a function scope.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { withZespanContext, zespan } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
const openai = zespan.wrapOpenAI(new OpenAI());

export async function POST(req: Request) {
  const { userId, sessionId } = await getSession(req);

  return withZespanContext(
    { userId, sessionId, tags: { feature: "chat", plan: "pro" } },
    async () => {
      const response = await openai.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: await req.text() }],
      });
      return Response.json(response);
    }
  );
}
```

<Tip>
  Set `userId` and `sessionId` on every request that involves a logged-in user. This enables per-user cost breakdown and session replay in the Zespan dashboard.
</Tip>

## Agent tracing

Use `withAgent` to trace a multi-step agent workflow. It creates an agent span and exposes methods to log plans, trace tool calls, and record handoffs.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { withAgent, zespan } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
const openai = zespan.wrapOpenAI(new OpenAI());

await withAgent(
  {
    name: "CustomerSupportAgent",
    role: "specialist",
    framework: "custom",
    tools: [{ name: "lookup_order", description: "Lookup order by id" }],
  },
  async (agent) => {
    agent.logPlan(["Lookup order", "Check refund policy", "Draft response"]);

    const order = await agent.traceTool(
      "lookup_order",
      { orderId: "123" },
      async () => ({ id: "123", status: "delivered", total: 49.99 })
    );

    agent.delegateTo("RefundPolicyAgent", "refund requested");

    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: `Order: ${JSON.stringify(order)}` }],
    });
    return response.choices[0].message.content;
  }
);
```

**`AgentContext` methods:**

* `agent.logPlan(steps: string[])` — records a planning span
* `agent.traceTool(name, args, fn)` — wraps an async function, records args and result as a tool span
* `agent.delegateTo(targetName, reason)` — records a handoff span

## Manual spans

Use `startSpan` to instrument any non-LLM operation (retrieval, embeddings, custom model calls) as a custom span and attach evaluation scores. It returns a `span` handle and a `run` helper for propagating context to nested calls.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { startSpan, zespan } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
const openai = zespan.wrapOpenAI(new OpenAI());

const { span, run } = startSpan({ name: "rag-pipeline", provider: "custom" });
try {
  const docs = await retrieveDocuments("user query");
  const response = await run(() =>
    openai.chat.completions.create({
      model: "gpt-4o",
      messages: [
        { role: "system", content: `Use these docs: ${docs}` },
        { role: "user", content: "user query" },
      ],
    })
  );
  span.setEvalScore("relevance", 0.92);
  await span.end({ status: "success" });
  return response.choices[0].message.content;
} catch (err) {
  await span.end({ status: "error", error_message: String(err) });
  throw err;
}
```

`span.end(options)` requires a `status` (`"success"`, `"error"`, `"timeout"`, `"rate_limited"`, or `"cancelled"`) and is async — always `await` it. See [Manual spans](/sdk/manual-spans) for the full option reference and a complete RAG pipeline example.

## Prompt management

The `PromptClient` fetches versioned prompts from the Zespan prompt library at runtime. Results are cached locally.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { PromptClient, zespan } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
const openai = zespan.wrapOpenAI(new OpenAI());
const prompts = new PromptClient();

const prompt = await prompts.get("support-reply", { label: "production" });
const text = prompts.compile(prompt, {
  customer_name: "Alex",
  order_id: "ORD-7821",
});

const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [
    { role: "system", content: text },
    { role: "user", content: "I need help with my order." },
  ],
});
```

See [Prompt management](/sdk/prompt-management) for the full API reference — `get`, `list`, `create`, `updateLabels`, `compile`, `clearCache`.

## Dataset experiment runs

The `client.datasets` client lets you run your own pipeline against a Zespan dataset and link the results back by trace ID — then score and compare runs in the dashboard. Fetch a dataset's items, run each one through your own code however you like, and link each item to a run using the trace ID your own tracing already produced.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { randomUUID } from "node:crypto";
import { zespan, withZespanContext } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
const openai = zespan.wrapOpenAI(new OpenAI());

const items = await zespan.getClient().datasets.getItems("support-replies");
const run = await zespan.getClient().datasets.createRun("support-replies", "gpt-4o-v2", {
  description: "Testing gpt-4o with the new system prompt",
});

for (const item of items) {
  // Generate your own trace ID and pass it into context so every wrapped
  // LLM call for this item shares it — that same ID is what you link back.
  const traceId = randomUUID();

  await withZespanContext({ traceId }, async () => {
    await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: String(item.input) }],
    });
  });

  await run.link(item.id, traceId);
}
```

### `datasets.getItems(datasetName)`

Lists a dataset's items by dataset name. Returns `Promise<DatasetItem[]>` — each item has `id`, `input`, `expectedOutput`, and `metadata`.

<ParamField body="datasetName" type="string" required>
  Name of the dataset to fetch items from.
</ParamField>

### `datasets.createRun(datasetName, runName, options?)`

Creates or fetches a named run against a dataset — idempotent, so it's safe to call every time a job starts. Returns `Promise<DatasetRunHandle>`.

<ParamField body="datasetName" type="string" required>
  Name of the dataset to run against.
</ParamField>

<ParamField body="runName" type="string" required>
  Name for this run, e.g. `"gpt-4o-v2"`. Calling `createRun` again with the same name returns the existing run instead of creating a duplicate.
</ParamField>

<ParamField body="options.description" type="string">
  Optional human-readable description of what this run is testing.
</ParamField>

### `run.link(datasetItemId, traceId, observationId?)`

Links a dataset item to the run via a trace ID your own code already produced. Returns `Promise<void>`.

<ParamField body="datasetItemId" type="string" required>
  `id` of the dataset item being evaluated, from `getItems`.
</ParamField>

<ParamField body="traceId" type="string" required>
  Trace ID that your pipeline produced for this item — for example, an ID you generated and passed into `withZespanContext`.
</ParamField>

<ParamField body="observationId" type="string">
  Optional span/observation ID, if you want to link to a specific span within the trace rather than the trace as a whole.
</ParamField>

Once results are linked, head to [Datasets](/dashboard/datasets) in the dashboard to score and compare runs against each other.

## PII redaction

Zespan automatically redacts values from `tags` and `metadata` fields (and stored prompt/completion text) before they leave your application. The key is preserved; the value is replaced with `"[REDACTED]"`.

**Default redacted keys** (applied when `redactKeys` is not set): `password`, `secret`, `token`, `api_key`.

Passing your own `redactKeys` array **replaces** the default list — it does not merge with it. Include any of the defaults you still want to keep:

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan.init({
  apiKey: process.env.ZESPAN_API_KEY!,
  redactKeys: ["password", "secret", "token", "api_key", "email", "phone", "dob"],
});
```

<Note>
  Redaction applies to `tags` and `metadata` fields, and to stored prompt and completion text. Prompt storage is on by default — set `storePrompts: false` to disable it entirely.
</Note>

For pattern-based detection of PII that doesn't live under a known key name — emails, phone numbers, SSNs, credit card numbers embedded in free text — set `redactPii: true` and configure a preset or category list. See [PII redaction](/sdk/pii-redaction) for the full `piiPreset`, `piiCategories`, and `piiRedactionMode` reference.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan.init({
  apiKey: process.env.ZESPAN_API_KEY!,
  redactPii: true,
  piiPreset: "gdpr",
});
```

## Guardrails

Guardrails run content safety checks before sending a prompt to the LLM (pre-check) and before returning the completion (post-check). Pass `guardrails: true` to any wrapper to enable both phases.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

const openai = zespan.wrapOpenAI(new OpenAI(), { guardrails: true });
```

See [Guardrails](/sdk/guardrails) for the full reference — parameter defaults, all fields, the direct `checkGuardrails()` API, and a multi-wrapper example.

## Config propagation

Zespan can push configuration changes — model overrides, sample rate, guardrail toggles — to your running application without a redeployment, as long as `projectId` is set at init (see [Init options](#init-options)). Changes made via ZespanPilot or the dashboard take effect within the next flush cycle (default 2 seconds).

See [Config propagation](/sdk/config-propagation) for the full list of rule types — including caching, retries, timeouts, fallbacks, and A/B tests — and the programmatic `ConfigClient` API.

## Flushing in serverless environments

In short-lived processes such as AWS Lambda, Vercel Functions, or Cloudflare Workers, call `zespan.getClient().flush()` explicitly before the handler returns to guarantee delivery.

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { zespan } from "@zespan/sdk";
import OpenAI from "openai";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
const openai = zespan.wrapOpenAI(new OpenAI());

export async function handler(event: any) {
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: event.prompt }],
  });
  const result = response.choices[0].message.content;

  await zespan.getClient().flush();
  return { statusCode: 200, body: result };
}
```

<Warning>
  Omitting `zespan.getClient().flush()` in serverless environments is the most common cause of missing traces. Always call it before your handler returns.
</Warning>
