""" Gemeinsamer, robuster OpenAI-Zugang für die Plugins. Kapselt zwei wiederkehrende Sorgen: * **Parameter-Verträglichkeit.** Neuere Modelle (o-Reihe, GPT-5-Reihe) lehnen `temperature` ungleich dem Standard und teils `response_format` ab. Statt daran zu scheitern, wird von der genauesten zur schlichtesten Variante durchprobiert — aber nur bei Parameter-Fehlern. Echte Fehler (falsches Modell, Auth, Netz) werden sofort durchgereicht. * **Antwort lesen.** Modelle schludern manchmal mit Markdown-Zäunen oder packen das Ergebnis in einen Wrapper. `parse_json` gleicht das aus. Der Schlüssel kommt aus der Umgebung (OPENAI_API_KEY). Das Modell aus dem Aufrufer oder OPENAI_MODEL, Vorgabe „gpt-5.5“. """ from __future__ import annotations import json import os import traceback from typing import Any DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.5") # Merkt sich prozessweit, welche Aufruf-Variante das Modell akzeptiert, damit # nicht jede Anfrage erneut durchprobiert wird. _CHAT_VARIANT: dict[str, Any] | None = None def parse_json(text: str) -> Any: """Antworttext zu Daten machen, auch bei Markdown-Zaun oder Wrapper.""" text = (text or "").strip() if text.startswith("```"): text = text.strip("`") if text[:4].lower() == "json": text = text[4:] text = text.strip() data = json.loads(text) # Einzeln verschachtelten Wrapper auspacken, aber nur wenn die Werte selbst # Tabellen sind (sonst würde ein echtes einelementiges Ergebnis zerstört). if isinstance(data, dict) and len(data) == 1: (only,) = data.values() if isinstance(only, dict) and only and all(isinstance(v, (dict, list)) for v in only.values()): return only return data def _chat_completion(client, model: str, messages: list[dict[str, str]]): global _CHAT_VARIANT varianten = [ {"response_format": {"type": "json_object"}, "temperature": 0}, {"response_format": {"type": "json_object"}}, {"temperature": 0}, {}, ] if _CHAT_VARIANT is not None: varianten = [_CHAT_VARIANT] letzter = None for extra in varianten: try: antwort = client.chat.completions.create(model=model, messages=messages, **extra) _CHAT_VARIANT = extra return antwort except Exception as exc: # noqa: BLE001 letzter = exc text = str(exc).lower() parameterfehler = any(w in text for w in ( "temperature", "response_format", "unsupported", "not supported", "unknown_parameter", "invalid_request", "unexpected keyword", )) if not parameterfehler: raise raise letzter def chat_json(messages: list[dict[str, str]], model: str | None = None) -> Any: """Eine Chat-Abfrage, deren Antwort als JSON gelesen zurückkommt.""" from openai import OpenAI client = OpenAI() response = _chat_completion(client, model or DEFAULT_MODEL, messages) text = response.choices[0].message.content or "{}" return parse_json(text) def probe(model: str | None = None) -> tuple[bool, str]: """ Eine einzelne Testabfrage. Rückgabe: (erfolg, Klartext-Meldung). Braucht kein Tandoor — nur OPENAI_API_KEY. """ model = model or DEFAULT_MODEL try: ergebnis = chat_json( [{"role": "system", "content": "Antworte ausschließlich mit JSON."}, {"role": "user", "content": 'Gib genau zurück: {"ok": true}'}], model, ) except Exception as exc: # noqa: BLE001 low = str(exc).lower() hinweis = "" if "model" in low and ("not" in low or "exist" in low or "unknown" in low): hinweis = " → Modell nicht vorhanden/freigeschaltet. In den Einstellungen ändern." elif "api key" in low or "authentication" in low or "401" in low: hinweis = " → API-Schlüssel wird nicht akzeptiert. OPENAI_API_KEY prüfen." elif "quota" in low or "insufficient" in low or "429" in low: hinweis = " → Kontingent/Guthaben erschöpft." return False, f"OpenAI nicht erreichbar: {exc}{hinweis}\n\n{traceback.format_exc().strip()}" return True, f"OpenAI antwortet. Verwendete Aufruf-Variante: {_CHAT_VARIANT}. Antwort: {ergebnis}"