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."}
]
}
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.
# Configuration : localiser le dossier helpers/ parmi plusieurs candidats.# Le cwd peut etre n'importe lequel selon le lanceur (nbclient, jupyter-lab, papermill, ...).import sysimport 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 =Nonefor 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_cbreakif helpers_path isNone:raiseFileNotFoundError(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 notin 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")
# 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', '?')}")exceptExceptionas e:print(f"Impossible de lister les modeles : {e}")print(" Verifier que Claudish est lance (docker ps | grep claudish)")
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}")exceptRuntimeErroras 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 urlparsefor var in ("ANTHROPIC_BASE_URL", "CLAUDISH_BASE_URL", "ANTHROPIC_MODEL", "ANTHROPIC_AUTH_TOKEN"): val = os.environ.get(var)if val isNone: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}")
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 completdef 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'implementationreturn 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 isNone: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 tiersTIERS = {"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'implementationreturn 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 isNone: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 Claudishdef 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'implementationreturn category# Test avec un echantillon de codessamples = [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)'}")