Integrations

Vercel AI SDK cost per user

In short: the AI SDK already emits an OpenTelemetry span for every model call, with the model and token counts on it. Export those spans to FiscaLLM, attach your user ID, and you get AI cost per user next to what each user pays. Your generateText and streamText calls stay as they are.

1. Export traces to FiscaLLM

Create an app, copy the ingest key and store it as FISCALLM_KEY. Then register an exporter once at startup (in Next.js, in instrumentation.ts):

npm install @ai-sdk/otel @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-proto
import { registerTelemetry } from "ai";
import { OpenTelemetry } from "@ai-sdk/otel";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";

new NodeTracerProvider({
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter({
        url: "https://fiscallm.com/api/v1/otel/v1/traces",
        headers: { Authorization: `Bearer ${process.env.FISCALLM_KEY}` },
      }),
    ),
  ],
}).register();

// Copy the user ID onto every span the AI SDK creates.
registerTelemetry(
  new OpenTelemetry({
    enrichSpan: ({ runtimeContext }) => ({ "user.id": runtimeContext?.userId }),
  }),
);

Already exporting to Langfuse, Datadog or another backend? Keep it and add FiscaLLM as one more span processor. See OpenTelemetry.

2. Pass the user ID with each call

const result = await generateText({
  model: openai("gpt-5-mini"),
  prompt,
  runtimeContext: { userId },                 // the ID your billing knows
  telemetry: {
    functionId: "support-chat",               // shows up as the feature
    includeRuntimeContext: { userId: true },
  },
});

userId should be the same ID RevenueCat or Stripe has for this user, so cost and revenue can be matched. functionId is optional; it splits cost by feature.

If you already put user.id on your own request span, that works too: FiscaLLM looks at the parents of each model call. Setting it through enrichSpan is more reliable, because a batch exporter can send the model call before the request span has ended.

AI SDK 4, 5 and 6

These versions have telemetry built in and need no @ai-sdk/otel. Register the same exporter and pass the user in the metadata:

const result = await generateText({
  model: openai("gpt-5-mini"),
  prompt,
  experimental_telemetry: {
    isEnabled: true,
    functionId: "support-chat",
    metadata: { userId },
  },
});

What you get

See the demo dashboard (no sign-up).

Prefer no OpenTelemetry?

Use the FiscaLLM SDK and report usage from the result yourself. There is a runnable example in examples/vercel-ai-sdk.

import { createMeter } from "fiscallm";
const meter = createMeter({ apiKey: process.env.FISCALLM_KEY, waitUntil });

const { usage } = await generateText({ model: openai("gpt-5-mini"), prompt });
meter.track({ userId, provider: "openai", model: "gpt-5-mini", inputTokens: usage.inputTokens, outputTokens: usage.outputTokens });

Questions

Does a multi-step tool or agent run count every model call?

Yes. Each step's model call is its own span and its own cost event, so a run with two steps is stored as two calls with their own token counts. The outer spans are not counted, so nothing is doubled.

Does streaming work?

Yes. streamText records usage on the span when the stream finishes, the same as generateText.

Which AI SDK versions work?

Tested on AI SDK 7 with @ai-sdk/otel. AI SDK 4 to 6 (experimental_telemetry) emit an older span format, which is read too.

Which providers work?

Any provider the AI SDK supports. The model ID on the span is priced from FiscaLLM's table for OpenAI, Anthropic and Google models; other models are stored and shown as unpriced until you set a price.

Are prompts and responses stored?

No. FiscaLLM reads the model, token counts, user ID and function ID from each span and nothing else.