feat: MI50 as a Lyra backend (OpenAI-compatible local GPU)

The MI50 box (CT202) runs an OpenAI-compatible llama.cpp server on
10.0.0.44:8080. Wire it in as a third backend:

- llm.complete gains backend="mi50" (OpenAI client pointed at MI50_BASE_URL)
- config: MI50_BASE_URL (default http://10.0.0.44:8080/v1) + MI50_MODEL
- chat.respond labels the model per backend; web _backend_for maps "mi50"
- UI backend selector adds "MI50 — local GPU"

Verified end-to-end: llm.complete(backend="mi50") returns from the live server.
See homelab-inference memory for the box topology.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 05:37:22 +00:00
parent ecf0b852f9
commit 30185f3fd8
6 changed files with 27 additions and 3 deletions
+4
View File
@@ -2,6 +2,10 @@
LOCAL_BASE_URL=http://localhost:11434
LOCAL_MODEL=qwen2.5:7b-instruct
# MI50 backend — OpenAI-compatible llama.cpp server on the home-lab GPU box.
MI50_BASE_URL=http://10.0.0.44:8080/v1
MI50_MODEL=local-gpu
# Cloud backend (OpenAI) — higher quality, costs money.
OPENAI_API_KEY=
CLOUD_MODEL=gpt-4o-mini
+3 -1
View File
@@ -81,7 +81,9 @@ def build_messages(session_id: str, user_msg: str) -> list[Message]:
def respond(session_id: str, user_msg: str, backend: Backend = "cloud") -> str:
"""Produce Lyra's reply to a single user message and persist the exchange."""
cfg = config.load()
model = cfg.local_model if backend == "local" else cfg.cloud_model
model = {"local": cfg.local_model, "cloud": cfg.cloud_model, "mi50": cfg.mi50_model}.get(
backend, backend
)
logbus.log(
"info", "chat request", session=session_id, backend=backend,
model=model, embed=cfg.embed_backend,
+4
View File
@@ -14,6 +14,8 @@ load_dotenv()
class Config:
local_base_url: str
local_model: str
mi50_base_url: str # OpenAI-compatible llama.cpp server on the MI50 box
mi50_model: str
openai_api_key: str
cloud_model: str
embed_backend: str # "cloud" (OpenAI) or "local" (Ollama)
@@ -27,6 +29,8 @@ def load() -> Config:
return Config(
local_base_url=os.getenv("LOCAL_BASE_URL", "http://localhost:11434"),
local_model=os.getenv("LOCAL_MODEL", "qwen2.5:7b-instruct"),
mi50_base_url=os.getenv("MI50_BASE_URL", "http://10.0.0.44:8080/v1"),
mi50_model=os.getenv("MI50_MODEL", "local-gpu"),
openai_api_key=os.getenv("OPENAI_API_KEY", ""),
cloud_model=os.getenv("CLOUD_MODEL", "gpt-4o-mini"),
embed_backend=os.getenv("EMBED_BACKEND", "cloud").lower(),
+7 -1
View File
@@ -14,7 +14,7 @@ class Message(TypedDict):
content: str
Backend = Literal["local", "cloud"]
Backend = Literal["local", "cloud", "mi50"]
def complete(messages: list[Message], backend: Backend = "local") -> str:
@@ -26,6 +26,12 @@ def complete(messages: list[Message], backend: Backend = "local") -> str:
resp = client.chat.completions.create(model=cfg.cloud_model, messages=messages)
return resp.choices[0].message.content or ""
if backend == "mi50":
# MI50 box runs an OpenAI-compatible llama.cpp server; key is unused.
client = OpenAI(api_key="not-needed", base_url=cfg.mi50_base_url)
resp = client.chat.completions.create(model=cfg.mi50_model, messages=messages)
return resp.choices[0].message.content or ""
resp = httpx.post(
f"{cfg.local_base_url}/api/chat",
json={"model": cfg.local_model, "messages": messages, "stream": False},
+4 -1
View File
@@ -32,7 +32,10 @@ _CLOUD = {"OPENAI", "cloud", "custom"}
def _backend_for(label: str | None) -> Backend:
if label and label.upper() in {"PRIMARY", "SECONDARY", "FALLBACK", "LOCAL"}:
key = (label or "").lower()
if key == "mi50":
return "mi50"
if key in {"local", "primary", "secondary", "fallback"}:
return "local"
return "cloud"
+5
View File
@@ -123,6 +123,11 @@
<span>Local — Ollama</span>
<small>Free, private, runs on your home lab (LOCAL_MODEL)</small>
</label>
<label class="radio-label">
<input type="radio" name="backend" value="mi50">
<span>MI50 — local GPU</span>
<small>Free, llama.cpp on the MI50 box (MI50_BASE_URL)</small>
</label>
<label class="radio-label">
<input type="radio" name="backend" value="cloud">
<span>Cloud — OpenAI</span>