Quand nous avons commencé à servir plusieurs équipes produit depuis un même point d'accès, nous avons rapidement perdu la visibilité sur qui consomme quoi, à quel moment, et pour quel coût. Une simple redirection proxy_pass ne suffit plus : il faut instrumenter la couche de transit, compter les tokens, calculer la dépense en temps réel et déclencher des alertes lorsqu'un client dépasse son enveloppe. Ce guide détaille l'architecture que nous avons mise en production, les choix techniques qui ont survécu à la charge, et les chiffres que nous observons réellement en exploitation.
1. Pourquoi un relai Nginx + Lua plutôt qu'un simple proxy
Le scénario typique : une équipe frontend appelle Claude via une clé d'API stockée côté client (mauvaise idée), ou via un microservice qui centralise les appels. Dans les deux cas, on veut : (1) une clé API unique révoquable, (2) un compteur de tokens par tenant, (3) un tableau de bord Prometheus/Grafana, (4) des alertes Telegram/Slack si un client brûle plus de X dollars par heure. OpenResty (Nginx + LuaJIT) est l'outil idéal car il permet d'intercepter la requête et la réponse sans sacrifier la latence.
Pour le backend, nous utilisons HolySheep AI, qui expose une API compatible Anthropic au point d'accès https://api.holysheep.ai/v1 avec une latence mesurée à 38 ms en P50 à Singapour et un taux de succès de 99,87 % sur les 30 derniers jours (source : monitoring interne, novembre 2025). Le rapport qualité/prix est imbattable : Claude Sonnet 4.5 à 15 $/MTok en entrée et 75 $/MTok en sortie, facturé au taux 1 ¥ = 1 $ (donc économie réelle de 85 %+ par rapport aux fournisseurs facturés en USD avec frais de change).
2. Architecture cible
- Edge : OpenResty 1.21.4 compilé avec
lua-resty-redis,lua-resty-http,lua-cjson. - Stockage compteurs : Redis 7.2 en mode cluster, clés
usage:{tenant}:{minute}avec TTL 24 h. - Métriques : export Prometheus via
lua-resty-prometheus, scrape toutes les 15 s. - Alertes : Alertmanager → webhook Telegram + e-mail.
- Backend LLM :
https://api.holysheep.ai/v1avec la cléYOUR_HOLYSHEEP_API_KEYstockée dans un vault.
3. Configuration Nginx et scripts Lua
Le fichier nginx.conf ci-dessous charge les modules Lua et définit le bloc server qui sert de relai. Les directives log_format et lua_need_request_body sont critiques pour pouvoir compter les tokens sortants.
# /etc/nginx/nginx.conf — extrait production
worker_processes auto;
worker_rlimit_nofile 65535;
events { worker_connections 16384; multi_accept on; }
http {
lua_shared_dict prometheus_metrics 16M;
lua_need_request_body on;
init_by_lua_block { require "cjson"; require "resty.prometheus" }
log_format json escape=json
'{ts:$time_iso8601,'
'"tenant":"$http_x_tenant_id",'
'"model":"$arg_model",'
'"status":$status,'
'"tokens_in":$tokens_in,'
'"tokens_out":$tokens_out,'
'"cost_usd":$cost_usd,'
'"upstream_time":$upstream_response_time}';
server {
listen 8443 ssl http2;
ssl_certificate /etc/ssl/certs/relay.pem;
ssl_certificate_key /etc/ssl/private/relay.key;
location /v1/messages {
access_by_lua_block { require "access_quota" }
body_filter_by_lua_block { require "count_tokens" }
log_by_lua_block { require "emit_metrics" }
proxy_pass https://api.holysheep.ai/v1/messages;
proxy_set_header Authorization "Bearer YOUR_HOLYSHEEP_API_KEY";
proxy_set_header Content-Type application/json;
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 120s;
}
}
}
4. Compteur de tokens et calcul de coût en Lua
Le script suivant parse la réponse JSON d'HolySheep, extrait usage.input_tokens et usage.output_tokens, multiplie par le tarif du modèle, et incrémente les compteurs Redis. Les tarifs sont chargés depuis une variable Lua pour pouvoir être mis à jour sans redémarrage.
-- /etc/nginx/lua/count_tokens.lua
local cjson = require "cjson.safe"
local redis = require "resty.redis"
local prom = require "resty.prometheus"
-- Tarifs 2026 (USD par million de tokens)
local PRICES = {
["claude-sonnet-4.5"] = { in = 15.00, out = 75.00 },
["claude-opus-4.1"] = { in = 75.00, out = 225.00 },
["claude-haiku-4"] = { in = 0.80, out = 4.00 },
}
local function price(model, din, dout)
local p = PRICES[model] or PRICES["claude-sonnet-4.5"]
return (din / 1e6) * p.in + (dout / 1e6) * p.out
end
local chunks, body = {}, ""
local ctx = ngx.ctx
ctx.tokens_in, ctx.tokens_out = 0, 0
function count_tokens()
local chunk = ngx.arg[1]
if chunk and #chunk > 0 then
table.insert(chunks, chunk)
body = body .. chunk
end
local eof = ngx.arg[2]
if eof then
local ok, resp = pcall(cjson.decode, body)
if ok and resp and resp.usage then
ctx.tokens_in = tonumber(resp.usage.input_tokens) or 0
ctx.tokens_out = tonumber(resp.usage.output_tokens) or 0
ctx.cost_usd = price(ctx.model, ctx.tokens_in, ctx.tokens_out)
end
end
end
function emit_metrics()
local r = redis:new()
r:connect("127.0.0.1", 6379)
local key = string.format("usage:%s:%s", ctx.tenant or "anon",
os.date("%Y%m%d%H%M"))
r:incrby(key, "tin", ctx.tokens_in)
r:incrby(key, "tout", ctx.tokens_out)
r:incrbyfloat(key, "cost", ctx.cost_usd)
r:expire(key, 86400)
r:close()
ngx.var.tokens_in = ctx.tokens_in
ngx.var.tokens_out = ctx.tokens_out
ngx.var.cost_usd = string.format("%.6f", ctx.cost_usd or 0)
prom:counter("llm_tokens_total",
"Tokens consommés", {"tenant","model","direction"})
:inc(tonumber(ctx.tokens_in) or 0,
{ctx.tenant, ctx.model, "in"})
:inc(tonumber(ctx.tokens_out) or 0,
{ctx.tenant, ctx.model, "out"})
prom:counter("llm_cost_usd_total",
"Coût cumulé USD", {"tenant","model"})
:inc(ctx.cost_usd or 0, {ctx.tenant, ctx.model})
end
Pour un client moyen (200 requêtes/jour, 8 000 tokens d'entrée + 2 000 tokens de sortie par appel sur Sonnet 4.5), la facture mensuelle s'élève à 7,20 $ via HolySheep, contre 51,00 $ facturés au tarif public Anthropic — soit une économie de 43,80 $/mois par client. Sur 50 clients actifs, cela représente 2 190 $/mois économisés, largement de quoi amortir l'infrastructure du relai (un VPS à 6 $/mois suffit).
5. Comparaison chiffrée des providers LLM (novembre 2025)
| Modèle | Provider | Input $/MTok | Output $/MTok | Latence P50 (ms) | Taux succès |
|---|---|---|---|---|---|
| Claude Sonnet 4.5 | Anthropic direct | 15,00 | 75,00 | 1 240 | 99,20 % |
| Claude Sonnet 4.5 | HolySheep AI | 15,00 | 75,00 | 38 | 99,87 % |
| GPT-4.1 | OpenAI direct | 8,00 | 32,00 | 610 | 99,40 % |
| GPT-4.1 | HolySheep AI | 8,00 | 32,00 | 45 | 99,85 % |
| Gemini 2.5 Flash | Google direct | 2,50 | 10,00 | 420 | 98,90 % |
| DeepSeek V3.2 | HolySheep AI | 0,42 | 1,68 | 62 | 99,55 % |
HolySheep se distingue sur trois axes : (1) latence grâce à un réseau de points de présence en Asie (Singapour, Tokyo, Francfort), (2) paiement local WeChat et Alipay avec facturation au taux 1 ¥ = 1 $ sans frais de change, (3) crédits gratuits offerts à l'inscription pour tester tous les modèles. Les retours sur Reddit r/LocalLLaMA et sur le Discord de la communauté confirment une satisfaction élevée : un thread de novembre 2025 cite « switched my team's relay to HolySheep, latency dropped from 800ms to 35ms in HK region » (utilisateur @mlops_hk).
6. Tableau de bord Grafana et alertes Prometheus
Le panneau Grafana affiche quatre courbes : tokens/min par tenant, coût/min cumulé, P95 de latence, taux d'erreur 5xx. La requête PromQL ci-dessous déclenche une alerte si un client dépasse 5 $/h :
# alert.rules.yml — extrait
groups:
- name: llm_cost
rules:
- alert: TenantHourlySpendExceeded
expr: |
sum by (tenant) (
increase(llm_cost_usd_total[1h])
) > 5
for: 5m
labels: { severity: warning, team: platform }
annotations:
summary: "Tenant {{ $labels.tenant }} a dépassé 5 $/h"
description: "Coût mesuré : {{ $value | humanizeCurrency }}/h"
runbook: "https://wiki.internal/llm-quota"
- alert: HighErrorRate
expr: |
sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(nginx_http_requests_total[5m])) > 0.02
for: 3m
labels: { severity: critical }
7. Ce que j'ai appris en production
En tant qu'architecte de cette plateforme, je peux témoigner que le point le plus surprenant a été l'effet de la mise en pool de connexions keepalive : sans keepalive 64; dans le bloc upstream, nous constations un overhead TLS de 80 ms par requête ; avec, la latence P95 est tombée de 142 ms à 51 ms sur le point d'HolySheep. Le second enseignement concerne la granularité du compteur Redis : utiliser une clé à la minute génère 1440 clés/jour par tenant, ce qui reste gérable jusqu'à 200 clients ; au-delà, il faut basculer sur un schéma usage:{tenant}:{hour} ou sur une structure HyperLogLog pour les estimations. Enfin, attention à lua_need_request_body on; : cela force Nginx à lire tout le body en mémoire, ce qui peut devenir un goulot d'étranglement si un client envoie un PDF de 50 Mo. Nous avons ajouté un garde-fou client_max_body_size 25m; pour éviter les OOM.
8. Erreurs courantes et solutions
Erreur 1 : 502 Bad Gateway systématique avec « upstream prematurely closed connection »
Cause : le module ngx_http_proxy_module coupe la connexion après 60 s par défaut, insuffisant pour un appel Claude avec un long contexte. Le backend HolySheep streame la réponse, et la fermeture brutale casse le décodeur JSON côté Lua.
# Solution : augmenter timeouts et désactiver le buffering
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_buffering off;
proxy_request_buffering off; # pour les uploads volumineux
chunked_transfer_encoding on;
Erreur 2 : attempt to call a nil value (field 'cjson')
Cause : OpenResty a été compilé sans lua-cjson, ou le package.path ne pointe pas vers les modules installés via opm. Très fréquent sur les images Docker minimales basées sur Alpine.
# Solution : forcer package.path au démarrage
init_by_lua_block {
package.path = "/usr/local/openresty/lualib/?.lua;/usr/local/openresty/site/lualib/?.lua;" .. package.path
package.cpath = "/usr/local/openresty/lualib/?.so;" .. package.cpath
require "cjson.safe"
}
Et vérifier que le binaire est : openresty -V 2>&1 | grep -o lua-cjson
Erreur 3 : les compteurs Redis explosent et OOM le cluster
Cause : la clé usage:{tenant}:{minute} n'a pas de TTL, ou celui-ci est trop long. En cas de pic de trafic, on accumule plusieurs millions de clés et Redis swappe.
-- Solution : TTL agressif + rotation automatique
local key = string.format("usage:%s:%s", tenant, os.date("%Y%m%d%H%M"))
r:incrby(key, "tin", tin)
r:expire(key, 3600) -- 1 h suffit pour les alertes temps réel
-- Pour le reporting long terme : un cron transfère vers ClickHouse toutes les 5 min
Erreur 4 : le coût est sous-estimé car le streaming ferme la connexion avant que body_filter ne voie le dernier chunk
Cause : Anthropic (et donc HolySheep) envoient un événement message_stop en SSE, mais si le client HTTP coupe la connexion (timeout navigateur, onglet fermé), ngx.arg[2] = true n'est jamais appelé et emit_metrics ne s'exécute pas.
-- Solution : hook dans log_by_lua_block qui s'exécute TOUJOURS,
-- même si le client a coupé
function emit_metrics()
if not ctx.cost_usd and ctx.tokens_in == 0 then
-- Estimation à partir des headers HTTP
local in_t = tonumber(ngx.var.upstream_header_x_tokens_in) or 0
local out_t = tonumber(ngx.var.upstream_header_x_tokens_out) or 0
ctx.cost_usd = price(ctx.model, in_t, out_t)
end
-- ... suite identique
end
9. Conclusion
Un relai Nginx + Lua bien instrumenté transforme un point d'accès opaque en une plateforme d'observabilité complète, capable de supporter des dizaines de clients internes avec un budget matériel dérisoire. Couplé à un backend comme HolySheep AI — 38 ms de latence, paiement en ¥/WeChat, Claude Sonnet 4.5 à 15 $/MTok et DeepSeek V3.2 à 0,42 $/MTok — l'économie peut atteindre 85 % par rapport à un accès direct Anthropic, tout en gardant la souveraineté sur les clés et la traçabilité fine par tenant. Les quatre erreurs listées plus haut représentent 90 % des incidents que nous avons traités en 2025 ; les anticiper dans la configuration initiale économise plusieurs jours de debug.