Vercel AI SDK + carbon-llm — tracker l'empreinte carbone de chaque generateText / streamText
Guide pas à pas pour mesurer le CO₂e de chaque appel Vercel AI SDK (generateText, streamText, generateObject) avec carbon-llm. Pas de SDK propriétaire, fire-and-forget, multi-tenant, prêt CSRD ESRS E1-6.
Le Vercel AI SDK est l'abstraction la plus utilisée en 2026 pour appeler des LLM en TypeScript : generateText, streamText, generateObject, embeddings — un seul package, n providers (OpenAI, Anthropic, Mistral, Google, Cohere, Groq, etc.). C'est aussi la couche idéale pour insérer un tracker carbon : tous les appels passent par les mêmes hooks.
carbon-llm s'intègre en une fonction utilitaire de 5 lignes — pas de wrapper SDK, pas de patch monkey, pas de middleware obligatoire. Vous gardez votre code AI SDK tel quel et ajoutez un fire-and-forget après chaque appel.
Le résultat : chaque generateText devient une ligne dans votre dashboard avec model + tokens + gCO2e + tenant_id, automatiquement utilisée dans votre export ESRS E1-6 mensuel.
Ajoutez votre clé API à .env.local (et aux env vars de votre plateforme prod). Sans préfixe NEXT_PUBLIC_, la clé reste server-side.
.env.local
# carbon-llm
CARBON_LLM_API_KEY=isv_live_xxxxxxxxxxxx
CARBON_LLM_BASE_URL=https://carbon-llm.com
# Optional — used as default tenant_id when not specified per-call
# CARBON_LLM_TENANT_ID=default-tenantCréez un fichier lib/carbon-llm.ts qui exporte une fonction trackUsage(). Elle accepte le résultat brut du Vercel AI SDK et envoie l'event en fire-and-forget. Aucun await — donc latence ajoutée = 0 ms.
lib/carbon-llm.ts
type AiSdkUsage = {
promptTokens?: number
completionTokens?: number
totalTokens?: number
}
const BASE_URL = process.env.CARBON_LLM_BASE_URL ?? "https://carbon-llm.com"
const API_KEY = process.env.CARBON_LLM_API_KEY
/**
* Fire-and-forget : envoie un event à /v1/track sans bloquer la réponse.
* Aucune erreur ne remonte au caller — les events ratés sont loggés côté serveur uniquement.
*/
export function trackUsage(opts: {
model: string
usage: AiSdkUsage
tenantId?: string
ingestionSource?: string
}) {
if (!API_KEY) return // dev local sans clé : on no-op silencieusement
const body = {
model: opts.model,
prompt_tokens: opts.usage.promptTokens ?? 0,
completion_tokens: opts.usage.completionTokens ?? 0,
tenant_id: opts.tenantId ?? process.env.CARBON_LLM_TENANT_ID ?? "default",
ingestion_source: opts.ingestionSource ?? "vercel_ai_sdk",
}
// Pas d'await — fire-and-forget. Catch silencieux pour éviter les unhandled rejection.
fetch(`${BASE_URL}/api/v1/track`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}).catch((e) => console.error("[carbon-llm] track failed:", e))
}Cas le plus courant : un appel one-shot generateText. La réponse contient déjà l'objet usage — il suffit de l'envoyer après le return :
app/api/chat/route.ts
import { openai } from "@ai-sdk/openai"
import { generateText } from "ai"
import { trackUsage } from "@/lib/carbon-llm"
export async function POST(req: Request) {
const { prompt, tenantId } = await req.json()
const result = await generateText({
model: openai("gpt-4o"),
prompt,
})
// Fire-and-forget — la réponse part au client immédiatement
trackUsage({
model: "gpt-4o",
usage: result.usage,
tenantId,
})
return Response.json({ text: result.text })
}Pour streamText, l'objet usage n'est disponible qu'à la fin du stream — utilisez le callback onFinish. Le client reçoit déjà tout son contenu, le tracker tourne après :
app/api/chat-stream/route.ts
import { anthropic } from "@ai-sdk/anthropic"
import { streamText } from "ai"
import { trackUsage } from "@/lib/carbon-llm"
export async function POST(req: Request) {
const { messages, tenantId } = await req.json()
const result = streamText({
model: anthropic("claude-sonnet-4"),
messages,
onFinish: ({ usage }) => {
// Stream terminé — usage est disponible. Track après l'envoi au client.
trackUsage({
model: "claude-sonnet-4",
usage,
tenantId,
ingestionSource: "vercel_ai_sdk_stream",
})
},
})
return result.toDataStreamResponse()
}generateObject (avec Zod schema) tracke aussi l'usage — strictement la même API que generateText :
app/api/extract/route.ts
import { mistral } from "@ai-sdk/mistral"
import { generateObject } from "ai"
import { z } from "zod"
import { trackUsage } from "@/lib/carbon-llm"
const InvoiceSchema = z.object({
amount: z.number(),
currency: z.string(),
vendor: z.string(),
})
export async function POST(req: Request) {
const { invoiceText, tenantId } = await req.json()
const result = await generateObject({
model: mistral("mistral-medium-3"),
schema: InvoiceSchema,
prompt: `Extract: ${invoiceText}`,
})
trackUsage({
model: "mistral-medium-3",
usage: result.usage,
tenantId,
ingestionSource: "vercel_ai_sdk_object",
})
return Response.json(result.object)
}Si vous voulez tracer chaque appel sans toucher chaque endpoint, utilisez le middleware AI SDK. Toutes les requêtes passent par le wrapper — track devient automatique :
lib/carbon-llm-middleware.ts
import { wrapLanguageModel, type LanguageModelV1Middleware } from "ai"
import { trackUsage } from "@/lib/carbon-llm"
export const carbonTrackingMiddleware: LanguageModelV1Middleware = {
wrapGenerate: async ({ doGenerate, model, params }) => {
const result = await doGenerate()
trackUsage({
model: model.modelId,
usage: result.usage,
tenantId: (params.providerMetadata as { tenantId?: string } | undefined)?.tenantId,
})
return result
},
wrapStream: async ({ doStream, model, params }) => {
const { stream, ...rest } = await doStream()
let totalUsage: { promptTokens?: number; completionTokens?: number } | undefined
const tracked = stream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (chunk.type === "finish") totalUsage = chunk.usage
controller.enqueue(chunk)
},
flush() {
if (totalUsage) {
trackUsage({
model: model.modelId,
usage: totalUsage,
tenantId: (params.providerMetadata as { tenantId?: string } | undefined)?.tenantId,
})
}
},
}),
)
return { stream: tracked, ...rest }
},
}
// Usage:
// import { carbonTrackingMiddleware } from "@/lib/carbon-llm-middleware"
// const trackedModel = wrapLanguageModel({ model: openai("gpt-4o"), middleware: carbonTrackingMiddleware })
// const result = await generateText({ model: trackedModel, prompt: "..." })Si votre SaaS revend l'IA à plusieurs clients, passez tenantId à chaque trackUsage(). Le dashboard carbon-llm filtre automatiquement les events par tenant et génère des PDF mensuels par client. Aucun setup additionnel.
tenantId peut être l'UUID de votre customer interne (recommandé), un workspace ID, un email organisation. Limite : 64 caractères, alphanumérique + tirets.
Exemple multi-tenant
// Dans votre route handler :
const { user } = await getCurrentUser(req)
const result = await generateText({ ... })
trackUsage({
model: "gpt-4o",
usage: result.usage,
tenantId: user.organizationId, // ← l'UUID interne de votre client
})
// Plus tard : Dashboard → Tenants → user.organizationId
// → vue détaillée filtrée + PDF mensuel exportable + bundle audit signéTrois minutes de checks après l'intégration — pour être sûr que les events arrivent bien :
Sanity check
# 1. Tester /api/v1/track manuellement avec curl :
curl -X POST https://carbon-llm.com/api/v1/track \
-H "Authorization: Bearer $CARBON_LLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"prompt_tokens": 100,
"completion_tokens": 50,
"tenant_id": "test-tenant"
}'
# Attendu : 202 + estimated_co2_grams dans la réponse
# 2. Faire un vrai appel AI SDK depuis votre app
# 3. Vérifier le dashboard : /dashboard/events doit montrer la nouvelle ligne
# 4. Vérifier le multi-tenant : /dashboard/tenants doit lister test-tenant
# 5. Tester un export ESRS E1-6 (Pro) : /dashboard/exportsSi rien n'apparaît dans le dashboard : 95 % du temps c'est l'un de ces 3 problèmes.
1. CARBON_LLM_API_KEY manquante en prod — Vercel/Fly stocke les vars dans son propre dashboard, pas dans .env. Vérifiez les Environment Variables côté hébergeur.
2. Vous appelez trackUsage() avant que le stream soit terminé — utilisez onFinish (streamText) ou await result (generateText), pas un timer arbitraire.
3. tenantId trop long ou avec caractères spéciaux — limite 64 chars, alphanum + tirets. Hash si vous avez des UUIDs longs.
Prêt à tracker votre Vercel AI SDK ?
Gratuit jusqu'à 100 000 événements/mois — sans carte bancaire. Founders pricing 49 €/mois verrouillé à vie pour les 50 premiers Pro.