API
Les clés d'API de l'espace et les routes pour appeler ses profils LLM, au format OpenAI.
Chaque espace ERHA expose une API HTTP pour appeler ses profils LLM depuis n'importe quel système. Le format est celui des chat completions d'OpenAI : le champ model désigne un profil de l'espace, jamais un fournisseur ni un modèle précis. Tout client compatible OpenAI fonctionne tel quel.
Clés d'API
Les clés se créent dans le module API de l'espace, réservé aux administrateurs. Chaque clé a un nom, et sa propre consommation, mesurée à l'appel et détaillée sur la page de la clé. Le secret, préfixé erha_sk_, n'est affiché qu'une seule fois, à la création. Une clé révoquée est refusée immédiatement.
Restrictions par clé
Chaque clé peut porter deux listes, modifiables depuis le module API (bouton « Restreindre »). Une liste vide ne restreint rien ; les deux s'appliquent ensemble.
| Liste | Ce qui est vérifié | Ce que ça protège |
|---|---|---|
| Adresses IP | L'adresse du client (IPv4, IPv6 ou plage CIDR). | Une clé qui fuit ne sert à rien hors des serveurs listés. C'est la restriction à poser sur toute intégration serveur à serveur. |
| Domaines | L'hôte de l'en-tête Origin (Referer à défaut), *. pour les sous-domaines. | Une clé utilisée dans une page web ne peut pas être rejouée depuis un autre site : le navigateur fixe lui-même cet en-tête. |
La restriction par domaine ne protège pas contre un script, curl ou Postman : hors navigateur, l'appelant écrit l'en-tête Origin qu'il veut. Une clé destinée à un serveur se restreint par adresse IP.
Un appel refusé répond 403 avec le type permission_error et nomme l'adresse ou le domaine refusé, pour que l'intégrateur sache quoi ajouter à la liste.
Dossiers de consommation
Un appel peut nommer un dossier : un module ou une fonctionnalité de votre produit. Sa consommation est alors comptée à part, sur la même clé. Une seule clé suffit pour savoir ce que consomme chaque partie de votre produit.
Le dossier se donne par l'en-tête X-Erha-Folder ou par le paramètre de requête folder.
curl https://api.erha.ai/v1/chat/completions \
-H "Authorization: Bearer erha_sk_..." \
-H "Content-Type: application/json" \
-H "X-Erha-Folder: facturation" \
-d '{ "model": "reecriture", "messages": [{ "role": "user", "content": "Bonjour" }] }'
curl "https://api.erha.ai/v1/chat/completions?folder=facturation" ...
Avec un SDK OpenAI, l'en-tête est le plus simple : il se pose une fois sur le client (voir SDK compatibles OpenAI).
| Règle | Détail |
|---|---|
| Format | Lettres minuscules, chiffres, -, _ et ., 64 caractères au plus. Le nom commence et finit par une lettre ou un chiffre. Exemples : facturation, crm.relances, export_pdf. |
| Majuscules | Ramenées en minuscules : Facturation et facturation sont le même dossier. |
| En-tête et paramètre | Si les deux sont donnés, ils doivent nommer le même dossier. |
| Sans dossier | L'appel est compté sur la clé, dans la ligne « Sans dossier ». |
La consommation de chaque dossier s'affiche dans le module API, dans le détail de la clé : appels, tokens et coût du mois. Un clic sur un dossier filtre l'historique des appels. Le dossier ne change pas la facture : l'appel est facturé sur la clé, comme sans dossier.
Règles de dossier par clé
Chaque clé peut porter deux réglages, modifiables depuis le module API (menu de la clé, « Dossiers »).
| Réglage | Effet |
|---|---|
| Dossiers autorisés | Les seuls dossiers acceptés. Un appel qui en nomme un autre est refusé. Une liste vide accepte tout dossier bien formé. 50 dossiers au plus. |
| Dossier obligatoire | Un appel qui ne nomme aucun dossier est refusé. |
Un appel refusé répond 400 avec le type invalid_request_error. Le message dit quelle règle bloque et, pour un dossier hors liste, donne les dossiers autorisés. Rien n'est consommé.
La liste des dossiers autorisés n'est pas une protection : qui détient la clé peut nommer n'importe quel dossier de la liste. Elle garde des noms stables, pour qu'une faute de frappe soit refusée au lieu d'ouvrir une nouvelle ligne dans le suivi. Pour séparer des droits, par exemple un module qui ne doit pas consommer au nom d'un autre, créez une clé par module.
Authentification
Toutes les requêtes portent la clé dans l'en-tête Authorization.
Authorization: Bearer erha_sk_...
URL de base
Les routes sont servies par l'API, sous /v1 :
https://api.erha.ai/v1
L'adresse console.erha.ai/api/v1 répond encore, relayée vers l'API, pour les intégrations déjà en place. Les nouvelles intégrations utilisent l'adresse de l'API.
Routes
| Méthode | Route | Rôle |
|---|---|---|
GET | /v1/models | Les profils que la clé peut appeler. |
POST | /v1/chat/completions | Un appel LLM sur un profil, au format chat completions. |
Lister les modèles
GET /v1/models renvoie les profils que la clé peut appeler. Ce sont les valeurs acceptées dans le champ model de POST /v1/chat/completions.
curl https://api.erha.ai/v1/models \
-H "Authorization: Bearer erha_sk_..."
{
"object": "list",
"data": [
{ "id": "reecriture", "object": "model", "owned_by": "erha" },
{ "id": "synthese", "object": "model", "owned_by": "erha" }
]
}
Chat completions
POST /v1/chat/completions exécute un appel LLM sur un profil de l'espace. L'appel est mesuré et facturé sur la clé utilisée, et sur le dossier qu'il nomme (voir Dossiers de consommation).
Un appel dispose de 280 secondes. Au-delà, l'API répond 504 et rien n'est facturé : réessayez, ou réduisez la taille de la requête.
| Champ | Type | Description |
|---|---|---|
model (requis) | string | Un profil de l'espace (voir GET /v1/models). |
messages (requis) | array | Tableau non vide de messages {role, content}. Les rôles system, user, assistant et tool sont acceptés ; les messages system sont concaténés en instruction système. Le content d'un message user accepte aussi un tableau de parties (voir Pièces jointes). |
stream | boolean | Renvoie un flux SSE au format OpenAI. Le contenu arrive en un seul chunk : c'est le protocole qui est supporté, pas la progressivité. |
tools, tool_choice | Voir Tools. |
curl https://api.erha.ai/v1/chat/completions \
-H "Authorization: Bearer erha_sk_..." \
-H "Content-Type: application/json" \
-d '{
"model": "reecriture",
"messages": [
{ "role": "system", "content": "Réponds en français." },
{ "role": "user", "content": "Bonjour" }
]
}'
La réponse suit le format chat.completion, avec l'usage en tokens de l'appel.
{
"id": "erha-5f0c…",
"object": "chat.completion",
"created": 1755500000,
"model": "reecriture",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Bonjour !" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 18,
"completion_tokens": 5,
"total_tokens": 23
}
}
Pièces jointes
À la place d'une chaîne, le champ content d'un message user accepte un tableau de parties : c'est ainsi qu'on joint une image ou un document. Les autres rôles restent en texte.
| Type | Champ | Description |
|---|---|---|
text | text | Texte simple. |
image_url | image_url.url | Image à analyser. |
file | file.file_data | Document à analyser. Un PDF passe ici, jamais en image_url. |
Formats acceptés : image/jpeg, image/png, image/gif et image/webp pour les images, application/pdf pour les documents. Tout autre type est refusé en 400, en nommant la partie fautive.
Les pièces jointes voyagent en base64 dans une URL data:. Contrairement à l'API OpenAI, une URL distante (http ou https) est refusée : l'API ne va jamais chercher un fichier sur un hôte désigné par le prompt.
curl https://api.erha.ai/v1/chat/completions \
-H "Authorization: Bearer erha_sk_..." \
-H "Content-Type: application/json" \
-d '{
"model": "synthese",
"messages": [
{
"role": "user",
"content": [
{
"type": "file",
"file": { "file_data": "data:application/pdf;base64,JVBERi0xLjQK..." }
},
{ "type": "text", "text": "Résume ce document." }
]
}
]
}'
{
"type": "image_url",
"image_url": { "url": "data:image/png;base64,iVBORw0KGgo..." }
}
Les documents demandent un profil adossé à Anthropic. Sur un profil OpenAI-compatible, une image passe, mais un document est refusé explicitement plutôt qu'envoyé sans être lu.
Tools
Le format OpenAI est accepté tel quel : tools, tool_choice, et le rôle tool pour renvoyer un résultat. Quand le modèle demande un appel, la réponse porte tool_calls et finish_reason vaut tool_calls.
{
"model": "reecriture",
"messages": [{ "role": "user", "content": "Quel temps fait-il à Paris ?" }],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}
Renvoyez le résultat dans un message de rôle tool portant le tool_call_id reçu, puis rappelez la route avec l'historique complet.
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "18 °C, ensoleillé"
}
Indisponible sur les profils adossés à Anthropic : l'appel est refusé plutôt que servi sans ses tools. Utilisez un profil OpenAI-compatible.
Erreurs
Les erreurs suivent le format OpenAI : un objet error avec un message et un type.
{
"error": {
"message": "Invalid or revoked API key.",
"type": "authentication_error"
}
}
| Code | Type | Signification |
|---|---|---|
| 400 | invalid_request_error | Requête invalide : corps non JSON, model ou messages manquant, partie de contenu refusée (type inconnu, media type non supporté, URL distante), ou dossier refusé (mal formé, absent alors que la clé l'exige, hors de la liste de la clé). |
| 401 | authentication_error | Clé absente, mal formée, invalide ou révoquée. |
| 402 | billing_error | L'espace ne peut pas être facturé : aucun moyen de paiement, ou compte de facturation résilié. Rien n'est consommé ni mesuré. |
| 403 | permission_error | Clé restreinte : l'adresse IP ou le domaine de l'appel n'est pas dans ses listes. |
| 404 | invalid_request_error | Profil inconnu pour l'espace. |
| 429 | rate_limit_error | Plus de 300 appels par minute sur la clé. |
| 502 | api_error | Erreur en aval pendant l'exécution de l'appel. |
| 503 | gateway_unavailable | La passerelle est momentanément indisponible. Rien n'a été consommé : réessayez. |
| 504 | timeout_error | Le modèle n'a pas répondu en 280 secondes. Rien n'est facturé : réessayez, ou réduisez la taille de la requête. |
SDK compatibles OpenAI
Tout client compatible OpenAI fonctionne : pointez baseURL vers l'API et utilisez un profil comme modèle. defaultHeaders (default_headers en Python) est facultatif : il range toute la consommation du client dans un dossier (voir Dossiers de consommation).
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "https://api.erha.ai/v1",
apiKey: "erha_sk_...",
defaultHeaders: { "X-Erha-Folder": "facturation" },
})
const completion = await client.chat.completions.create({
model: "reecriture",
messages: [{ role: "user", content: "Bonjour" }],
})
from openai import OpenAI
client = OpenAI(
base_url="https://api.erha.ai/v1",
api_key="erha_sk_...",
default_headers={"X-Erha-Folder": "facturation"},
)
completion = client.chat.completions.create(
model="reecriture",
messages=[{"role": "user", "content": "Bonjour"}],
)