Pourquoi votre outil IA ne doit jamais contenir de clé Gemini dans le navigateur — guide pas à pas du proxy côté serveur

mai 4, 2026

Quelques personnes ont lu mon article sur la façon dont la clé API de la première version de GeminiOmni a été récupérée en dix-sept minutes et m'ont demandé la version complète de l'architecture du proxy. Voici cet article.

Si vous construisez quoi que ce soit qui appelle une API IA payante — Gemini, OpenAI, Anthropic, Replicate, toute clé mesurée — le schéma ci-dessous est ce qui vous sépare d'une surprise un mardi matin dans vos alertes de facturation. Ce n'est pas compliqué. C'est ce qui porte toute la charge.

Je vais vous présenter l'architecture, les quatre choses concrètes que vous devez maîtriser, et les cas limites que j'ai rencontrés en faisant fonctionner ce système en production pendant un mois.

Le schéma de base, illustré

La forme est la même, quel que soit le fournisseur d'IA que vous utilisez :

┌─────────────────┐       ┌──────────────────────┐       ┌──────────────┐
│                 │       │                      │       │              │
│  Navigateur     │       │  Votre route API     │       │  Fournisseur │
│  (non fiable)   │  ───→ │  (serveur, a la clé) │  ───→ │  (Gemini /   │
│                 │       │                      │       │   OpenAI...) │
│  - prompt       │       │  - vérification auth │       │              │
│  - upload       │       │  - limite de débit   │       └──────────────┘
│                 │       │  - vérification quota │
│                 │       │  - attache la clé API │
│                 │       │  - appelle fournisseur│
│                 │       │  - retourne résultat  │
│                 │ ←───  │                      │
└─────────────────┘       └──────────────────────┘

L'idée est que le navigateur voit un prompt entrer et un résultat sortir. Il n'a aucune idée du modèle appelé, de la clé API, de l'apparence du fournisseur en amont, ni du coût de l'appel. C'est la quantité d'information appropriée pour le navigateur.

Ce à quoi ressemble une configuration "correcte", concrètement

Quatre conditions doivent être remplies pour que cette architecture vous protège réellement. En manquer une seule et la protection s'effondre.

1. La clé API ne vit que sur le serveur

En termes Next.js : la variable d'environnement NE doit PAS commencer par NEXT_PUBLIC_. En termes d'exécution : seul le code qui s'exécute dans un gestionnaire de route (app/api/.../route.ts), un composant serveur ou une action serveur doit toucher la clé.

// app/api/ai/generate/route.ts — serveur uniquement
const GEMINI_KEY = process.env.GEMINI_API_KEY; // ✅ serveur uniquement
if (!GEMINI_KEY) throw new Error('GEMINI_API_KEY non définie');

Une vérification au démarrage qui fait planter l'application si GEMINI_API_KEY est absente vaut son pesant d'or. Le pire mode de défaillance est que l'application s'exécute silencieusement sans clé et retombe sur une valeur par défaut qui vide vos économies.

2. Le navigateur ne voit jamais les réponses brutes du fournisseur

Lorsque l'appel au fournisseur se termine, vous sélectionnez ce qu'il faut renvoyer. Le navigateur n'a PAS besoin de savoir :

  • Quel modèle exact a été appelé (Veo 3.1 Fast vs Veo 3.1 Standard)
  • L'ID de requête du fournisseur (utile pour le support, inutile pour les clients)
  • Les en-têtes de limite de débit du fournisseur
  • Le nombre de tokens ou le coût de l'appel

Pourquoi c'est important : la divulgation d'informations s'accumule. Si votre navigateur peut voir "model: gemini-2.5-flash, tokens: 4096", un scraper peut faire correspondre votre interface utilisateur à la page de tarification de votre fournisseur et rétro-ingénierer votre économie unitaire. Ce n'est pas catastrophique, mais ce n'est pas génial. Supprimez-le.

// Ne renvoyer que ce dont l'UI a besoin
return Response.json({
  imageUrl: result.generated_images[0].url,
  watermarked: !user.isPro,
  // Ne pas renvoyer : nom du modèle, tokens utilisés, réponse brute du fournisseur
});

3. Les vérifications d'auth, de limite de débit et de quota se font AVANT l'appel au fournisseur

Le proxy n'est pas qu'un simple passage. C'est l'endroit où vous appliquez les règles :

// Dans la route API, dans l'ordre :
1. Authentifier la requête (cookie de session, JWT, peu importe).
   Les requêtes anonymes sont autorisées pour les fonctionnalités gratuites, mais suivies par IP.

2. Limiter le débit par ID utilisateur (ou par IP pour les anonymes).
   GeminiOmni : 10 req/min anonymes, 60 req/min Pro, 200 req/min Team.

3. Vérifier le quota de l'utilisateur pour l'opération spécifique.
   Les utilisateurs gratuits ont 5 générations vidéo par mois — vérifier la BASE DE DONNÉES
   AVANT l'appel coûteux, pas après.

4. Maintenant seulement — appeler le fournisseur.

5. Incrémenter les compteurs d'utilisation en cas de succès.

L'ordre est important. Si vous appelez le fournisseur en premier, puis vérifiez le quota, vous avez déjà payé pour l'inférence, que l'utilisateur y ait eu droit ou non. Chaque vérification de quota se fait à l'avance.

4. Le proxy a sa propre observabilité

Enregistrez chaque appel au fournisseur depuis le proxy avec : horodatage, ID utilisateur, route, modèle, taille d'entrée, taille de sortie, latence, succès/échec, et le coût approximatif en dollars. C'est votre reçu de facturation et votre surface de débogage.

await logUsage({
  userId,
  route: '/api/ai/generate',
  model: 'gemini-2.5-flash-image',
  inputBytes: prompt.length + imageBytes,
  outputBytes: result.length,
  latencyMs: Date.now() - start,
  costUsd: estimateCost(model, inputBytes, outputBytes),
  status: 'success',
});

Lorsque votre alerte de facturation du mardi matin se déclenche parce que les dépenses ont doublé pendant la nuit, cette table est ce que vous interrogerez en SQL pour trouver quel utilisateur, quelle route, quel modèle. Sans elle, vous devinez.

Cas limites rencontrés en production

Un mois de fonctionnement du proxy en production a révélé quelques éléments non évidents.

Les réponses en streaming nécessitent des proxies de streaming. Le point de terminaison streamGenerateContent de Gemini renvoie un flux de générations partielles. Si vous appelez naïvement response.json() dans votre proxy, vous mettrez en mémoire tampon l'intégralité du flux et perdrez l'avantage de la latence. La solution consiste à rediriger le flux amont via votre proxy en tant que ReadableStream, ce qui signifie que votre route API renvoie new Response(stream) au lieu de Response.json(...). Cela vaut la peine d'être fait — la latence perçue de la génération vidéo passe de "attendre 90 secondes" à "voir quelque chose se produire immédiatement".

Les téléchargements de fichiers volumineux nécessitent des téléchargements directs vers le stockage. Si un utilisateur télécharge une vidéo de 50 Mo pour une génération image-à-vidéo, vous ne voulez pas que ces 50 Mo transitent par votre route API Next.js. Le schéma est : le client télécharge directement vers Cloud Storage / R2 via une URL pré-signée, puis envoie uniquement l'URL à votre proxy. Le proxy transmet l'URL au paramètre "media URI" de Gemini, et les octets ne touchent jamais votre serveur. Cela réduit votre facture de sortie d'environ 95 % sur les chemins gourmands en téléchargement.

Les erreurs du fournisseur doivent être traduites. Gemini renvoie des erreurs comme "PROMPT_BLOCKED", "QUOTA_EXCEEDED" ou "INVALID_ARGUMENT". Le navigateur n'a pas besoin de voir ces chaînes — elles sont déroutantes et divulguent des détails sur le fournisseur. Traduisez chacune en un message destiné à l'utilisateur : "Votre prompt a été bloqué par les filtres de sécurité", "Vous avez utilisé vos générations gratuites pour ce mois", "Cette image semble corrompue." Une simple table de correspondance de trois lignes.

L'estimation du coût est par appel, pas par token. J'ai essayé d'estimer le coût en dollars de chaque appel en comptant les tokens. Puis j'ai remarqué que la facturation de Veo est en SECONDES de vidéo de sortie, Imagen est par IMAGE, et Nano Banana 2 a un tarif à quatre niveaux basé sur la résolution. La fonction de coût pour le proxy est une instruction switch, pas une multiplication. Il n'y a pas de moyen propre de contourner cela — chaque fournisseur facture différemment et vous devez encoder chacun.

À quoi cela ressemble dans la base de code de GeminiOmni

Chaque appel de modèle dans /tools/text-to-video, /tools/image-to-video, /tools/nano-banana-edit, /tools/pdf-chat, et le chat en direct à /chat passe par app/api/ai/generate/route.ts et app/api/ai/query/route.ts. Deux points de terminaison. Environ 400 lignes de code ensemble. Les quatre vérifications ci-dessus sont toutes appliquées avant que tout appel au fournisseur ne quitte mon serveur.

J'ai nommé le modèle dans l'interface utilisateur, mais pas dans la réponse de l'API. Le badge sous chaque clip généré dit "Veo 3.1 Fast" — cela provient d'une recherche côté serveur au moment du rendu, pas de la réponse de l'API. C'est le genre de chose qui semble idiot jusqu'à ce que vous réalisiez que toute chaîne renvoyée par l'API est à une capture d'écran de devenir une cible.

Le résumé honnête

Le schéma du proxy n'est pas excitant. Il n'y a pas d'algorithmes ingénieux dedans. C'est juste le minimum requis pour exécuter une API IA payante sur l'internet public sans perdre d'argent face à des gens qui n'ont pas payé.

Mais c'est le minimum requis. Chaque développeur indépendant que je connais et qui a lancé sans cela a appris la leçon de la même manière : une alerte de facturation à une heure inopportune, une révocation paniquée de la clé, et une longue soirée à réécrire le flux de données. Moins cher de le faire dès le premier jour.

À lire ensuite

— Lena

Lena Hoffmann

Lena Hoffmann