import os import pty import fcntl import subprocess import sqlite3 import asyncio import openai import re import httpx import struct import termios from telegram import Update from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters from telegram.error import InvalidToken from google import genai from google.genai import types import json from fastapi import FastAPI, WebSocket, BackgroundTasks, Request, Form, WebSocketDisconnect from fastapi.responses import RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from dotenv import load_dotenv, set_key # Lade Umgebungsvariablen load_dotenv() app = FastAPI() static_path = os.path.join(os.path.dirname(__file__), "static") app.mount("/static", StaticFiles(directory=static_path), name="static") templates = Jinja2Templates(directory="templates") SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") DB_PATH = "cluster.db" chat_history = [] PROMPT_FILE = "system_prompt.txt" ENV_FILE = os.path.join(os.path.dirname(__file__), ".env") # KI KONFIGURATION AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower() OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1") GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.0-flash") OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3") NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "moonshotai/kimi-k2.5") # Telegram Bot Konfiguration TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") ALLOWED_ID = os.getenv("ALLOWED_TELEGRAM_USER_ID", "") telegram_app = None # --- DATENBANK INITIALISIERUNG (ERWEITERT) --- def init_db(): conn = sqlite3.connect(DB_PATH) # Spalten erweitert um sudo_password, os, arch, docker_installed conn.execute(''' CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, ip TEXT UNIQUE, user TEXT, sudo_password TEXT, os TEXT DEFAULT 'Unbekannt', arch TEXT DEFAULT 'Unbekannt', docker_installed INTEGER DEFAULT 0, status TEXT ) ''') conn.commit() conn.close() init_db() def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def get_system_prompt(): conn = get_db() nodes = conn.execute('SELECT * FROM nodes').fetchall() conn.close() node_info = "" for n in nodes: docker_str = "Ja" if n['docker_installed'] else "Nein" node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n" if os.path.exists(PROMPT_FILE): with open(PROMPT_FILE, "r", encoding="utf-8") as f: template = f.read() else: template = "Du bist ein Cluster-Orchestrator. Nodes:\n{node_info}\nBefehle via cmd" print(f"⚠️ Warnung: {PROMPT_FILE} fehlt.") return template.replace("{node_info}", node_info) # --- KI FUNKTIONEN --- async def get_ai_response(user_input, system_prompt): global chat_history chat_history.append({"role": "user", "content": user_input}) chat_history = chat_history[-30:] ai_msg = "" try: # Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK) if AI_PROVIDER in ["openai", "ollama", "nvidia"]: messages = [{"role": "system", "content": system_prompt}] + chat_history if AI_PROVIDER == "ollama": url = OLLAMA_BASE_URL if not url.endswith('/v1') and not url.endswith('/v1/'): url = url.rstrip('/') + '/v1' key = "ollama" model_to_use = OLLAMA_MODEL elif AI_PROVIDER == "nvidia": url = "https://integrate.api.nvidia.com/v1" key = NVIDIA_API_KEY model_to_use = NVIDIA_MODEL else: # openai url = None key = OPENAI_API_KEY model_to_use = OPENAI_MODEL # WICHTIG: Hier .AsyncOpenAI nutzen, da die Funktion async ist client = openai.AsyncOpenAI(base_url=url, api_key=key) response = await client.chat.completions.create( model=model_to_use, messages=messages ) ai_msg = response.choices[0].message.content elif AI_PROVIDER == "google": # Für Google Gemini if not GOOGLE_API_KEY: return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!" client = genai.Client(api_key=GOOGLE_API_KEY) # Wir müssen unser Array in das spezielle Google-Format umwandeln google_history = [] # Alle Nachrichten AUSSER der allerletzten (die aktuelle User-Frage) in die History packen for msg in chat_history[:-1]: role = "user" if msg["role"] == "user" else "model" google_history.append( types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])]) ) # Chat MIT dem übersetzten Gedächtnis starten chat = client.chats.create( model=GOOGLE_MODEL, config=types.GenerateContentConfig(system_instruction=system_prompt), history=google_history ) # Jetzt erst die neue Nachricht an den Chat mit Gedächtnis schicken response = chat.send_message(user_input) ai_msg = response.text except Exception as e: ai_msg = f"Fehler bei der KI-Anfrage: {e}" print(f"KI Fehler: {e}") # 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen chat_history.append({"role": "assistant", "content": ai_msg}) return ai_msg async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE): # Sicherheits-Check: Nur deine Chat-ID wird akzeptiert if str(update.effective_chat.id) != ALLOWED_ID: await update.message.reply_text("Zugriff verweigert. 🔒") return user_msg = update.message.text # "Tippt..." Status in Telegram anzeigen (schöne UX) await update.message.reply_chat_action(action="typing") # 1. KI Antwort holen ai_response = await get_ai_response(user_msg, get_system_prompt()) # 2. Befehle extrahieren commands = re.findall(r'(.*?)', ai_response, re.I | re.S) clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() # KI Text-Antwort senden if clean_msg: await update.message.reply_text(clean_msg) # 3. Befehle ausführen und Ergebnisse als Telegram-Nachricht senden if commands: for target, cmd in commands: await update.message.reply_text(f"⏳ Führe aus auf *{target}*:\n`{cmd}`", parse_mode='Markdown') # Node in DB suchen conn = get_db() n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone() conn.close() if n: # Befehl ausführen und Output abfangen try: proc = await asyncio.create_subprocess_shell( f"ssh -o StrictHostKeyChecking=no {n['user']}@{n['ip']} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) stdout, _ = await proc.communicate() output = stdout.decode('utf-8', errors='ignore').strip() # Ergebnis an Telegram senden (abgeschnitten auf 4000 Zeichen, da Telegram Limits hat) result_text = output[:4000] if output else "✅ Befehl ohne Output ausgeführt." await update.message.reply_text(f"💻 **Output von {n['name']}:**\n```\n{result_text}\n```", parse_mode='Markdown') # Auch ins KI-Gedächtnis schreiben chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}"}) except Exception as e: await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}") else: await update.message.reply_text(f"⚠️ Node '{target}' nicht in der Datenbank gefunden.") # --- FASTAPI LIFESPAN EVENTS (Bot starten/stoppen) --- @app.on_event("startup") async def startup_event(): global telegram_app # Prüfe auch, ob der Token nicht aus Versehen noch der Platzhalter ist if TELEGRAM_TOKEN and ALLOWED_ID and "dein-telegram-bot-token" not in TELEGRAM_TOKEN: print("🤖 Starte Telegram Bot im Hintergrund...") try: telegram_app = ApplicationBuilder().token(TELEGRAM_TOKEN).build() # Leitet alle Text-Nachrichten an unsere Funktion weiter telegram_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_telegram_message)) # Bot asynchron in die FastAPI Event-Loop einhängen await telegram_app.initialize() await telegram_app.start() await telegram_app.updater.start_polling() print("✅ Telegram Bot lauscht!") except InvalidToken: print("❌ Telegram-Fehler: Der Token in der .env ist ungültig! Der Bot bleibt inaktiv, aber der Server läuft weiter.") except Exception as e: print(f"❌ Unerwarteter Fehler beim Telegram-Start: {e}") else: print("ℹ️ Telegram Bot inaktiv (Token oder ID fehlen/sind Platzhalter in der .env).") @app.on_event("shutdown") async def shutdown_event(): global telegram_app if telegram_app: print("🛑 Stoppe Telegram Bot...") await telegram_app.updater.stop() await telegram_app.stop() await telegram_app.shutdown() # --- WebSocket Manager --- class ConnectionManager: def __init__(self): self.active_connections = [] async def connect(self, ws: WebSocket): await ws.accept(); self.active_connections.append(ws) def disconnect(self, ws: WebSocket): self.active_connections.remove(ws) async def broadcast(self, msg: str): for c in self.active_connections: try: await c.send_text(msg) except: pass manager = ConnectionManager() async def get_remote_info(ip, user): """Versucht Linux/Mac-Infos zu lesen, falls fehlgeschlagen, dann Windows.""" # 1. Versuch: Linux/Mac linux_cmd = "uname -m && (sw_vers -productName 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 || uname -s) && (command -v docker >/dev/null 2>&1 && echo 1 || echo 0)" ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{linux_cmd}\"" try: output = subprocess.check_output(ssh_cmd, shell=True, stderr=subprocess.DEVNULL).decode().strip().split('\n') if len(output) >= 2: return { "arch": output[0], "os": output[1].replace('"', ''), "docker": int(output[2]) if len(output) > 2 else 0 } except: pass # Linux-Versuch gescheitert, weiter zu Windows # 2. Versuch: Windows (CMD) # ver = OS Version, echo %PROCESSOR_ARCHITECTURE% = Arch, where docker = Docker Check win_cmd = 'ver && echo %PROCESSOR_ARCHITECTURE% && (where docker >nul 2>&1 && echo 1 || echo 0)' ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\"" try: output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n') # Windows Output sieht oft so aus: ["Microsoft Windows [Version 10.0...]", "AMD64", "1"] raw_os = output[0] if len(output) > 0 else "Windows" os_name = "Windows" if "Version 10" in raw_os: os_name = "Windows 10/11" elif "Version 11" in raw_os: os_name = "Windows 11" arch = output[1] if len(output) > 1 else "x86" if "AMD64" in arch: arch = "x86-64" docker_val = int(output[2]) if len(output) > 2 else 0 return {"arch": arch, "os": os_name, "docker": docker_val} except Exception as e: print(f"Windows-Check fehlgeschlagen für {ip}: {e}") return None # Nutze diese Funktion nun in bootstrap_node und refresh_status # --- ERWEITERTES NODE BOOTSTRAPPING (Inventur) --- async def bootstrap_node(ip, user, password): await manager.broadcast(f"🔑 Kopple {ip}...") with open(f"{SSH_KEY}.pub", "r") as f: pub_key = f.read().strip() # Wir nutzen ein absolut minimalistisches Kommando. # Es erstellt das Verzeichnis (falls nötig) und hängt den Key an. # Das funktioniert in der Windows CMD und der Linux Bash. cmd_universal = f'mkdir .ssh & echo {pub_key} >> .ssh/authorized_keys' # sshpass direkt mit dem simplen Befehl setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\"" try: # Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen. # Wir unterdrücken das mit -o StrictHostKeyChecking=no proc = subprocess.run(setup_cmd, shell=True, capture_output=True, text=True, timeout=15) if proc.returncode == 0: await manager.broadcast(f"✅ Key an {ip} übertragen.") else: # Falls 'mkdir' einen Fehler wirft (weil Ordner existiert), ist das egal, # solange der Key danach drin ist. await manager.broadcast(f"ℹ️ Info: {ip} antwortet (Key-Check folgt).") except Exception as e: await manager.broadcast(f"❌ Fehler: {e}") # Inventur (get_remote_info) prüft jetzt, ob es wirklich klappt await manager.broadcast(f"🔍 Teste schlüssellosen Zugriff auf {ip}...") info = await get_remote_info(ip, user) if info: status = "Docker Aktiv" if info['docker'] else "Bereit (Kein Docker)" conn = get_db() conn.execute(''' UPDATE nodes SET os = ?, arch = ?, docker_installed = ?, status = ? WHERE ip = ? ''', (info['os'], info['arch'], info['docker'], status, ip)) conn.commit() conn.close() await manager.broadcast(f"✅ Node {ip} erkannt als {info['os']} ({info['arch']}).") else: await manager.broadcast(f"⚠️ Inventur auf {ip} fehlgeschlagen.") # --- ROUTES --- @app.get("/") async def index(request: Request): conn = get_db() nodes = conn.execute('SELECT * FROM nodes').fetchall() conn.close() return templates.TemplateResponse("index.html", {"request": request, "nodes": nodes}) @app.get("/api/node/{node_id}") async def get_node(node_id: int): conn = get_db() node = conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,)).fetchone() conn.close() return dict(node) if node else {} @app.put("/api/node/{node_id}") async def api_update_node(node_id: int, request: Request): data = await request.json() conn = get_db() try: conn.execute(''' UPDATE nodes SET name = ?, ip = ?, user = ?, sudo_password = ?, os = ?, arch = ?, status = ?, docker_installed = ? WHERE id = ? ''', ( data.get("name"), data.get("ip"), data.get("user"), data.get("sudo_password"), data.get("os"), data.get("arch"), data.get("status"), data.get("docker_installed"), node_id )) conn.commit() return {"status": "success"} except Exception as e: print(f"Update Fehler: {e}") return {"status": "error", "message": str(e)}, 500 finally: conn.close() @app.post("/add_node") async def add_node(background_tasks: BackgroundTasks, name: str = Form(...), ip: str = Form(...), user: str = Form(...), password: str = Form(...)): conn = get_db() try: # Speichere Initialdaten inkl. Sudo-Passwort conn.execute(''' INSERT INTO nodes (name, ip, user, sudo_password, status) VALUES (?, ?, ?, ?, ?) ''', (name, ip, user, password, "Kopplung...")) conn.commit() background_tasks.add_task(bootstrap_node, ip, user, password) except sqlite3.IntegrityError: pass finally: conn.close() return RedirectResponse(url="/", status_code=303) @app.post("/edit_node/{node_id}") async def edit_node(node_id: int, name: str = Form(...), ip: str = Form(...), user: str = Form(...)): conn = get_db() conn.execute('UPDATE nodes SET name=?, ip=?, user=? WHERE id=?', (name, ip, user, node_id)) conn.commit() conn.close() return RedirectResponse(url="/", status_code=303) @app.post("/remove_node/{node_id}") async def remove_node(node_id: int): conn = get_db() conn.execute('DELETE FROM nodes WHERE id = ?', (node_id,)) conn.commit() conn.close() return RedirectResponse(url="/", status_code=303) @app.get("/refresh_status/{node_id}") async def refresh_status(node_id: int): conn = get_db() node = conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,)).fetchone() if not node: return {"status": "Offline"} info = await get_remote_info(node['ip'], node['user']) if info: new_status = "Docker Aktiv" if info['docker'] else "Bereit (Kein Docker)" conn.execute('UPDATE nodes SET status=?, os=?, arch=?, docker_installed=? WHERE id=?', (new_status, info['os'], info['arch'], info['docker'], node_id)) conn.commit() result = {"status": new_status, "os": info['os'], "arch": info['arch'], "docker": info['docker']} else: new_status = "Offline" conn.execute('UPDATE nodes SET status=? WHERE id=?', (new_status, node_id)) conn.commit() result = {"status": new_status, "os": node['os'], "arch": node['arch'], "docker": node['docker_installed']} conn.close() return result # --- WebSockets Terminal / Chat / Logs (Integration wie gehabt) --- @app.websocket("/ws/install_logs") async def log_websocket(websocket: WebSocket): await manager.connect(websocket) try: while True: await websocket.receive_text() except WebSocketDisconnect: manager.disconnect(websocket) @app.websocket("/ws/terminal/{ip}") async def terminal_websocket(websocket: WebSocket, ip: str): await websocket.accept() conn = get_db() node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone() conn.close() if not node: await websocket.close() return master_fd, slave_fd = pty.openpty() # Wir starten SSH im interaktiven Modus proc = await asyncio.create_subprocess_exec( "ssh", "-i", SSH_KEY, # <--- Das hier ist entscheidend! "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", # Verhindert, dass SSH hängen bleibt, falls der Key doch nicht geht "-t", f"{node['user']}@{ip}", stdin=slave_fd, stdout=slave_fd, stderr=slave_fd ) async def pty_to_ws(): fl = fcntl.fcntl(master_fd, fcntl.F_GETFL) fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) try: while True: await asyncio.sleep(0.01) try: data = os.read(master_fd, 1024).decode(errors='ignore') if data: await websocket.send_text(data) except BlockingIOError: continue except: pass async def ws_to_pty(): try: while True: data = await websocket.receive_text() # Prüfen, ob es ein Resize-Kommando (JSON) ist if data.startswith('{"type":"resize"'): resize_data = json.loads(data) cols = resize_data['cols'] rows = resize_data['rows'] # Das hier setzt die Größe des PTY im Betriebssystem s = struct.pack("HHHH", rows, cols, 0, 0) fcntl.ioctl(master_fd, termios.TIOCSWINSZ, s) else: # Normale Terminal-Eingabe os.write(master_fd, data.encode()) except: pass try: await asyncio.gather(pty_to_ws(), ws_to_pty()) finally: if proc.returncode is None: proc.terminate() os.close(master_fd) os.close(slave_fd) @app.websocket("/ws/chat") async def chat_endpoint(websocket: WebSocket): await websocket.accept() try: while True: user_msg = await websocket.receive_text() ai_response = await get_ai_response(user_msg, get_system_prompt()) commands = re.findall(r'(.*?)', ai_response, re.I | re.S) clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() if clean_msg: await websocket.send_text(clean_msg) if commands: tasks = [] for target, cmd in commands: conn = get_db(); n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone(); conn.close() if n: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip())) if tasks: await websocket.send_text("ℹ️ *Führe Befehle aus...*") await asyncio.gather(*tasks) summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt()) await websocket.send_text(f"--- Info ---\n{summary}") except: pass async def run_remote_task(ip, user, cmd): await manager.broadcast(f"🚀 Task: {cmd} auf {ip}") proc = await asyncio.create_subprocess_shell(f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) full_output = "" while True: line = await proc.stdout.readline() if not line: break out = line.decode('utf-8', errors='ignore').strip() if out: await manager.broadcast(f"🛠️ {out}"); full_output += out + "\n" await proc.wait() chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}"}) # --- Settings API --- @app.get("/api/settings") async def get_settings(): return { "provider": AI_PROVIDER, "google_model": GOOGLE_MODEL, "openai_model": OPENAI_MODEL, "nvidia_model": NVIDIA_MODEL, "ollama_model": OLLAMA_MODEL, "ollama_base_url": OLLAMA_BASE_URL # URL ans Frontend schicken } @app.post("/api/settings") async def update_settings(request: Request): # WICHTIG: OLLAMA_BASE_URL als global deklarieren global AI_PROVIDER, GOOGLE_MODEL, OPENAI_MODEL, NVIDIA_MODEL, OLLAMA_MODEL, OLLAMA_BASE_URL data = await request.json() provider = data.get("provider") model = data.get("model") ollama_url = data.get("ollama_base_url") # URL vom Frontend empfangen if provider: AI_PROVIDER = provider set_key(ENV_FILE, "AI_PROVIDER", provider) if provider == "google" and model: GOOGLE_MODEL = model set_key(ENV_FILE, "GOOGLE_MODEL", model) elif provider == "openai" and model: OPENAI_MODEL = model set_key(ENV_FILE, "OPENAI_MODEL", model) elif provider == "nvidia" and model: NVIDIA_MODEL = model set_key(ENV_FILE, "NVIDIA_MODEL", model) elif provider == "ollama" and model: OLLAMA_MODEL = model set_key(ENV_FILE, "OLLAMA_MODEL", model) # Wenn eine Ollama-URL mitgeschickt wurde, speichern wir sie if ollama_url: OLLAMA_BASE_URL = ollama_url set_key(ENV_FILE, "OLLAMA_BASE_URL", ollama_url) return {"status": "success"} @app.get("/api/models") async def get_models(provider: str, url: str = None): try: models = [] if provider == "ollama" and url: # Das Backend hat keine CORS-Probleme und fragt Ollama direkt clean_url = url.replace("/v1", "").rstrip("/") async with httpx.AsyncClient() as client: response = await client.get(f"{clean_url}/api/tags", timeout=5.0) data = response.json() models = [m["name"] for m in data.get("models", [])] elif provider == "openai": if not OPENAI_API_KEY or "hier" in OPENAI_API_KEY: return {"models": []} # Hier greift das Backend auf deinen OpenAI API-Key zu client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) response = await client.models.list() # Nur GPT-Modelle filtern, um die Liste sauber zu halten models = [m.id for m in response.data if "gpt" in m.id or "o1" in m.id] models.sort() elif provider == "nvidia": if not NVIDIA_API_KEY or "hier" in NVIDIA_API_KEY: return {"models": ["FEHLER: Key fehlt in .env"]} client = openai.AsyncOpenAI( api_key=NVIDIA_API_KEY, base_url="https://integrate.api.nvidia.com/v1" ) try: # Timeout hinzufügen, damit die Seite nicht ewig hängt response = await asyncio.wait_for(client.models.list(), timeout=5.0) models = [m.id for m in response.data] models.sort() return {"models": models} except Exception as e: print(f"NVIDIA API Error: {e}") return {"models": [f"NVIDIA Fehler: {str(e)[:20]}..."]} elif provider == "google": if not GOOGLE_API_KEY: return {"models": ["API-Key fehlt"]} client = genai.Client(api_key=GOOGLE_API_KEY) models = [] # Im neuen SDK (google-genai) heißt das Feld 'supported_actions' for m in client.models.list(): if 'generateContent' in m.supported_actions: # Wir nehmen den Namen und entfernen das 'models/' Präfix model_name = m.name.replace("models/", "") models.append(model_name) models.sort() return {"models": models} except Exception as e: print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}") return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback @app.get("/debug_keys") async def debug_keys(): return { "AI_PROVIDER": AI_PROVIDER, "NVIDIA_KEY_LOADED": bool(NVIDIA_API_KEY and "hier" not in NVIDIA_API_KEY), "NVIDIA_KEY_START": NVIDIA_API_KEY[:10] if NVIDIA_API_KEY else "Missing", "GOOGLE_KEY_LOADED": bool(GOOGLE_API_KEY), "OLLAMA_URL": OLLAMA_BASE_URL } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)