From fb193d6b635d139cc06946f05d99ff24eb38b0e7 Mon Sep 17 00:00:00 2001 From: Xavier Date: Mon, 5 Oct 2026 08:59:21 +0200 Subject: [PATCH] =?UTF-8?q?MAJ=20MAIN.PY=20avec=20les=20diff=C3=A9rent=20m?= =?UTF-8?q?od=C3=A8les=20dispo=20sur=20openrouter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index 6bd884f..efab95b 100644 --- a/main.py +++ b/main.py @@ -5,23 +5,93 @@ from dotenv import load_dotenv load_dotenv() -# En-têtes et CSS pour le style AETHAS38 +# --- 1. GESTION DES MODELES (Mise en cache au démarrage) --- +MODELS_DATA = [] + +def init_models(): + """Télécharge la liste complète au lancement du serveur et la stocke en mémoire.""" + global MODELS_DATA + try: + res = requests.get("https://openrouter.ai/api/v1/models", timeout=5) + if res.status_code == 200: + MODELS_DATA = res.json().get("data", []) + except: + pass + + if not MODELS_DATA: # Sécurité si le réseau coupe au démarrage + MODELS_DATA = [ + {"id": "meta-llama/llama-3.1-8b-instruct:free", "name": "Llama 3.1 8B", "pricing": {"prompt": "0", "completion": "0"}}, + {"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "pricing": {"prompt": "1", "completion": "1"}} + ] + +init_models() + +def check_if_free(model): + """Vérifie si le modèle est gratuit en analysant ses tarifs OpenRouter.""" + if "free" in model.get("id", "").lower(): + return True + try: + pricing = model.get("pricing", {}) + prompt = float(pricing.get("prompt", -1)) + completion = float(pricing.get("completion", -1)) + # Si le prix de lecture et d'écriture est à 0, c'est 100% gratuit + if prompt == 0.0 and completion == 0.0: + return True + except: + pass + return False + +def get_model_options(filter_type="all"): + """Génère la liste déroulante des modèles avec leur étiquette, selon le filtre.""" + options = [] + for m in sorted(MODELS_DATA, key=lambda x: x['name']): + is_free = check_if_free(m) + + # Application du filtre (Tous / Gratuits / Payants) + if filter_type == "free" and not is_free: continue + if filter_type == "paid" and is_free: continue + + # Ajout de l'étiquette + tag = "Gratuit" if is_free else "Payant" + label = f"{m['name']} ({tag})" + + options.append(Option(label, value=m['id'])) + + return Select(*options, name="model_id", cls="model-select", style="width: 100%;") + + +# --- 2. GESTION DU BUDGET --- +def get_budget(): + mgmt_key = os.getenv("OPENROUTER_MANAGEMENT_KEY") + if not mgmt_key: return "Clé manquante" + try: + res = requests.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {mgmt_key}"}, timeout=5) + if res.status_code == 200: + data = res.json().get("data", {}) + return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $" + except: + pass + return "Erreur lecture" + + +# --- 3. INTERFACE UTILISATEUR (UI) --- theme_hdrs = [ Script(src="https://cdn.tailwindcss.com"), Style(""" body { background-color: #0f172a; color: #f8fafc; font-family: system-ui, sans-serif; } - .sidebar { background-color: #1e293b; border-right: 1px solid #333; height: 100vh; padding: 20px; } + .sidebar { background-color: #1e293b; border-right: 1px solid #333; height: 100vh; padding: 20px; display: flex; flex-direction: column;} .main-content { padding: 20px; height: 100vh; display: flex; flex-direction: column; } .chat-container { flex-grow: 1; overflow-y: auto; margin-bottom: 20px; border: 1px solid #333; padding: 15px; border-radius: 8px; background: #151e2e; } - .input-row { display: flex; gap: 10px; align-items: flex-end; } + .input-row { display: flex; gap: 10px; align-items: flex-end; width: 100%; } .input-box { flex-grow: 1; padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; outline: none; } + .model-select { padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: #00e5ff; outline: none; font-weight: bold; cursor: pointer;} .send-btn { padding: 12px 24px; border-radius: 5px; background: #00e5ff; color: #0f172a; font-weight: bold; cursor: pointer; border: none; transition: 0.2s; } .send-btn:hover { background: #38bdf8; } - /* Style des messages */ .msg-user { color: #00e5ff; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;} .msg-ia { color: #a855f7; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;} .bubble-user { background: #1e293b; padding: 10px; border-radius: 5px; display: inline-block; color: white; } - .bubble-ia { background: #2a2a35; padding: 10px; border-radius: 5px; display: inline-block; color: #e2e8f0; border-left: 3px solid #a855f7; } + .bubble-ia { background: #2a2a35; padding: 10px; border-radius: 5px; display: inline-block; color: #e2e8f0; border-left: 3px solid #a855f7; white-space: pre-wrap; } + .budget-box { background: #151e2e; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-top: auto; } """) ] @@ -34,14 +104,39 @@ def get(): Div( H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"), P("Tech Core - Multi IA", style="color:#a855f7; margin-bottom: 20px;"), + + Div( + P("BUDGET OPENROUTER", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:5px;"), + P(get_budget(), id="budget-display", style="color:#10b981; font-size:18px; font-weight:bold;"), + cls="budget-box" + ), cls="sidebar" ), Div( Div(id="chat-history", cls="chat-container"), Form( - Input(type="text", name="msg", placeholder="Demandez moi de coder quelque chose...", cls="input-box", required=True), - Button("Envoyer", type="submit", cls="send-btn"), - cls="input-row", + # Ligne 1 : Filtre et Liste dynamique des modèles + Div( + Select( + Option("Tous les modèles", value="all"), + Option("Gratuits uniquement", value="free"), + Option("Payants uniquement", value="paid"), + name="filter_type", + cls="model-select", + hx_get="/filter_models", # Demande la mise à jour à HTMX... + hx_target="#model-select-wrapper", # ...pour l'injecter dans cet élément + hx_swap="innerHTML", + style="width: 250px;" + ), + Div(get_model_options("all"), id="model-select-wrapper", style="flex-grow: 1;"), + cls="flex gap-2 mb-2 w-full" + ), + # Ligne 2 : Saisie texte et Bouton d'envoi + Div( + Input(type="text", name="msg", placeholder="Demandez moi de coder quelque chose...", cls="input-box", required=True), + Button("Envoyer", type="submit", cls="send-btn"), + cls="input-row" + ), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend" @@ -52,38 +147,50 @@ def get(): ) ) + +# --- 4. ROUTES DYNAMIQUES (HTMX) --- + +@rt('/filter_models') +def get(filter_type: str): + """Route appelée automatiquement quand on change le filtre (Gratuit/Payant)""" + return get_model_options(filter_type) + @rt('/chat') -def post(msg: str): - # 1. Préparation de la requête vers l'API OpenRouter +def post(msg: str, model_id: str): api_key = os.getenv("OPENROUTER_API_KEY") headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - # Payload avec un modèle gratuit d'OpenRouter payload = { - "model": "meta-llama/llama-3-8b-instruct:free", + "model": model_id, "messages": [{"role": "user", "content": msg}] } - # 2. Envoi de la requête try: response = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload) if response.status_code == 200: ia_reponse = response.json()["choices"][0]["message"]["content"] else: - ia_reponse = f"Erreur API ({response.status_code}) : Vérifiez votre clé dans le fichier .env" + ia_reponse = f"Erreur API ({response.status_code}) : {response.text}" except Exception as e: ia_reponse = f"Erreur réseau : {e}" - # 3. Injection de la réponse dans l'interface Web - return Div( + nom_modele = model_id.split('/')[-1] + + # 1. On prépare la bulle de chat + chat_bubble = Div( P("Vous", cls="msg-user"), Div(msg, cls="bubble-user"), - P("AETHAS38 (Llama 3)", cls="msg-ia"), + P(f"AETHAS38 ({nom_modele})", cls="msg-ia"), Div(ia_reponse, cls="bubble-ia") ) + + # 2. On recalcule le budget financier et on lui ordonne de s'actualiser avec hx_swap_oob + updated_budget = P(get_budget(), id="budget-display", style="color:#10b981; font-size:18px; font-weight:bold;", hx_swap_oob="true") + + return chat_bubble, updated_budget if __name__ == '__main__': serve(port=5001) \ No newline at end of file