Router

Router — Démarrage rapide

Premier appel en moins de 5 minutes. Compatible drop-in OpenAI : curl, Python, Node, litellm. Couvre aussi streaming, tool calling, JSON mode, vision, idempotency et strict mode.

Router — Démarrage rapide

Router est une passerelle IA distincte — remplace https://api.openai.com par une URL unique qui route vers 40+ modèles (Anthropic, OpenAI, Google, Cohere, Mistral, open-source). Guard anti-exfiltration actif par défaut, facturation CAD, fallback automatique, streaming SSE, tool calling, vision — tout OpenAI-compatible.

1. Obtenir une clé API

  1. Connectez-vous à meridian.signalorange.ca/login
  2. Paramètres → Clés API → Nouvelle clé
  3. Donnez-lui un nom (production, staging, local-dev)
  4. Copiez la clé immédiatement — elle ne sera plus affichée
export MERIDIAN_API_KEY="sk-mer-..."

Besoin de gérer les clés programmatiquement (CI, rotation) ? Voir Router — API keys self-service.

2. Premier appel — curl

curl https://router.signalorange.ca/api/v1/chat/completions \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "auto",
    "messages": [
      {"role": "user", "content": "Bonjour, présente-toi en une phrase."}
    ]
  }'

Réponse :

{
  "id": "chatcmpl-abc",
  "object": "chat.completion",
  "choices": [
    {"index": 0, "message": {"role": "assistant", "content": "Bonjour ! ..."}, "finish_reason": "stop"}
  ],
  "model": "anthropic/claude-sonnet-4",
  "usage": {"total_tokens": 142},
  "billing": {"cost_cad_cents": 1},
  "guard": {"detections_count": 0, "request_id": "chatcmpl-abc"}
}

Le champ guard.request_id peut être utilisé pour inspecter ce que Guard a filtré via l'API d'audit.

3. Python (SDK OpenAI drop-in)

from openai import OpenAI

client = OpenAI(
    base_url="https://router.signalorange.ca/api/v1",
    api_key=os.environ["MERIDIAN_API_KEY"],
)

response = client.chat.completions.create(
    model="auto",
    messages=[{"role": "user", "content": "Bonjour"}],
)

print(response.choices[0].message.content)

client.models.list() fonctionne aussi — Router expose /api/v1/models au format OpenAI.

4. Node.js / TypeScript

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://router.signalorange.ca/api/v1",
  apiKey: process.env.MERIDIAN_API_KEY,
});

const response = await client.chat.completions.create({
  model: "auto",
  messages: [{ role: "user", content: "Bonjour" }],
});

5. litellm

import litellm

response = litellm.completion(
    model="openai/auto",
    messages=[{"role": "user", "content": "Bonjour"}],
    api_base="https://router.signalorange.ca/api/v1",
    api_key=os.environ["MERIDIAN_API_KEY"],
)

Streaming (SSE)

Ajoutez stream: true pour recevoir la réponse en chunks OpenAI-compatibles :

stream = client.chat.completions.create(
    model="auto",
    messages=[{"role": "user", "content": "Raconte-moi une histoire."}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

Raw SSE (pour debug) :

curl https://router.signalorange.ca/api/v1/chat/completions \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -N \
  -d '{"model":"auto","messages":[{"role":"user","content":"Hi"}],"stream":true}'

Format renvoyé :

data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":" there"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]

Tool calling / function calling

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Returns current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]
        }
    }
}]

response = client.chat.completions.create(
    model="claude-sonnet-4",
    messages=[{"role": "user", "content": "Weather in Montréal?"}],
    tools=tools,
    tool_choice="auto",
)

# If the model decides to call the tool:
if response.choices[0].finish_reason == "tool_calls":
    for call in response.choices[0].message.tool_calls:
        print(call.function.name, call.function.arguments)

Supporte tool_choice: "none" | "auto" | "required" | {"type": "function", "function": {"name": "..."}}.

JSON mode

Force le modèle à retourner du JSON valide :

{
  "model": "auto",
  "messages": [
    {"role": "system", "content": "Tu renvoies UNIQUEMENT du JSON valide."},
    {"role": "user", "content": "Extrais le nom et la ville: Alice vit à Québec."}
  ],
  "response_format": {"type": "json_object"}
}

La réponse content sera un document JSON parsable.

Structured outputs (json_schema)

Pour exiger une forme précise, pas seulement du JSON valide :

{
  "model": "auto",
  "messages": [
    {"role": "user", "content": "Extrais le nom et la ville: Alice vit à Québec."}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "personne",
      "strict": true,
      "schema": {
        "type": "object",
        "required": ["nom", "ville"],
        "properties": {
          "nom": {"type": "string"},
          "ville": {"type": "string"}
        },
        "additionalProperties": false
      }
    }
  }
}

Comportement :

  • Modèles OpenAI / Anthropic : schéma imposé par le fournisseur (natif).
  • Autres modèles : schéma transmis tel quel + sortie validée par Router. Si la sortie n'est pas du JSON valide ou ne respecte pas le schéma → HTTP 422 schema_validation_failed (extrait brut en error.raw_preview, aucune facturation — la réservation est libérée). La sortie n'est jamais corrigée silencieusement.
  • GET /api/v1/models expose supported_parameters par modèle (support natif annoncé par le catalogue amont).

Outils serveur (web_search / fetch)

{"type": "web_search"} et {"type": "fetch"} dans tools sont exécutés par Router pendant la conversation (pas de clé cliente requise) :

{
  "model": "auto",
  "messages": [{"role": "user", "content": "Quel est le taux directeur actuel ? Vérifie en ligne."}],
  "tools": [{"type": "web_search"}, {"type": "fetch"}]
}

Router injecte les définitions appelables, exécute les tool_calls du modèle (recherche puis lecture de pages, jusqu'à 2 tours), puis répond avec tool_executions: [{name, arguments, result}] — result reprend le format exact des endpoints POST /api/v1/tools/web_search et POST /api/v1/tools/fetch, texte intégral inclus. Chaque tour de modèle est facturé normalement en plus du coût fixe de l'outil.

Vision multimodal

Passez un tableau de parts comme content :

response = client.chat.completions.create(
    model="gpt-5",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "Décris cette image en 2 phrases."},
            {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
        ]
    }],
)

Guard redacte les parts text (par exemple si le texte contient un NAS) mais laisse passer les parts image_url telles quelles. Les modèles vision (GPT-5, Claude Opus, Gemini 2.5 Pro) voient l'image directement.

Idempotency-Key

Retry-safe : si vous renvoyez la même requête avec le même header Idempotency-Key, Router retourne la réponse en cache sans ré-appeler le modèle :

curl https://router.signalorange.ca/api/v1/chat/completions \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Idempotency-Key: req_2026-04-21-doc-42" \
  -H "Content-Type: application/json" \
  -d '{"model":"auto","messages":[...]}'
  • Clé unique par requête (UUID recommandé)
  • Fenêtre de 24h
  • Si vous réutilisez la clé avec un body différent → HTTP 422 idempotency_conflict
  • Retournée avec header Idempotent-Replayed: true
  • Pas supporté avec stream: true (les streams ne se rejouent pas)

Strict mode (désactiver fallback)

Par défaut, si le modèle demandé tombe, Router bascule automatiquement vers un modèle compatible. Avec strict: true, Router n'essaie que le modèle demandé :

{
  "model": "anthropic/claude-opus-4-7",
  "strict": true,
  "messages": [...]
}

Si Claude Opus n'est pas disponible → HTTP 502 provider_error, pas de fallback silencieux. Utile pour :

  • A/B tests où la métrique exige le modèle exact
  • Compliance qui impose un modèle contractuel
  • Debug quand vous voulez un échec explicite

Preflight — estimer le coût avant l'appel

curl https://router.signalorange.ca/api/v1/chat/completions/estimate \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"auto","messages":[...],"max_tokens":500}'

Réponse :

{
  "model": "anthropic/claude-sonnet-4",
  "estimated_prompt_tokens": 142,
  "max_completion_tokens": 500,
  "cost_usd_cents": 62,
  "cost_cad_cents": 96,
  "exchange_rate": 1.35,
  "disclaimer": "Estimation based on character-count/4 heuristic..."
}

Aucun appel upstream, aucun token consommé. Voir Router — Facturation pour les détails.

Et après ?