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 agrégée dans votre vue CO₂ et exportable en CSV.
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 agrège le CO₂ par tenant et l'usage est exportable en CSV. 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
// → CO₂ agrégé par tenant + export CSV de l'usageTrois 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 doit montrer la nouvelle ligne d'usage
# 4. Vérifier l'agrégation : le CO₂ du test-tenant apparaît dans la vue
# 5. Exporter l'usage en CSV depuis le dashboardSi 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(e) à tracker votre Vercel AI SDK ?
Phase d'accès anticipé : tout est gratuit, toutes les fonctionnalités débloquées (mesure CO₂, rappel données, déploiement MDM).