Suivre les émissions carbone des LLMs dans Next.js — guide pas à pas

Instrumentez votre application Next.js pour mesurer et journaliser l'empreinte CO₂ de chaque appel API OpenAI, Anthropic ou Mistral avec carbon-llm. Aucun prompt collecté, prêt pour la déclaration ESRS E1.

Pourquoi suivre les émissions LLM dans Next.js ?

La Corporate Sustainability Reporting Directive (CSRD) de l'UE et sa norme ESRS E1 imposent aux entreprises de déclarer leurs émissions de gaz à effet de serre Scope 3 — incluant les services numériques achetés. L'inférence LLM relève du Scope 3 Catégorie 1 (biens et services achetés) ou de la Catégorie 11 (utilisation des produits vendus) selon votre modèle économique.

Next.js est le framework dominant pour les applications IA en production : server actions, routes API et endpoints de streaming appellent tous des fournisseurs LLM. L'instrumentation appartient à cette couche serveur — pas au client — car le client ne voit jamais les comptes de tokens.

carbon-llm est conçu précisément pour ce cas d'usage : un seul POST par appel LLM, aucune donnée de prompt, retour immédiat, et appels en fire-and-forget qui n'ajoutent jamais de latence à vos réponses.

Variables d'environnement

Ajoutez votre clé API à .env.local. Next.js n'expose pas au navigateur les variables sans le préfixe NEXT_PUBLIC_, votre clé est donc sécurisée.

.env.local

CARBON_LLM_API_KEY=isv_live_xxxxxxxxxxxx
Exemple de Server Action (App Router)

Instrumentez une Server Action Next.js qui appelle OpenAI. L'appel fetch vers /v1/track est en fire-and-forget après la réponse, il ne bloque donc jamais l'utilisateur :

app/actions/chat.ts

"use server"
import OpenAI from "openai"

const openai = new OpenAI()
const CARBON_API = "https://carbon-llm.com/api/v1/track"

export async function chat(prompt: string) {
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: prompt }],
  })

  // Fire-and-forget — jamais awaited, ne bloque jamais l'utilisateur
  fetch(CARBON_API, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.CARBON_LLM_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      tenant_id: "default",
      model: response.model,
      prompt_tokens: response.usage?.prompt_tokens ?? 0,
      completion_tokens: response.usage?.completion_tokens ?? 0,
    }),
  }).catch(() => {}) // silencieux en cas d'erreur réseau

  return response.choices[0].message.content
}
Exemple de route API (Pages Router ou Edge)

Si vous utilisez le Pages Router ou avez besoin d'un endpoint en Edge runtime, le pattern est identique — appelez /v1/track après avoir collecté l'objet usage :

pages/api/chat.ts

import type { NextApiRequest, NextApiResponse } from "next"
import Anthropic from "@anthropic-ai/sdk"

const anthropic = new Anthropic()
const CARBON_API = "https://carbon-llm.com/api/v1/track"

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { prompt } = req.body as { prompt: string }

  const message = await anthropic.messages.create({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: prompt }],
  })

  fetch(CARBON_API, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.CARBON_LLM_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      tenant_id: "default",
      model: message.model,
      prompt_tokens: message.usage.input_tokens,
      completion_tokens: message.usage.output_tokens,
    }),
  }).catch(() => {})

  res.json({ text: (message.content[0] as { text: string }).text })
}
Réponses en streaming

Avec le streaming, les comptes de tokens ne sont disponibles qu'une fois le flux terminé. Accumulez-les et appelez /v1/track à la fermeture du stream :

app/api/chat/route.ts — streaming

import { openai } from "@ai-sdk/openai"
import { streamText } from "ai"

const CARBON_API = "https://carbon-llm.com/api/v1/track"

export const runtime = "edge"

export async function POST(req: Request) {
  const { messages } = await req.json()

  const result = streamText({
    model: openai("gpt-4o"),
    messages,
    onFinish({ usage }) {
      // Appelé une fois le flux terminé — safe pour /v1/track
      fetch(CARBON_API, {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.CARBON_LLM_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          tenant_id: "default",
          model: "gpt-4o",
          prompt_tokens: usage.promptTokens,
          completion_tokens: usage.completionTokens,
        }),
      }).catch(() => {})
    },
  })

  return result.toDataStreamResponse()
}
Multi-tenant : suivi par client

Si vous construisez un SaaS et avez besoin d'une ventilation des émissions par client (obligatoire pour les rapports CSRD multi-clients), passez un tenant_id :

Suivi par tenant

body: JSON.stringify({
  tenant_id: session.organizationId, // votre identifiant client / organisation
  model: response.model,
  prompt_tokens: response.usage?.prompt_tokens ?? 0,
  completion_tokens: response.usage?.completion_tokens ?? 0,
})
Vérifier et exporter

Après l'instrumentation, ouvrez le tableau de bord carbon-llm et déclenchez un véritable appel LLM depuis votre application. Vous devriez voir l'évènement apparaître dans le flux Évènements en quelques secondes.

Une fois les données accumulées, allez dans Rapports → Générer un rapport ESRS E1. Choisissez une plage de dates, sélectionnez votre ou vos tenants, et exportez en PDF ou JSON. Le rapport PDF inclut les sources des facteurs d'émission, la méthodologie et l'attribution Scope 3 requises par ESRS E1.

Prêt à démarrer le suivi ?

Gratuit jusqu'à 100 000 évènements/mois, phase d'accès anticipé.