Claude Code via Claudish

Navigation : Index

Module : Vibe-Coding / Claudish / Notebooks Niveau : Debutant Duree : 25 min Prerequis : Python 3.10+, httpx>=0.25, acces au proxy Claudish (ANTHROPIC_AUTH_TOKEN)

Objectifs d’apprentissage


Pourquoi ce notebook ?

Claudish est le proxy multi-provider du cluster MyIA : il rend Claude Code, Roo Code, les bots (Hermes, NanoClaw) agnostiques du fournisseur. Un agent qui croit parler a Anthropic peut en realite interroger GLM (z.ai), Qwen (vLLM local), ou Anthropic natif, sans changer une ligne de son code.

Le detail du deploiement (wire, topologie, router 3 tiers, fork MyIA) est documente dans ../docs/Claudish-Proxy.md. Ce notebook est le pendant pratique : il montre comment appeler Claudish depuis Python, sans passer par le CLI Claude Code.

Limite assumee : pour executer les cellules d’appel direct, il faut une cle Claudish (ANTHROPIC_AUTH_TOKEN). Si la cle manque, le notebook continue a tourner : les cellules de demonstration visuelle (wire, headers, JSON) s’executent quand même, seules les cellules d’appel reel leveront une RuntimeError claire.

1. Le wire Anthropic que Claudish expose

Claudish se place en frontal d’un client qui parle le wire Anthropic (Claude Code, le SDK, les bots Hermes/NanoClaw). Cote client, rien ne change : on envoie une requête HTTP standard sur /v1/messages avec les mêmes headers que ceux d’api.anthropic.com.

Anatomie d’une requête

POST /v1/messages HTTP/1.1
Host: models.myia.io            (ou http://localhost:3000 en dev)
x-api-key: <cle claudish>
anthropic-version: 2023-06-01
content-type: application/json

{
  "model": "glm-5.2",                 # tier Sonnet -> route vers z.ai GLM
  "max_tokens": 256,
  "messages": [
    {"rôle": "user", "content": "Explique le no-fallback en 2 phrases."}
  ]
}

Reponse (même format qu’Anthropic natif)

{
  "id": "msg_01abc...",
  "type": "message",
  "rôle": "assistant",
  "content": [
    {"type": "text", "text": "Le no-fallback signifie ..."}
  ],
  "model": "glm-5.2",
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 23, "output_tokens": 87}
}

Le client ne sait pas que la reponse vient de GLM plutot que d’Anthropic : le wire est identique. C’est ca, le principe du proxy.

2. Les 3 tiers budgetes (router no-fallback)

Au lieu de laisser le proxy basculer silencieusement entre providers (ce qui degrade qualite et cout en plein milieu d’une conversation), chaque tier a un provider unique budgete :

Tier Modèle visible Provider reel Usage typique
Opus claude-opus-4-8 Anthropic natif Raisonnement lourd, architecture
Sonnet glm-5.2 z.ai GLM Coding Plan Code au quotidien, defaut
Haiku qwen3.6-35b-a3b vLLM self-heberge (po-2023) Tâches rapides, illimite

Principe no-fallback : sur un rate-limit, Claudish backoff puis reessaie le même provider. Sur une panne franche, il fail-hard (erreur explicite). Mieux vaut une erreur visible qu’une derive cachee de qualite. Voir docs/Claudish-Proxy.md §3 pour le detail.

3. Setup : importer le client et verifier la connexion

Le module helpers/claudish_client.py encapsule le wire Anthropic dans une API Python simple : chat(), stream_chat(), list_models(). Pas besoin de manipuler httpx directement.

Configuration requise

Deux variables d’environnement :

  • ANTHROPIC_AUTH_TOKEN : la cle Claudish (recuperee depuis .secrets/master.env, jamais de fallback inline).
  • CLAUDISH_BASE_URL (optionnel) : URL du proxy. Par defaut http://localhost:3000, ou https://models.myia.io en prod.

Voir ../configs/claudish.env.secrets.example pour le template complet.

# Configuration : localiser le dossier helpers/ parmi plusieurs candidats.
# Le cwd peut etre n'importe lequel selon le lanceur (nbclient, jupyter-lab, papermill, ...).
import sys
import os

# On essaie plusieurs emplacements relatifs au cwd courant.
# Le notebook est dans Claudish/notebooks/, et helpers/ est son voisin direct.
candidates = [
    # Cas 1 : cwd = notebooks/
    'helpers',
    # Cas 2 : cwd = Claudish/
    os.path.join('notebooks', 'helpers'),
    # Cas 3 : cwd = Vibe-Coding/
    os.path.join('Claudish', 'notebooks', 'helpers'),
    # Cas 4 : cwd = GenAI/
    os.path.join('Vibe-Coding', 'Claudish', 'notebooks', 'helpers'),
    # Cas 5 : cwd = racine du repo (worktree root)
    os.path.join('MyIA.AI.Notebooks', 'GenAI', 'Vibe-Coding', 'Claudish', 'notebooks', 'helpers'),
]

helpers_path = None
for c in candidates:
    abs_c = os.path.abspath(c)
    if os.path.isdir(abs_c) and os.path.isfile(os.path.join(abs_c, 'claudish_client.py')):
        helpers_path = abs_c
        break

if helpers_path is None:
    raise FileNotFoundError(
        f"Dossier helpers/ introuvable. cwd={os.getcwd()}. "
        f"Candidats testes : {candidates}. "
        f"Lancer le notebook depuis son propre dossier ou depuis la racine du repo."
    )

if helpers_path not in sys.path:
    sys.path.insert(0, helpers_path)

from claudish_client import (
    chat,
    stream_chat,
    list_models,
    get_endpoint,
    get_api_key,
    KNOWN_MODELS,
    DEFAULT_BASE_URL,
)

print("Module claudish_client charge OK")
print(f"   helpers_path : {helpers_path}")
print(f"   Endpoint par defaut : {DEFAULT_BASE_URL}")
print(f"   Cle ANTHROPIC_AUTH_TOKEN : {'definie' if get_api_key() else 'MANQUANTE'}")
print(f"   Modeles connus : {len(KNOWN_MODELS)} declares")
Module claudish_client charge OK
   helpers_path : D:\Dev\CoursIA\.claude\worktrees\claudish-expansion\MyIA.AI.Notebooks\GenAI\Vibe-Coding\Claudish\notebooks\helpers
   Endpoint par defaut : http://localhost:3000
   Cle ANTHROPIC_AUTH_TOKEN : MANQUANTE
   Modeles connus : 6 declares
# Lister les modeles exposes par Claudish (endpoint public /v1/models, pas de cle requise)
try:
    models = list_models()
    print(f"Claudish expose {len(models)} modele(s) :\n")
    for m in models:
        print(f"   - {m.get('id'):24s}  display={m.get('display_name', '?')}")
except Exception as e:
    print(f"Impossible de lister les modeles : {e}")
    print("   Verifier que Claudish est lance (docker ps | grep claudish)")
Claudish expose 5 modele(s) :

   - glm-5.2                   display=glm-5.2
   - glm-5.1                   display=glm-5.1
   - glm-4.7                   display=glm-4.7
   - qwen3.6-35b-a3b           display=qwen3.6-35b-a3b
   - MiniMax-M3                display=MiniMax-M3

Interpretation : la liste ci-dessus reflete exactement le modelMap actif du profil Claudish. Si tu vois glm-5.2 mais pas claude-opus-4-8, c’est que le profil en cours est configure pour le tier Sonnet uniquement (voir docs/Claudish-Proxy.md §6 pour les profils).

4. Premier appel : wire Anthropic en Python

Voici un exemple resolu qui appelle Claudish via chat(). Le modèle par defaut est glm-5.2 (tier Sonnet -> z.ai GLM). Si la cle manque, une RuntimeError explicite est levee.

Exemple guide : question simple sur Sonnet

# Exemple resolu : appel Sonnet (glm-5.2 -> z.ai GLM)
prompt = "En une phrase, qu'est-ce que le wire Anthropic ?"

try:
    reponse = chat(prompt, model="glm-5.2", max_tokens=128)
    print(f"Reponse (tier Sonnet, provider reel : z.ai GLM) :\n  {reponse}")
except RuntimeError as e:
    print(f"Appel impossible : {e}")
    print("   -> definir ANTHROPIC_AUTH_TOKEN puis relancer la cellule")
Appel impossible : Cle Claudish manquante : definir ANTHROPIC_AUTH_TOKEN (voir configs/claudish.env.secrets.example).
   -> definir ANTHROPIC_AUTH_TOKEN puis relancer la cellule

Interpretation : cet appel a traverse 3 couches : (1) chat() construit la requête wire Anthropic, (2) httpx la POST sur /v1/messages, (3) Claudish la traduit vers le wire z.ai GLM, recupere la reponse, la reformatte en wire Anthropic, et la renvoie. Pour le client, c’est transparent : il a poste et lu au format Anthropic.

5. Brancher Claude Code sur Claudish

Pour brancher le CLI Claude Code sur Claudish (au lieu d’api.anthropic.com), il suffit de 3 variables d’environnement :

$env:ANTHROPIC_BASE_URL   = "https://models.myia.io"   # URL Claudish
$env:ANTHROPIC_AUTH_TOKEN = "<cle claudish>"          # cle d'auth
$env:ANTHROPIC_MODEL      = "glm-5.2"                  # tier par defaut

Puis, dans le même shell :

claude -p "Bonjour, qui es-tu ?"
# -> croit parler a Anthropic, mais passe en realite par GLM (tier Sonnet)

Aucun patch de code cote Claude Code : il suit son ANTHROPIC_BASE_URL et envoie son wire standard. C’est le point cle de Claudish : rendre les clients Anthropic-compatibles sans toucher a leur code.

Exemple guide : voir les env vars dans le shell

# Verifier quelles env vars sont deja positionnees (NE PAS afficher les valeurs completes)
from urllib.parse import urlparse

for var in ("ANTHROPIC_BASE_URL", "CLAUDISH_BASE_URL", "ANTHROPIC_MODEL", "ANTHROPIC_AUTH_TOKEN"):
    val = os.environ.get(var)
    if val is None:
        print(f"  {var:24s} = (non definie)")
    elif "TOKEN" in var or "KEY" in var:
        print(f"  {var:24s} = <masque, {len(val)} caracteres>")
    elif "://" in val:
        # Masquer l hote dans les URLs : evite de fuiter l adresse LAN du gateway Claudish.
        # On garde le port (debug) mais jamais l hote (PII machine).
        u = urlparse(val)
        port = f":{u.port}" if u.port else ""
        print(f"  {var:24s} = {u.scheme}://<hote masque>{port}")
    else:
        print(f"  {var:24s} = {val}")
  ANTHROPIC_BASE_URL       = http://<hote masque>:3000
  CLAUDISH_BASE_URL        = (non definie)
  ANTHROPIC_MODEL          = (non definie)
  ANTHROPIC_AUTH_TOKEN     = <masque, 0 caracteres>

Interpretation : ANTHROPIC_AUTH_TOKEN doit etre presente (même masquee) pour que chat() reussisse. Si elle manque, va voir .secrets/master.env ou demande a l’admin cluster la valeur (placeholders uniquement cote doc).

Le notebook peut continuer sans cle : seules les cellules d’appel reel echoueront avec un message clair.

6. Exercice 1 : composer un appel direct sur /v1/messages

Objectif : implementer call_claudish_raw() qui envoie un prompt et retourne la reponse JSON complete (pas seulement le texte). Cela permet d’inspecter usage, model, stop_reason pour debugger ou facturer.

Indice : tu peux utiliser httpx.Client() directement, ou importer get_endpoint et get_api_key depuis claudish_client et construire le payload a la main.

# Exercice 1 : appel brut, retourne le dict JSON complet
def call_claudish_raw(prompt: str, model: str = "glm-5.2", max_tokens: int = 128):
    """Envoie un prompt a Claudish et retourne la reponse JSON brute.

    Args:
        prompt: Question utilisateur.
        model: Modele (defaut 'glm-5.2', tier Sonnet).
        max_tokens: Limite tokens sortie.

    Returns:
        Le dict JSON de la reponse wire Anthropic (champs id, model,
        content, usage, stop_reason, ...).
    """
    # TODO etudiant : construire headers + payload, POST sur /v1/messages, retourner r.json()
    # Indice 1 : utiliser get_endpoint() et get_api_key() du module claudish_client
    # Indice 2 : headers = {"x-api-key": ..., "anthropic-version": "2023-06-01", "content-type": "application/json"}
    # Indice 3 : payload = {"model": model, "max_tokens": max_tokens, "messages": [{"role": "user", "content": prompt}]}
    # Etape 1 : verifier que la cle est presente, sinon lever RuntimeError explicite
    # Etape 2 : POST httpx avec timeout=30s
    # Etape 3 : si status >= 400, lever RuntimeError avec le body
    # Etape 4 : retourner response.json()
    response_json = None  # TODO etudiant : remplacez par l'implementation
    return response_json


# Test (la fonction retourne None tant que le stub n'est pas complete)
result = call_claudish_raw("Cite un avantage de Claudish.")
if result is None:
    print("Stub non complete : la fonction retourne None. Implementer puis relancer.")
else:
    print(f"Modele utilise : {result.get('model', '?')}")
    print(f"Tokens input : {result.get('usage', {}).get('input_tokens', '?')}")
    print(f"Tokens output : {result.get('usage', {}).get('output_tokens', '?')}")
    print(f"Stop reason : {result.get('stop_reason', '?')}")
    content = result.get('content', [])
    if content and content[0].get('type') == 'text':
        print(f"Texte : {content[0]['text'][:200]}")
Stub non complete : la fonction retourne None. Implementer puis relancer.

7. Exercice 2 : comparer 3 tiers sur une même question

Objectif : poser la même question aux 3 tiers (Opus, Sonnet, Haiku) et comparer les reponses. Cela permet de sentir le compromis qualite/cout/vitesse en condition reelle.

Note : claude-opus-4-8 et qwen3.6-35b-a3b ne sont visibles que si ton profil Claudish les expose. Si un tier renvoie 404, note-le dans la sortie (pas un bug du client).

# Exercice 2 : comparaison 3 tiers
TIERS = {
    "Opus":   "claude-opus-4-8",
    "Sonnet": "glm-5.2",
    "Haiku":  "qwen3.6-35b-a3b",
}

def compare_tiers(question: str, max_tokens: int = 128):
    """Pose la meme question aux 3 tiers et collecte les resultats.

    Args:
        question: La question commune.
        max_tokens: Limite par appel.

    Returns:
        Dict {tier_name: {"model": ..., "text": ..., "error": ...}}
    """
    # TODO etudiant : appeler chat() pour chaque tier de TIERS, collecter resultat ou erreur
    # Indice : utiliser un try/except par tier pour ne pas bloquer sur un tier indisponible
    # Etape 1 : initialiser un dict resultats = {}
    # Etape 2 : pour chaque (tier_name, model_id) dans TIERS.items() :
    #          - appeler chat(question, model=model_id, max_tokens=max_tokens)
    #          - stocker {"model": model_id, "text": reponse}
    #          - en cas d'exception RuntimeError, stocker {"model": model_id, "error": str(e)}
    # Etape 3 : retourner le dict
    resultats = None  # TODO etudiant : remplacez par l'implementation
    return resultats


# Test (la fonction retourne None tant que le stub n'est pas complete)
resultats = compare_tiers("Quelle est la capitale de la France ?")
if resultats is None:
    print("Stub non complete : la fonction retourne None. Implementer puis relancer.")
else:
    for tier, data in resultats.items():
        print(f"\n=== Tier {tier} (modele {data.get('model')}) ===")
        if "error" in data:
            print(f"  Erreur : {data['error'][:200]}")
        else:
            print(f"  Texte : {data.get('text', '')[:200]}")
Stub non complete : la fonction retourne None. Implementer puis relancer.

8. Exercice 3 : distinguer 429 (quota) vs 529 (surcharge)

Objectif : implementer classify_http_error() qui retourne une catégorie sémantique a partir du code HTTP recu de Claudish.

Pourquoi c’est important : un client bien eleve retry sur 529 (surcharge transitoire, ~5 min), mais arrete sur 429 (quota epuise, pas la peine de retenter tout de suite). Confondre les deux = soit gaspiller du quota, soit abandonner une requête qui aurait reussi 30 secondes plus tard.

Voir docs/Claudish-Proxy.md §6 et §7.3 pour le detail du overload->529.

# Exercice 3 : classifier un code HTTP Claudish
def classify_http_error(status_code: int) -> str:
    """Classifie un code HTTP retourne par Claudish.

    Args:
        status_code: Code HTTP (401, 429, 500, 503, 529, ...).

    Returns:
        Une chaine parmi :
        - "auth"          : 401, 403 -> reconfigurer la cle
        - "quota"         : 429 -> quota epuise, NE PAS retry
        - "overload"      : 529 -> surcharge transitoire, RETRY avec backoff
        - "server"        : 500, 502, 504 -> bug cote provider
        - "unavailable"   : 503 -> service down, RETRY long
        - "client"        : 4xx autres -> requete mal formee
        - "ok"            : 2xx
        - "unknown"       : tout le reste
    """
    # TODO etudiant : implementer la classification avec une serie de if/elif
    # Indice : 401/403 -> auth ; 429 -> quota ; 529 -> overload ; 500/502/504 -> server ; 503 -> unavailable ; 4xx autres -> client ; 2xx -> ok
    category = None  # TODO etudiant : remplacez par l'implementation
    return category


# Test avec un echantillon de codes
samples = [200, 401, 403, 429, 500, 502, 503, 504, 529]
for code in samples:
    cat = classify_http_error(code)
    print(f"  HTTP {code:3d} -> {cat if cat else '(stub: None)'}")
  HTTP 200 -> (stub: None)
  HTTP 401 -> (stub: None)
  HTTP 403 -> (stub: None)
  HTTP 429 -> (stub: None)
  HTTP 500 -> (stub: None)
  HTTP 502 -> (stub: None)
  HTTP 503 -> (stub: None)
  HTTP 504 -> (stub: None)
  HTTP 529 -> (stub: None)

9. Resume

Ce que tu as vu

Concept Detail
Wire Anthropic POST /v1/messages + headers x-api-key / anthropic-version + payload JSON standard
3 tiers budgetes Opus = Anthropic, Sonnet = z.ai GLM, Haiku = Qwen vLLM
Brancher Claude Code 3 env vars : ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL
Codes HTTP 429 = quota (stop), 529 = surcharge (retry), 401/403 = auth (fix cle)

Points cles a retenir

  1. Claudish est un proxy, pas une lib : on l’appelle via le wire Anthropic standard, il route vers le bon provider.
  2. Le client ne sait jamais quel provider reel a repondu (Anthropic, GLM, Qwen).
  3. Le modelMap du profil actif determine la visibilite des tiers (/v1/models reflete l’etat reel).
  4. Pas de bascule silencieuse : sur rate-limit, backoff sur le même provider ; sur panne, fail-hard.
  5. 429 vs 529 = quetes sémantiques distinctes, le client doit retry seulement sur 529.

Pour aller plus loin

Retour au sommet