Carbon-LLMGuides · Vercel AI SDK
Retour à l'accueil

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.

Pourquoi mesurer le carbone du Vercel AI SDK ?

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.

Variables d'environnement

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-tenant
Helper carbon-llm (5 lignes, à copier-coller)

Cré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))
}
Exemple : generateText

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 })
}
Exemple : streamText (pattern recommandé pour SSE)

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()
}
Exemple : generateObject (sortie structurée)

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)
}
Pattern alternatif : Vercel AI SDK middleware (1 seul endroit)

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: "..." })
Multi-tenant : un dashboard par client final

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'usage
Vérifier que tout marche

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 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 dashboard
Erreurs courantes (et leur correction)

Si 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).