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 (https://www.china-aiapi.com/grokbot)
Fragen und Verbesserungen zum Code gerne hier im Thema. Weitere Infos poste ich ebenfalls nur hier, nicht in neuen Themen.