Tracking spend in production LLM apps gets messy the moment you route requests to more than one model. This guide shows how to build reliable Vercel AI SDK token usage tracking across providers so you can attribute every prompt and completion token to the right user, feature, and vendor.
Step 1: Install and configure the Vercel AI SDK for multi-provider access
Start with the core ai package and the provider bindings you actually use. The SDK normalizes responses, but each provider needs its own factory.
npm install ai @ai-sdk/openai @ai-sdk/anthropic
Configure the clients from environment variables. Keep keys server-side; never ship them to the browser.
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
export const models = {
'gpt-4o': openai('gpt-4o'),
'claude-3-5-sonnet': anthropic('claude-3-5-sonnet-20240620'),
};
Step 2: Extract token usage from non-streaming calls
The generateText function returns a usage object with normalized fields. This is the simplest place to start Vercel AI SDK token usage tracking.
import { generateText } from 'ai';
export async function runPrompt(model: string, prompt: string) {
const { text, usage } = await generateText({
model: models[model],
prompt,
});
return {
text,
usage: {
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
totalTokens: usage.totalTokens,
},
};
}
The SDK converts provider-specific counts (e.g., Anthropic’s input_tokens) into promptTokens and completionTokens. Trust those fields; don’t parse raw responses.
Step 3: Capture usage from streamed responses
Production chat UIs stream. streamText exposes a usage promise and an onFinish callback. Use onFinish to record tokens without blocking the first token.
import { streamText } from 'ai';
export async function streamPrompt(model: string, prompt: string, onToken: (t: string) => void) {
const result = streamText({
model: models[model],
prompt,
onFinish: ({ usage }) => {
// fire-and-forget persistence
recordUsage(model, usage);
},
});
for await (const delta of result.textStream) {
onToken(delta);
}
// alternatively await result.usage if you need it in the caller
const finalUsage = await result.usage;
return finalUsage;
}
If you need the total before responding to the client, await result.usage after consuming the stream.
Step 4: Normalize and tag usage with request context
Raw token counts are useless without attribution. Wrap the SDK calls in a function that injects userId, feature, and model into your ledger.
type UsageEvent = {
userId: string;
feature: string;
model: string;
promptTokens: number;
completionTokens: number;
totalTokens: number;
ts: Date;
};
const events: UsageEvent[] = [];
export function recordUsage(
userId: string,
feature: string,
model: string,
usage: { promptTokens: number; completionTokens: number; totalTokens: number }
) {
events.push({
userId,
feature,
model,
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
totalTokens: usage.totalTokens,
ts: new Date(),
});
}
Call recordUsage from your API route after generateText or inside onFinish for streams. This gives you Vercel AI SDK token usage tracking tied to business context.
Step 5: Route through a single gateway to reduce provider skew
Maintaining separate provider bindings multiplies edge cases: differing headers, error shapes, and usage fields. If you route through a gateway such as n4n.ai, the single OpenAI-compatible endpoint fronts 240+ models and returns per-token usage metering, so your SDK code treats every call identically. The gateway also honors client routing directives and forwards provider cache-control hints, which keeps cache hits accounted for correctly.
Point the OpenAI factory at the gateway instead of the vendor:
const gateway = createOpenAI({
baseURL: process.env.N4N_BASE_URL ?? 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY!,
});
export const models = {
'gpt-4o': gateway('openai/gpt-4o'),
'claude-3-5-sonnet': gateway('anthropic/claude-3-5-sonnet'),
};
Your recordUsage logic doesn’t change. The usage object is still normalized by the Vercel AI SDK on top of the gateway’s metering.
Step 6: Persist token counts to your own ledger
In-memory arrays are fine for tests, not for production. Write to Postgres (or equivalent) with a minimal schema:
CREATE TABLE token_usage (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
feature TEXT NOT NULL,
model TEXT NOT NULL,
prompt_tokens INT NOT NULL,
completion_tokens INT NOT NULL,
total_tokens INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Insert from your recordUsage function using a pooled client:
import { pool } from './db';
export async function recordUsageDb(
userId: string,
feature: string,
model: string,
usage: { promptTokens: number; completionTokens: number; totalTokens: number }
) {
await pool.query(
`INSERT INTO token_usage (user_id, feature, model, prompt_tokens, completion_tokens, total_tokens)
VALUES ($1, $2, $3, $4, $5, $6)`,
[userId, feature, model, usage.promptTokens, usage.completionTokens, usage.totalTokens]
);
}
Swap the in-memory events.push for recordUsageDb in your routes.
Step 7: Verify end-to-end with a cross-provider script
Write a small Node script that hits two models, records usage, and prints the aggregate. Success means the summed tokens match the sum of individual call usages and the model column differs per provider.
import { runPrompt } from './lib';
import { recordUsageDb } from './lib/db';
async function main() {
const a = await runPrompt('gpt-4o', 'Say hello in 5 words.');
await recordUsageDb('test-user', 'onboarding', 'gpt-4o', a.usage);
const b = await runPrompt('claude-3-5-sonnet', 'Say hello in 5 words.');
await recordUsageDb('test-user', 'onboarding', 'claude-3-5-sonnet', b.usage);
const total = a.usage.totalTokens + b.usage.totalTokens;
console.log({
gpt: a.usage,
claude: b.usage,
combined: total,
});
}
main();
Run it with tsx script.ts. Verify success by checking the console output shows non-zero promptTokens and completionTokens for both models, and by querying SELECT model, sum(total_tokens) FROM token_usage GROUP BY model;—you should see two rows.
Step 8: Handle edge cases in Vercel AI SDK token usage tracking
A few things will bite you in production:
- Cache hits: Anthropic and OpenAI return lower prompt token counts when a prompt prefix is cached. The SDK surfaces the same
promptTokensfield; don’t assume it equals your input length. - Stream interruptions: If the client disconnects,
onFinishmay not fire. Use a timeout or middleware that records partial usage fromawait result.usagein afinallyblock. - Model aliases: Gateway model IDs like
openai/gpt-4odiffer from vendor-native IDs. Store the ID you passed to the SDK, not the resolved vendor ID, to keep queries stable.
Implement these and your Vercel AI SDK token usage tracking will survive real traffic.