feat: host_type field for OpenRouter / OpenAI-compatible API support

Adds host_type ("openwebui" | "openai") to the host schema so Cortex can
talk to both Open WebUI/Ollama and OpenRouter/standard-OpenAI endpoints.

Path differences per type:
  openwebui (default): /api/chat/completions, /api/models
  openai:              /chat/completions,     /models

model_registry.py:
  - host_type added to host schema (default "openwebui", backward compat)
  - save_host() accepts host_type parameter
  - _resolve_model() passes host_type through with the merged host fields

llm_client._local():
  - Reads host_type from resolved model_cfg
  - Selects correct chat completions path accordingly

routers/local_llm.py:
  - save_host route accepts host_type form field
  - fetch-models uses /models for openai type, /api/models for openwebui
  - Existing host rows show type selector pre-filled from stored value

local_llm.html:
  - "Add host" form includes type selector

To use OpenRouter:
  - Add host: URL = https://openrouter.ai/api/v1, Type = OpenAI-compatible
  - API key from openrouter.ai (store in .env or model_registry.json only)
  - Fetch models or add manually (e.g. anthropic/claude-sonnet-4-5-20251022)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-06 21:11:22 -04:00
parent 2dd94696d5
commit a6e404c143
4 changed files with 82 additions and 32 deletions

View File

@@ -6,7 +6,18 @@ Stored in: home/{user}/model_registry.json
Schema:
{
"version": 1,
"hosts": [{"id", "label", "api_url", "api_key"}, ...],
"hosts": [{"id", "label", "api_url", "api_key",
"host_type": "openwebui" | "openai"}, ...],
#
# host_type controls the API path layout:
# "openwebui" (default) — Open WebUI / Ollama:
# chat: POST {url}/api/chat/completions
# models: GET {url}/api/models
# "openai" — OpenRouter, LiteLLM, Anthropic-compatible, etc.:
# chat: POST {url}/chat/completions
# models: GET {url}/models
# Set api_url to the base path that ends just before /chat/completions,
# e.g. https://openrouter.ai/api/v1 for OpenRouter.
"models": [
{
"id": str, # unique within this registry
@@ -212,7 +223,12 @@ def _resolve_model(registry: dict, model_id: str) -> dict | None:
if not host:
logger.warning("model %s references missing host_id %s", model_id, host_id)
return None
return {**model, "api_url": host.get("api_url", ""), "api_key": host.get("api_key", "")}
return {
**model,
"api_url": host.get("api_url", ""),
"api_key": host.get("api_key", ""),
"host_type": host.get("host_type", "openwebui"),
}
return dict(model)
@@ -308,15 +324,21 @@ def get_defined_roles(username: str) -> dict[str, dict]:
# ── Write API (CRUD) ──────────────────────────────────────────────────────────
def save_host(username: str, host_id: str | None,
label: str, api_url: str, api_key: str) -> str:
"""Create or update a host. Returns the host ID."""
label: str, api_url: str, api_key: str,
host_type: str = "openwebui") -> str:
"""Create or update a host. Returns the host ID.
host_type: "openwebui" (default) or "openai" (OpenRouter, LiteLLM, etc.)
"""
data = _load(username)
host_type = host_type if host_type in ("openwebui", "openai") else "openwebui"
if host_id:
for h in data["hosts"]:
if h["id"] == host_id:
h["label"] = label.strip()
h["api_url"] = api_url.strip()
h["label"] = label.strip()
h["api_url"] = api_url.strip()
h["host_type"] = host_type
if api_key.strip():
h["api_key"] = api_key.strip()
_save(username, data)
@@ -325,10 +347,11 @@ def save_host(username: str, host_id: str | None,
host_id = secrets.token_hex(4)
data["hosts"].append({
"id": host_id,
"label": label.strip(),
"api_url": api_url.strip(),
"api_key": api_key.strip(),
"id": host_id,
"label": label.strip(),
"api_url": api_url.strip(),
"api_key": api_key.strip(),
"host_type": host_type,
})
_save(username, data)
return host_id