diff --git a/backend/main.py b/backend/main.py index d196054..cc7786f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -60,7 +60,6 @@ def login_page(db: Session = Depends(get_db)): return RedirectResponse(url="/set @app.post("/api/setup") def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)): if not is_setup_required(db): raise HTTPException(status_code=403, detail="Déjà installé.") - # Le créateur initial devient automatiquement Super-Admin ET Admin new_admin = User(email=admin_data.email, username=admin_data.username, hashed_password=get_password_hash(admin_data.password), totp_secret=generate_totp_secret(), is_admin=True, is_superadmin=True) db.add(new_admin) new_settings = SystemSettings(smtp_host=admin_data.smtp_host, smtp_port=admin_data.smtp_port, smtp_user=admin_data.smtp_user, smtp_password=admin_data.smtp_password, openrouter_api_key=admin_data.openrouter_api_key, openrouter_management_key=admin_data.openrouter_management_key, groq_api_key=admin_data.groq_api_key, gemini_api_key=admin_data.gemini_api_key, deepseek_api_key=admin_data.deepseek_api_key, mistral_api_key=admin_data.mistral_api_key, cloudflare_account_id=admin_data.cloudflare_account_id, cloudflare_api_token=admin_data.cloudflare_api_token, huggingface_api_key=admin_data.huggingface_api_key) @@ -97,15 +96,12 @@ def change_password(passwords: PasswordChange, db: Session = Depends(get_db), cu @app.post("/api/users/me/avatar") async def upload_avatar(file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - """Enregistre l'avatar. Le redimensionnement parfait est géré visuellement par CSS (object-cover) dans le frontend.""" file_location = os.path.join(avatars_path, f"user_{current_user.id}.jpg") - with open(file_location, "wb") as buffer: - shutil.copyfileobj(file.file, buffer) + with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) current_user.avatar_path = f"/assets/avatars/user_{current_user.id}.jpg?v={int(datetime.now().timestamp())}" db.commit() return {"message": "Avatar mis à jour", "avatar_path": current_user.avatar_path} -# --- GESTION DES PROJETS ET MESSAGES --- @app.get("/api/projects", response_model=List[ProjectResponse]) def get_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): return db.query(Project).filter(Project.user_id == current_user.id).order_by(Project.created_at.desc()).all() @@ -151,7 +147,9 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() settings = db.query(SystemSettings).first() conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]} - ai_resp = await run_orchestrator(history, settings, conf) + + ai_resp = await run_orchestrator(db, history, settings, conf) # Transmission de DB + db.add(Message(role="assistant", content=ai_resp, project_id=project_id)); db.commit() return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() @@ -166,6 +164,10 @@ def get_models_info(db: Session = Depends(get_db), current_user: User = Depends( "finances": db.query(FinancialLog).all() } +@app.get("/api/finances") +def get_finances(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + return db.query(FinancialLog).all() + @app.post("/api/models/sync") async def trigger_model_sync(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): if not current_user.is_admin: raise HTTPException(status_code=403, detail="Accès admin requis.") diff --git a/backend/orchestrator.py b/backend/orchestrator.py index 76bfe81..4e793b2 100644 --- a/backend/orchestrator.py +++ b/backend/orchestrator.py @@ -14,20 +14,33 @@ def determine_domain(model_id: str) -> str: return "Texte Polyvalent" async def sync_finances(db: Session, settings: SystemSettings): + """Interroge les fournisseurs pour récupérer le solde financier exact.""" async with httpx.AsyncClient() as client: if settings.openrouter_management_key or settings.openrouter_api_key: try: key = settings.openrouter_management_key or settings.openrouter_api_key - resp = await client.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {key}"}) - if resp.status_code == 200: - data = resp.json().get("data", {}) - limit = data.get("limit") - usage = data.get("usage", 0) - balance = (limit - usage) if limit is not None else 0.0 - update_finance_db(db, "OpenRouter", balance, usage) - except Exception: pass + # Test de l'endpoint des crédits prépayés en priorité + cred_resp = await client.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {key}"}) + balance = 0.0 + usage = 0.0 + + if cred_resp.status_code == 200 and cred_resp.json().get("data"): + data = cred_resp.json().get("data", {}) + balance = float(data.get("total_credits") or 0.0) - float(data.get("total_usage") or 0.0) + usage = float(data.get("total_usage") or 0.0) + else: + # Fallback sur l'usage de la clé si pas de crédits prépayés + key_resp = await client.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {key}"}) + if key_resp.status_code == 200: + data = key_resp.json().get("data", {}) + limit = data.get("limit") + usage = float(data.get("usage") or 0.0) + balance = (float(limit) - usage) if limit is not None else -usage + + update_finance_db(db, "OpenRouter", balance, usage) + except Exception as e: print(f"Erreur Finance OR: {e}") - if settings.groq_api_key: update_finance_db(db, "Groq", 999.0, 0.0) + if settings.groq_api_key: update_finance_db(db, "Groq", 999.0, 0.0) # Gratuit en Beta if settings.deepseek_api_key: try: @@ -53,14 +66,14 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type resp = await client.get("https://openrouter.ai/api/v1/models") if resp.status_code == 200: for item in resp.json().get("data", []): - pricing = item.get("pricing", {}) - pp = float(pricing.get("prompt", 0)) * 1000000 if pricing.get("prompt") else 0.0 - pc = float(pricing.get("completion", 0)) * 1000000 if pricing.get("completion") else 0.0 + pricing = item.get("pricing") or {} + pp = float(pricing.get("prompt") or 0.0) * 1000000 + pc = float(pricing.get("completion") or 0.0) * 1000000 is_free = (pp == 0.0 and pc == 0.0) desc = item.get("description", "Modèle IA générique.")[:200] + "..." process_model(db, "openrouter", item["id"], item["name"], desc, determine_domain(item["id"]), is_free, item.get("context_length", 0), pp, pc) added += 1 - except Exception: pass + except Exception as e: print(f"Erreur Modèles OR: {e}") if settings.groq_api_key: try: @@ -95,21 +108,26 @@ async def ask_agent(client, model_id, messages, is_openrouter=False): resp = await client.chat.completions.create(**kwargs) return resp.choices[0].message.content -async def run_orchestrator(history: list, settings: SystemSettings, config: dict) -> str: +async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict) -> str: workers = config.get("workers", ["gemini-3.5-flash-lite"]) user_prompt = history[-1].content formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]] + final_response = "" if len(workers) == 1: w_mod = workers[0] - return await ask_agent(get_client_for_model(w_mod, settings), w_mod, formatted_history + [{"role": "user", "content": user_prompt}], "openrouter" in w_mod.lower()) + final_response = await ask_agent(get_client_for_model(w_mod, settings), w_mod, formatted_history + [{"role": "user", "content": user_prompt}], "openrouter" in w_mod.lower()) + else: + p_mod = config.get("prompter", "gemini-3.5-flash-lite") + optimized = await ask_agent(get_client_for_model(p_mod, settings), p_mod, [{"role": "system", "content": "Optimise cette requête."}, {"role": "user", "content": user_prompt}]) - p_mod = config.get("prompter", "gemini-3.5-flash-lite") - optimized = await ask_agent(get_client_for_model(p_mod, settings), p_mod, [{"role": "system", "content": "Optimise cette requête."}, {"role": "user", "content": user_prompt}]) + w_tasks = [ask_agent(get_client_for_model(w, settings), w, formatted_history + [{"role": "user", "content": optimized}], "openrouter" in w.lower()) for w in workers] + responses = await asyncio.gather(*w_tasks, return_exceptions=True) - w_tasks = [ask_agent(get_client_for_model(w, settings), w, formatted_history + [{"role": "user", "content": optimized}], "openrouter" in w.lower()) for w in workers] - responses = await asyncio.gather(*w_tasks, return_exceptions=True) - - c_mod = config.get("concatenator", "gemini-3.5-flash-lite") - synth = f"Requête: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)]) + "\n\nFais une synthèse finale." - return await ask_agent(get_client_for_model(c_mod, settings), c_mod, [{"role": "user", "content": synth}]) \ No newline at end of file + c_mod = config.get("concatenator", "gemini-3.5-flash-lite") + synth = f"Requête: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)]) + "\n\nFais une synthèse finale." + final_response = await ask_agent(get_client_for_model(c_mod, settings), c_mod, [{"role": "user", "content": synth}]) + + # MAJ Financière après requête + await sync_finances(db, settings) + return final_response \ No newline at end of file diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 1a0b2fa..9eaf25e 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -15,14 +15,36 @@ .neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; } + .custom-scrollbar::-webkit-scrollbar { width: 4px; } + .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
+ +
+
+
+

Paramètres

+ +
+
+

Sécurité du compte

+
{{ settingsMessage }}
+
+
+
+ +
+
+
+ -
+