mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-28 15:45:22 -04:00
Cookbook launch and gallery upload fixes
This commit is contained in:
@@ -2446,6 +2446,17 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
|
||||
disk_tasks = on_disk.get("tasks") or [] if isinstance(on_disk, dict) else []
|
||||
incoming_tasks = data.get("tasks") if isinstance(data.get("tasks"), list) else []
|
||||
incoming_removed = data.get("removedTasks") if isinstance(data.get("removedTasks"), dict) else {}
|
||||
disk_removed = on_disk.get("removedTasks") if isinstance(on_disk, dict) and isinstance(on_disk.get("removedTasks"), dict) else {}
|
||||
removed_tasks = {**disk_removed, **incoming_removed}
|
||||
data["removedTasks"] = removed_tasks
|
||||
removed_ids = set(removed_tasks.keys())
|
||||
if removed_ids:
|
||||
incoming_tasks = [
|
||||
t for t in incoming_tasks
|
||||
if not (isinstance(t, dict) and t.get("sessionId") in removed_ids)
|
||||
]
|
||||
data["tasks"] = incoming_tasks
|
||||
# Anti-poisoning guard: a stale browser tab can keep POSTing a
|
||||
# download task as status='done' from before the strict-finish
|
||||
# fix landed, undoing any server-side correction. For each
|
||||
@@ -2483,6 +2494,8 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
sid = t.get("sessionId")
|
||||
if not sid or sid in incoming_ids:
|
||||
continue # client's version wins
|
||||
if sid in removed_ids:
|
||||
continue # intentional cross-device clear/remove
|
||||
ts = t.get("ts") or 0
|
||||
if isinstance(ts, (int, float)) and (now_ms - ts) <= RACE_WINDOW_MS:
|
||||
preserved.append(t)
|
||||
|
||||
+58
-4
@@ -714,6 +714,16 @@ def _effective_endpoint_kind(ep: Any, base_url: str) -> str:
|
||||
return "auto"
|
||||
|
||||
|
||||
def _is_loading_model_response(resp: Any) -> bool:
|
||||
if getattr(resp, "status_code", None) != 503:
|
||||
return False
|
||||
try:
|
||||
body = resp.text or ""
|
||||
except Exception:
|
||||
body = ""
|
||||
return "loading model" in body.lower()
|
||||
|
||||
|
||||
|
||||
def _probe_endpoint(base_url: str, api_key: str = None, timeout: int = 5) -> List[str]:
|
||||
"""Probe a base URL's /models endpoint and return list of model IDs.
|
||||
@@ -778,6 +788,9 @@ def _probe_endpoint(base_url: str, api_key: str = None, timeout: int = 5) -> Lis
|
||||
models.append(_e)
|
||||
return [m for m in models if _is_chat_model(m)]
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response is not None and _is_loading_model_response(e.response):
|
||||
logger.info(f"Endpoint still loading model at {url}")
|
||||
return []
|
||||
if api_key:
|
||||
status = e.response.status_code if e.response is not None else "unknown"
|
||||
logger.warning(f"Failed to probe {url} with API key: HTTP {status}")
|
||||
@@ -827,6 +840,15 @@ def _ping_endpoint(base_url: str, api_key: str = None, timeout: float = 1.5) ->
|
||||
or "ollama" in (parsed_base.hostname or "").lower()
|
||||
)
|
||||
|
||||
def _is_loading_model_response(r) -> bool:
|
||||
if getattr(r, "status_code", None) != 503:
|
||||
return False
|
||||
try:
|
||||
body = r.text or ""
|
||||
except Exception:
|
||||
body = ""
|
||||
return "loading model" in body.lower()
|
||||
|
||||
def _result_from_response(r) -> Dict[str, Any]:
|
||||
if 300 <= r.status_code < 400:
|
||||
loc = r.headers.get("location", "")
|
||||
@@ -843,6 +865,13 @@ def _ping_endpoint(base_url: str, api_key: str = None, timeout: float = 1.5) ->
|
||||
"status_code": r.status_code,
|
||||
"error": None,
|
||||
}
|
||||
if _is_loading_model_response(r):
|
||||
return {
|
||||
"reachable": True,
|
||||
"loading": True,
|
||||
"status_code": r.status_code,
|
||||
"error": "Loading model",
|
||||
}
|
||||
return {"reachable": False, "status_code": r.status_code, "error": f"HTTP {r.status_code}"}
|
||||
|
||||
last_error: Optional[str] = None
|
||||
@@ -1427,7 +1456,7 @@ def setup_model_routes(model_discovery):
|
||||
t0 = _time.time()
|
||||
ping = _ping_endpoint(base, ep.api_key, timeout=1.5)
|
||||
entry["latency_ms"] = round((_time.time() - t0) * 1000)
|
||||
entry["status"] = "online" if ping.get("reachable") or cached_count else "offline"
|
||||
entry["status"] = "loading" if ping.get("loading") else ("online" if ping.get("reachable") or cached_count else "offline")
|
||||
entry["error"] = ping.get("error")
|
||||
entry["model_count"] = cached_count or (len(ANTHROPIC_MODELS) if provider == "anthropic" else 0)
|
||||
except Exception as e:
|
||||
@@ -1606,7 +1635,32 @@ def setup_model_routes(model_discovery):
|
||||
ping_timeout = 10.0 if _classify_endpoint(base_for_ping, kind_for_ping) == "local" else 3.5
|
||||
ping = _ping_endpoint(r.base_url, r.api_key, timeout=ping_timeout)
|
||||
if ping.get("reachable"):
|
||||
status = "empty"
|
||||
status = "loading" if ping.get("loading") else "empty"
|
||||
if ping.get("loading"):
|
||||
base = _normalize_base(r.base_url)
|
||||
kind = _effective_endpoint_kind(r, base)
|
||||
results.append({
|
||||
"id": r.id,
|
||||
"name": r.name,
|
||||
"base_url": r.base_url,
|
||||
"has_key": bool(r.api_key),
|
||||
"api_key_fingerprint": _api_key_fingerprint(r.api_key),
|
||||
"is_enabled": r.is_enabled,
|
||||
"models": visible,
|
||||
"pinned_models": pinned,
|
||||
"hidden_count": len(hidden),
|
||||
"online": True,
|
||||
"status": status,
|
||||
"ping_error": (ping or {}).get("error") if ping else None,
|
||||
"model_type": getattr(r, "model_type", None) or "llm",
|
||||
"supports_tools": getattr(r, "supports_tools", None),
|
||||
"endpoint_kind": kind,
|
||||
"category": _classify_endpoint(base, kind),
|
||||
"model_refresh_mode": _endpoint_refresh_mode(r, kind),
|
||||
"model_refresh_interval": getattr(r, "model_refresh_interval", None),
|
||||
"model_refresh_timeout": getattr(r, "model_refresh_timeout", None),
|
||||
})
|
||||
continue
|
||||
# Best-effort: if the probe came back reachable, try
|
||||
# to populate cached_models in the background so the
|
||||
# NEXT picker load shows "online" instead of "empty".
|
||||
@@ -1859,7 +1913,7 @@ def setup_model_routes(model_discovery):
|
||||
"models": _merge_model_ids(model_ids, _pinned),
|
||||
"pinned_models": _pinned,
|
||||
"online": bool(model_ids) or bool(_pinned) or bool(ping.get("reachable")),
|
||||
"status": "online" if (model_ids or _pinned) else ("empty" if ping.get("reachable") else "offline"),
|
||||
"status": "online" if (model_ids or _pinned) else ("loading" if ping.get("loading") else ("empty" if ping.get("reachable") else "offline")),
|
||||
"ping_error": ping.get("error") if ping else None,
|
||||
"endpoint_kind": requested_kind,
|
||||
"category": _classify_endpoint(base_url, requested_kind),
|
||||
@@ -1888,7 +1942,7 @@ def setup_model_routes(model_discovery):
|
||||
return {
|
||||
"base_url": base_url,
|
||||
"online": bool(models) or bool(ping.get("reachable")),
|
||||
"status": "online" if models else ("empty" if ping.get("reachable") else "offline"),
|
||||
"status": "online" if models else ("loading" if ping.get("loading") else ("empty" if ping.get("reachable") else "offline")),
|
||||
"ping_error": ping.get("error") if ping else None,
|
||||
"models": models,
|
||||
"count": len(models),
|
||||
|
||||
@@ -1108,7 +1108,7 @@ def setup_shell_routes() -> APIRouter:
|
||||
{
|
||||
"name": "llama_cpp",
|
||||
"pip": "llama-cpp-python[server]",
|
||||
"desc": "Serve GGUF models via llama.cpp",
|
||||
"desc": "Great for single-GPU or CPU inference with GGUF models",
|
||||
"category": "LLM",
|
||||
"target": "remote",
|
||||
# Build-toolchain prereqs. Cookbook's launch bootstrap
|
||||
@@ -1129,7 +1129,7 @@ def setup_shell_routes() -> APIRouter:
|
||||
{
|
||||
"name": "vllm",
|
||||
"pip": "vllm",
|
||||
"desc": "High-throughput LLM serving engine",
|
||||
"desc": "Great for high-throughput multi-GPU inference",
|
||||
"category": "LLM",
|
||||
"target": "remote",
|
||||
},
|
||||
|
||||
+76
-3
@@ -3,11 +3,16 @@ import os
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Request, File, UploadFile, HTTPException
|
||||
from typing import List
|
||||
import logging
|
||||
from core.middleware import require_admin
|
||||
from core.database import SessionLocal, GalleryImage
|
||||
from src.auth_helpers import effective_user
|
||||
from src.constants import GENERATED_IMAGES_DIR
|
||||
from src.upload_handler import count_recent_uploads
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -50,6 +55,69 @@ def setup_upload_routes(upload_handler):
|
||||
raise HTTPException(404, "File not found")
|
||||
|
||||
raise HTTPException(404, "File not found")
|
||||
|
||||
def _promote_chat_image_to_gallery(meta: dict, owner: str | None) -> str | None:
|
||||
"""Make chat-uploaded images visible in Gallery without changing chat storage."""
|
||||
is_image_file = getattr(upload_handler, "is_image_file", None)
|
||||
if not callable(is_image_file):
|
||||
return None
|
||||
if not is_image_file(meta.get("name", ""), meta.get("mime", "")):
|
||||
return None
|
||||
|
||||
source_path = meta.get("path")
|
||||
if not source_path or not os.path.isfile(source_path):
|
||||
return None
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
file_hash = meta.get("hash")
|
||||
if file_hash:
|
||||
q = db.query(GalleryImage).filter(
|
||||
GalleryImage.file_hash == file_hash,
|
||||
GalleryImage.is_active == True, # noqa: E712
|
||||
)
|
||||
if owner:
|
||||
q = q.filter(GalleryImage.owner == owner)
|
||||
existing = q.first()
|
||||
if existing:
|
||||
return existing.id
|
||||
|
||||
image_dir = Path(GENERATED_IMAGES_DIR)
|
||||
image_dir.mkdir(parents=True, exist_ok=True)
|
||||
ext = Path(meta.get("name") or source_path).suffix.lower()
|
||||
if ext not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
|
||||
mime_ext = {
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/jpg": ".jpg",
|
||||
"image/webp": ".webp",
|
||||
"image/gif": ".gif",
|
||||
}.get(meta.get("mime", ""))
|
||||
ext = mime_ext or ".png"
|
||||
filename = f"{uuid.uuid4().hex[:12]}{ext}"
|
||||
dest_path = image_dir / filename
|
||||
shutil.copy2(source_path, dest_path)
|
||||
|
||||
image_id = str(uuid.uuid4())
|
||||
db.add(GalleryImage(
|
||||
id=image_id,
|
||||
filename=filename,
|
||||
prompt=meta.get("name") or "Chat upload",
|
||||
model="chat-upload",
|
||||
owner=owner,
|
||||
file_hash=file_hash,
|
||||
width=meta.get("width"),
|
||||
height=meta.get("height"),
|
||||
file_size=meta.get("size"),
|
||||
))
|
||||
db.commit()
|
||||
return image_id
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.warning("Failed to add chat image upload to gallery: %s", e)
|
||||
return None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@router.post("")
|
||||
async def api_upload(request: Request, files: List[UploadFile] = File(...)):
|
||||
@@ -78,8 +146,10 @@ def setup_upload_routes(upload_handler):
|
||||
|
||||
for u in files:
|
||||
try:
|
||||
meta = upload_handler.save_upload(u, client_ip, owner=effective_user(request))
|
||||
out.append({
|
||||
owner = effective_user(request)
|
||||
meta = upload_handler.save_upload(u, client_ip, owner=owner)
|
||||
gallery_id = _promote_chat_image_to_gallery(meta, owner)
|
||||
item = {
|
||||
"id": meta["id"],
|
||||
"name": meta["name"],
|
||||
"mime": meta["mime"],
|
||||
@@ -89,7 +159,10 @@ def setup_upload_routes(upload_handler):
|
||||
"width": meta.get("width"),
|
||||
"height": meta.get("height"),
|
||||
"is_duplicate": meta.get("is_duplicate", False)
|
||||
})
|
||||
}
|
||||
if gallery_id:
|
||||
item["gallery_id"] = gallery_id
|
||||
out.append(item)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user