Integration docs

view as markdown

Send Vercel AI SDK 7 traces to taleseal

Register telemetry once at startup; every generateText/streamText call then emits current GenAI semantic-convention spans, and taleseal composes each run into a draft tale in your dashboard — private, no public URL, published only when you choose.

Prerequisites

Install

Shell
npm install [email protected] @ai-sdk/[email protected] @opentelemetry/[email protected] @opentelemetry/[email protected] zod

Configure

AI SDK 7 telemetry is on by default once registered — you bring your own OTel SDK for the export. Save as telemetry.ts and import it once at startup:

TypeScript
// telemetry.ts — register once at startup
import { OpenTelemetry } from "@ai-sdk/otel";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { registerTelemetry } from "ai";

export const provider = new NodeTracerProvider({
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter({
        url: "https://taleseal.com/v1/traces",
        headers: { "x-api-key": process.env.TALESEAL_API_KEY ?? "" },
      }),
    ),
  ],
});
provider.register();

registerTelemetry(new OpenTelemetry({ tracer: provider.getTracer("my-agent") }));

Name each run with the per-call option telemetry: { functionId: "my-agent" } — it becomes the tale's agent name. In a short-lived script, await provider.forceFlush() before exit or the batch never leaves the process.

Fire a test run (no model API key needed)

MockLanguageModelV4 from ai/test scripts the model, so this proves the telemetry path without any provider credentials. Save as test-taleseal.ts and run it (bun test-taleseal.ts or npx tsx test-taleseal.ts) with TALESEAL_API_KEY set:

TypeScript
// test-taleseal.ts — a scripted two-step tool run on a mock model
import { generateText, stepCountIs, tool } from "ai";
import { MockLanguageModelV4 } from "ai/test";
import { z } from "zod";
import { provider } from "./telemetry"; // the file from the Configure step

const usage = {
  inputTokens: { total: 20, noCache: 20, cacheRead: undefined, cacheWrite: undefined },
  outputTokens: { total: 10, text: 10, reasoning: undefined },
};

const model = new MockLanguageModelV4({
  // an array scripts consecutive calls: first a tool call, then the final answer
  doGenerate: [
    {
      content: [{ type: "tool-call", toolCallId: "call-1", toolName: "getWeather", input: '{"city":"Sheffield"}' }],
      finishReason: { unified: "tool-calls", raw: undefined },
      usage,
      warnings: [],
    },
    {
      content: [{ type: "text", text: "Dry and bright in Sheffield." }],
      finishReason: { unified: "stop", raw: undefined },
      usage,
      warnings: [],
    },
  ],
});

await generateText({
  model,
  prompt: "What is the weather in Sheffield?",
  tools: {
    getWeather: tool({
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => `Dry and bright in ${city}.`,
    }),
  },
  stopWhen: stepCountIs(3),
  telemetry: { functionId: "my-agent" },
});

// a short-lived script must flush, or the batch never leaves the process
await provider.forceFlush();

The run exports 6 spans in one batch: invoke_agent, two steps, two chat calls and execute_tool getWeather.

Verify

  1. Run the test script above; it exits after the flush.
  2. Ask taleseal what it received:
Shell
curl -s https://taleseal.com/v1/otlp/status -H "Authorization: Bearer $TALESEAL_API_KEY"
  1. The response lists your recent runs, newest first:
JSON
{
  "runs": [
    {
      "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
      "firstSeen": "2026-07-13T14:02:01Z",
      "lastSeen": "2026-07-13T14:02:05Z",
      "spans": 6,
      "inputTokens": 40,
      "outputTokens": 20,
      "errored": false,
      "state": "collecting",
      "title": null,
      "draftUrl": null
    }
  ],
  "generatedAt": "2026-07-13T14:02:11Z"
}

Success = a run with spans ≥ 6 and errored: false, within seconds of the flush. state: "collecting" means spans are being received and counted — the integration works; you do not need to wait for anything else.

  1. The draft tale appears once the run has been idle for about two minutes (plus up to a minute of sweep interval): poll step 2 until state is "finalised" and title is set; draftUrl points at the draft.
  2. Tell your human: the draft is in the dashboard at taleseal.com/dashboard. Drafts are private with no public URL; publishing is a deliberate act from the dashboard.

If it didn't work

SymptomCauseFix
Script exits cleanly but status shows no runsProcess exited before the batch exporter flushedawait provider.forceFlush() before exit
401 invalid or missing API keyTALESEAL_API_KEY unset or wrong, or the header renamedExport the env var; the header must be x-api-key
415 unsupported media type on exportExporter configured for gRPC or an odd content typeUse @opentelemetry/exporter-trace-otlp-proto (http/protobuf); gRPC is not supported
Tale's agent name is mock-model-id or blankNo telemetry: { functionId } on the callPass telemetry: { functionId: "my-agent" } per call
400 request body over … or 429Batch too large or rate limitedExporters honour retry semantics; reduce batch size if persistent
Spans counted but draft never appearsRun still inside the idle gap, or spans still tricklingFinalisation is (idle gap 120 s) + (sweep tick ≤ 60 s) after the last span

Integration overview · llms.txt · panic path: DELETE /v1/tales?runId=<trace id hex>