Des appels directs aux fournisseurs à api.aicu.ai
Un retour d'expérience sur la migration de deux outils que nous utilisons réellement en interne : notre bot Slack (Aicuty Bot) et notre outil de génération d'articles.
Pourquoi prendre la peine de réécrire
Avant la migration, notre code de génération d'images ressemblait à ceci :
- Le bot Slack détenait une
OPENAI_API_KEYet appelaitapi.openai.comdirectement - L'outil d'articles détenait une
GEMINI_API_KEYet appelaitgenerativelanguage.googleapis.comdirectement - Quand l'un des deux échouait, l'erreur n'atterrissait jamais ailleurs que dans la sortie standard de l'environnement d'exécution ou dans un message Slack
Le vrai problème n'était pas le nombre de clés. C'était que personne ne pouvait dire ce qui était utilisé, ni en quelle quantité. En faisant le compte des journaux de production avant la migration, 1,325 requêtes LLM étaient passées sans une seule entrée dans le registre. Aucune base pour facturer qui que ce soit.
Le principe : un seul point de sortie
api.aicu.ai est compatible OpenAI : il suffit de changer l'endpoint et la clé.
| Avant | Après | |
|---|---|---|
| Endpoint | Un par fournisseur | https://api.aicu.ai/v1 |
| Clés | OPENAI_API_KEY + GEMINI_API_KEY + … | Une seule AICU_API_KEY |
| Choix du modèle | Selon ce que chaque SDK exige | L'argument model |
| Historique d'utilisation | À construire soi-même | Automatique, dans le tableau de bord |
Étape 1 : glisser un client léger devant
Tout réécrire d'un coup n'était pas la solution prudente. Écrire un module léger et l'insérer, si. Il s'active par une variable d'environnement : en cas de problème, vous pouvez revenir immédiatement au chemin d'origine.
# aicu_images.py
import base64, os, requests
def is_enabled() -> bool:
return bool(os.environ.get("AICU_API_KEY"))
def generate(prompt, *, model="gpt-image-2", character=None,
reference_image=None, mask=None, size=None, quality=None,
aspect_ratio=None, timeout=240):
payload = {"model": model, "prompt": prompt}
if character:
payload["character"] = character
if reference_image:
payload["reference_image"] = base64.b64encode(reference_image).decode()
if mask:
payload["mask"] = base64.b64encode(mask).decode()
for k, v in (("size", size), ("quality", quality), ("aspect_ratio", aspect_ratio)):
if v:
payload[k] = v
res = requests.post(
"https://api.aicu.ai/v1/images/generations",
headers={"Authorization": f"Bearer {os.environ['AICU_API_KEY']}"},
json=payload, timeout=timeout,
)
res.raise_for_status()
data = res.json()
return base64.b64decode(data["data"][0]["b64_json"]), bool(data.get("cached"))
Étape 2 : remplacer le point d'appel (avec un repli)
+ if aicu_images.is_enabled():
+ try:
+ image_data, cached = aicu_images.generate(
+ prompt, model="gpt-image-2", character=member,
+ reference_image=ref_bytes, mask=mask_bytes,
+ size="1536x1024", quality="high",
+ )
+ except aicu_images.AicuImageError as exc:
+ print(f"[gpt-image2] AICU gateway failed: {exc}")
+
+ if image_data is None:
oai = OpenAI(api_key=OPENAI_API_KEY)
response = oai.images.edit(model="gpt-image-2", image=..., prompt=prompt)
image_data = base64.b64decode(response.data[0].b64_json)
Tant que vous ne définissez pas AICU_API_KEY, le comportement ne change pas d'un millimètre. Ce n'est qu'une fois la variable définie que les appels passent par la passerelle. Déployer d'abord en production cet état « installé mais inerte » enlève la peur de la bascule.
Étape 3 : quitter Gemini
Le SDK de Gemini a une forme assez différente, mais le remplacement est en fait plus court.
- client = genai.Client(api_key=GEMINI_API_KEY)
- response = client.models.generate_content(
- model="gemini-3.1-flash-image-preview",
- contents=prompt,
- config=types.GenerateContentConfig(
- response_modalities=["IMAGE", "TEXT"],
- image_config=types.ImageConfig(aspect_ratio="16:9"),
- ),
- )
- for part in response.candidates[0].content.parts:
- if part.inline_data:
- output_path.write_bytes(part.inline_data.data)
+ data, cached = aicu_images.generate(
+ prompt, model="nano-banana", aspect_ratio="16:9"
+ )
+ output_path.write_bytes(data)
Vous pouvez passer tel quel le nom de modèle d'origine, cela fonctionne quand même (gemini-3.1-flash-image-preview est résolu comme alias de nano-banana). Faites d'abord tourner le code, raccourcissez le nom ensuite.
Là où nous avons trébuché
Comment passer les images de référence. La première conception vous faisait téléverser l'image dans R2 au préalable — mais les outils à migrer avaient déjà l'image de référence en main. Nous avons donc fait accepter directement du base64 à reference_image. Pas d'enregistrement préalable, et la migration s'est réduite au remplacement d'une seule fonction.
Ne pas abandonner l'édition par masque. Le bot Slack pouvait joindre une image de masque pour modifier une partie d'une image. Comme la passerelle transmet mask, nous avons migré sans supprimer la fonctionnalité. Quand une migration retire des fonctionnalités, les gens qui s'en servent reviennent en arrière.
Scopes des clés. Les clés que nous avions déjà émises ne portaient que ["tts"], si bien que les appels d'images revenaient en 403. Les clés alpha sont désormais émises d'emblée avec llm / tts / images.
Ce que nous y avons gagné
- Les échecs sont visibles. Le tableau de bord indique quel modèle a échoué et combien de fois. Ce qui nous aurait échappé avant — comme « 707 échecs parce qu'OpenRouter n'avait plus de crédit » — apparaît immédiatement.
- Le cache est rentable. Régénérer dans les mêmes conditions renvoie le résultat depuis R2 sans facturation (
cached: true). - Nous connaissons nos coûts. La marge et le seuil de rentabilité par modèle sont calculables : les discussions sur les prix peuvent s'appuyer sur des chiffres.
- Les redevances des personnages peuvent être réparties. → API Personnages
Assistance
- Tableau de bord : https://api.aicu.ai/dashboard
- Historique d'utilisation : https://api.aicu.ai/dashboard/history
© 2026 AICU Inc.