|
|
@@ -2162,52 +2162,52 @@ async def update_config(request: Request) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/test-chat/models")
|
|
|
|
@app.get("/test-chat/models")
|
|
|
|
async def test_chat_models(request: Request) -> dict:
|
|
|
|
async def test_chat_models(request: Request) -> dict:
|
|
|
|
"""Return the list of models currently available in upstream_openai (Ollama)."""
|
|
|
|
"""Return the list of lm-studio/openai models configured in the DB."""
|
|
|
|
|
|
|
|
pool = request.app.state.pool
|
|
|
|
cfg = request.app.state.yaml_config
|
|
|
|
cfg = request.app.state.yaml_config
|
|
|
|
upstream = cfg.get("upstream_openai", "")
|
|
|
|
upstream = cfg.get("upstream_openai", "")
|
|
|
|
if not upstream:
|
|
|
|
async with pool.acquire() as conn:
|
|
|
|
return {"ok": False, "error": "upstream_openai not configured", "models": []}
|
|
|
|
rows = await conn.fetch(
|
|
|
|
url = f"{upstream.rstrip('/')}/v1/models"
|
|
|
|
"SELECT model_name, base_url FROM models WHERE provider IN ('lm-studio', 'openai') ORDER BY id"
|
|
|
|
try:
|
|
|
|
)
|
|
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
|
|
if rows:
|
|
|
|
r = await client.get(url)
|
|
|
|
models = [r["model_name"] for r in rows]
|
|
|
|
if not r.is_success:
|
|
|
|
return {"ok": True, "models": models, "upstream": upstream}
|
|
|
|
return {"ok": False, "error": f"HTTP {r.status_code} from {url}", "models": []}
|
|
|
|
return {"ok": False, "error": "No lm-studio or openai models configured — add one via Model Manager.", "models": []}
|
|
|
|
ids = sorted(m.get("id", "") for m in r.json().get("data", []))
|
|
|
|
|
|
|
|
return {"ok": True, "models": ids, "upstream": upstream}
|
|
|
|
|
|
|
|
except httpx.ConnectError:
|
|
|
|
|
|
|
|
return {"ok": False, "error": f"Connection refused at {url} — is Ollama running?", "models": []}
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
|
|
return {"ok": False, "error": str(exc), "models": []}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/test-chat")
|
|
|
|
@app.post("/test-chat")
|
|
|
|
async def test_chat(request: Request) -> dict:
|
|
|
|
async def test_chat(request: Request) -> dict:
|
|
|
|
cfg = request.app.state.yaml_config
|
|
|
|
cfg = request.app.state.yaml_config
|
|
|
|
|
|
|
|
pool = request.app.state.pool
|
|
|
|
data = await request.json()
|
|
|
|
data = await request.json()
|
|
|
|
message = (data.get("message") or "Say hello in one sentence.").strip()
|
|
|
|
message = (data.get("message") or "Say hello in one sentence.").strip()
|
|
|
|
model = (data.get("model") or "").strip()
|
|
|
|
model = (data.get("model") or "").strip()
|
|
|
|
upstream = cfg.get("upstream_openai", "")
|
|
|
|
upstream = cfg.get("upstream_openai", "")
|
|
|
|
|
|
|
|
|
|
|
|
if not upstream:
|
|
|
|
# Load configured models from DB so the UI can refresh the dropdown
|
|
|
|
return {"ok": False, "error": "upstream_openai not configured"}
|
|
|
|
async with pool.acquire() as conn:
|
|
|
|
|
|
|
|
db_rows = await conn.fetch(
|
|
|
|
# Always fetch the available model list so the UI can show it
|
|
|
|
"SELECT model_name, base_url FROM models WHERE provider IN ('lm-studio', 'openai') ORDER BY id"
|
|
|
|
available: list[str] = []
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
available = [r["model_name"] for r in db_rows]
|
|
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
|
|
db_model_map = {r["model_name"]: r["base_url"] or "" for r in db_rows}
|
|
|
|
mr = await client.get(f"{upstream.rstrip('/')}/v1/models")
|
|
|
|
|
|
|
|
if mr.is_success:
|
|
|
|
|
|
|
|
available = sorted(m.get("id", "") for m in mr.json().get("data", []))
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not model:
|
|
|
|
if not model:
|
|
|
|
if not available:
|
|
|
|
if not available:
|
|
|
|
return {"ok": False, "error": f"No model specified and none found at {upstream}/v1/models", "available": []}
|
|
|
|
return {"ok": False, "error": "No lm-studio/openai models configured. Add one via Model Manager first.", "available": []}
|
|
|
|
model = available[0]
|
|
|
|
model = available[0]
|
|
|
|
|
|
|
|
|
|
|
|
url = f"{upstream.rstrip('/')}/v1/chat/completions"
|
|
|
|
# Resolve upstream: prefer the model's own base_url from DB, fall back to upstream_openai config
|
|
|
|
|
|
|
|
raw_base = db_model_map.get(model, "") or upstream
|
|
|
|
|
|
|
|
if not raw_base:
|
|
|
|
|
|
|
|
return {"ok": False, "error": "upstream_openai not configured and model has no base_url", "available": available}
|
|
|
|
|
|
|
|
# Strip /v1 suffix — we append /v1/chat/completions ourselves
|
|
|
|
|
|
|
|
effective_upstream = raw_base.rstrip("/")
|
|
|
|
|
|
|
|
if effective_upstream.endswith("/v1"):
|
|
|
|
|
|
|
|
effective_upstream = effective_upstream[:-3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url = f"{effective_upstream}/v1/chat/completions"
|
|
|
|
body = {
|
|
|
|
body = {
|
|
|
|
"model": model,
|
|
|
|
"model": model,
|
|
|
|
"stream": False,
|
|
|
|
"stream": False,
|
|
|
@@ -2222,11 +2222,12 @@ async def test_chat(request: Request) -> dict:
|
|
|
|
if not r.is_success:
|
|
|
|
if not r.is_success:
|
|
|
|
return {"ok": False, "error": f"HTTP {r.status_code}: {r.text[:400]}", "url": url, "model": model, "ms": ms, "available": available}
|
|
|
|
return {"ok": False, "error": f"HTTP {r.status_code}: {r.text[:400]}", "url": url, "model": model, "ms": ms, "available": available}
|
|
|
|
resp = r.json()
|
|
|
|
resp = r.json()
|
|
|
|
reply = resp.get("choices", [{}])[0].get("message", {}).get("content", "(empty)")
|
|
|
|
choices = resp.get("choices") or []
|
|
|
|
|
|
|
|
reply = choices[0].get("message", {}).get("content", "(empty)") if choices else "(empty response)"
|
|
|
|
return {"ok": True, "reply": reply, "model": model, "url": url, "ms": ms, "available": available}
|
|
|
|
return {"ok": True, "reply": reply, "model": model, "url": url, "ms": ms, "available": available}
|
|
|
|
except httpx.ConnectError as exc:
|
|
|
|
except httpx.ConnectError as exc:
|
|
|
|
ms = int((time.perf_counter() - t0) * 1000)
|
|
|
|
ms = int((time.perf_counter() - t0) * 1000)
|
|
|
|
return {"ok": False, "error": f"Connection refused — is Ollama running at {upstream}? ({exc})", "url": url, "ms": ms, "available": available}
|
|
|
|
return {"ok": False, "error": f"Connection refused at {effective_upstream} ({exc})", "url": url, "ms": ms, "available": available}
|
|
|
|
except httpx.TimeoutException:
|
|
|
|
except httpx.TimeoutException:
|
|
|
|
ms = int((time.perf_counter() - t0) * 1000)
|
|
|
|
ms = int((time.perf_counter() - t0) * 1000)
|
|
|
|
return {"ok": False, "error": f"Request timed out after {ms}ms", "url": url, "ms": ms, "available": available}
|
|
|
|
return {"ok": False, "error": f"Request timed out after {ms}ms", "url": url, "ms": ms, "available": available}
|
|
|
@@ -3705,7 +3706,7 @@ ADMIN_HTML = """<!DOCTYPE html>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Test Chat</h2>
|
|
|
|
<h2>Test Chat</h2>
|
|
|
|
<p style="font-size:0.83em;color:#666;margin-bottom:0.8em">
|
|
|
|
<p style="font-size:0.83em;color:#666;margin-bottom:0.8em">
|
|
|
|
Send a message directly to <code>upstream_openai</code> (Ollama) to verify the connection end-to-end.
|
|
|
|
Send a test message via a configured model (lm-studio / openai) to verify the connection end-to-end.
|
|
|
|
</p>
|
|
|
|
</p>
|
|
|
|
<div id="tc-models-row" style="font-size:0.82em;color:#555;margin-bottom:0.6em">
|
|
|
|
<div id="tc-models-row" style="font-size:0.82em;color:#555;margin-bottom:0.6em">
|
|
|
|
<span id="tc-models-label">Loading available models…</span>
|
|
|
|
<span id="tc-models-label">Loading available models…</span>
|
|
|
|