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.
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-protoimport { 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.
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.
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 },
},
});See the demo dashboard (no sign-up).
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 });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.
Yes. streamText records usage on the span when the stream finishes, the same as generateText.
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.
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.
No. FiscaLLM reads the model, token counts, user ID and function ID from each span and nothing else.