APIGOTO: OpenAI-kompatibles LLM-Gateway – Anbindung an FHEM per HttpUtils

Begonnen von apigoto, 18 Oktober 2026, 11:45:41

Vorheriges Thema - Nächstes Thema

apigoto

Transparenz vorab: Ich arbeite im APIGOTO-Team. APIGOTO ist ein unabhängiges OpenAI-kompatibles Gateway, nicht verbunden mit OpenAI, Anthropic oder Google. Deshalb poste ich hier im Marktplatz und nutze die Gruppe ,,Commercial User".

Die Anleitung unten funktioniert mit jedem Endpunkt, der `/v1/chat/completions` spricht, also auch mit einem lokalen Server im LAN (llama.cpp-Server, Ollama im OpenAI-Modus usw.). Es ändern sich nur Basis-URL und Modell-ID. Die Schnittstelle ist offen dokumentiert (OpenAI-kompatibles HTTP/JSON).

Was es macht: Ein Dummy `KI` bekommt eine Frage, FHEM schickt sie nicht blockierend an das Modell und schreibt die Antwort ins Reading `antwort`. Ein Anwendungsfall ist eine kurze Morgenzusammenfassung aus Sensorwerten.

1. Endpunkt vom FHEM-Server aus testen

curl https://api.china-aiapi.com/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"model":"MODEL_ID","messages":[{"role":"user","content":"Sag hallo"}]}'

2. Schlüssel einmalig ablegen (nicht in fhem.cfg)

In der FHEM-Befehlszeile:

{ setKeyValue("llm_api_key", "HIER_KEY_EINFÜGEN") }
3. Funktion in 99_myUtils.pm

use JSON::PP;
use HttpUtils;

sub LLM_Ask($$) {
  my ($dev, $frage) = @_;
  my ($err, $key) = getKeyValue("llm_api_key");
  return "kein API-Key" if ($err || !$key);

  my $body = encode_json({
    model      => "MODEL_ID",
    max_tokens => 150,
    messages   => [
      { role => "system", content => "Antworte kurz auf Deutsch, ein bis zwei Sätze." },
      { role => "user",   content => $frage },
    ],
  });

  HttpUtils_NonblockingGet({
    url      => "https://api.china-aiapi.com/v1/chat/completions",
    method   => "POST",
    timeout  => 60,          # Standard sind nur 4 s, für ein LLM zu kurz
    hideurl  => 1,
    header   => "Content-Type: application/json\r\nAuthorization: Bearer $key",
    data     => $body,
    dev      => $dev,
    callback => \&LLM_AskCallback,
  });
  return undef;
}

sub LLM_AskCallback($$$) {
  my ($param, $err, $data) = @_;
  my $hash = $defs{$param->{dev}};
  if ($err) {
    readingsSingleUpdate($hash, "fehler", $err, 1);
    return;
  }
  if ($param->{code} != 200) {
    readingsSingleUpdate($hash, "fehler", "HTTP $param->{code}: " . substr($data, 0, 200), 1);
    return;
  }
  my $res = eval { decode_json($data) };
  my $text = $res && $res->{choices}[0]{message}{content};
  readingsSingleUpdate($hash, defined $text ? "antwort" : "fehler",
                       defined $text ? $text : "keine Antwort im JSON", 1);
}

Danach `reload 99_myUtils` (oder FHEM neu starten).

4. Dummy und notify

define KI dummy
attr KI setList frage:textField-long
attr KI readingList frage
define n_KI notify KI:frage:.* { LLM_Ask("KI", ReadingsVal("KI","frage","")) }

Test: `set KI frage Wie warm war es heute laut Sensoren?`. Nach ein paar Sekunden steht die Antwort in `KI:antwort`. Für eine Zusammenfassung baut man die Frage per `at` aus Readings zusammen, zum Beispiel `ReadingsVal("Aussen","temperature","?")`.

Fehlersuche

- Reading `fehler` ,,timed out": `timeout` erhöhen. Der Standard von 4 s reicht für ein Sprachmodell praktisch nie.
- HTTP 401: Key falsch gespeichert. `{ getKeyValue("llm_api_key") }` prüfen.
- HTTP 404: `/v1` fehlt oder ist doppelt in der URL.
- ,,model not found": Modell-ID genau so wie in `GET /v1/models`.
- Kurzes Hängen von FHEM: Die DNS-Auflösung in HttpUtils ist laut Wiki noch blockierend. Bei langsamem DNS die IP eines lokalen Servers nutzen.
- Datenschutz: Alles, was in der Frage steht (Anwesenheit, Werte), verlässt das Haus, außer bei einem lokalen Server.

Zum Gateway selbst: eine Basis-URL für mehrere Modellfamilien, automatisches Failover bei Upstream-Fehlern (höchstens 3 Wiederholungen pro Anfrage über eine andere Route), Log pro Anfrage, Abrechnung nach Verbrauch, BYOK: www.china-aiapi.com

Fragen und Verbesserungen zum Code gerne hier im Thema. Weitere Infos poste ich ebenfalls nur hier, nicht in neuen Themen.
Arbeite im APIGOTO-Team (OpenAI-kompatibles LLM-Gateway, unabhängig von OpenAI/Anthropic/Google)