mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-23 08:32:20 +02:00
Merge verified Odysseus fixes
This commit is contained in:
+186
-1
@@ -33,6 +33,57 @@ def _validate_cookbook_ssh_target(remote_host: Any, ssh_port: Any = "") -> tuple
|
||||
return remote, sport
|
||||
|
||||
|
||||
def _cookbook_label_key(value: Any) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", str(value or "").lower())
|
||||
|
||||
|
||||
def _cookbook_is_exact_repo_id(value: Any) -> bool:
|
||||
return bool(re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", str(value or "").strip()))
|
||||
|
||||
|
||||
def _cookbook_match_saved_preset(query: str, presets: List[Any], host: str = "") -> Optional[Dict[str, Any]]:
|
||||
"""Resolve a user-facing model label to a saved serve preset.
|
||||
|
||||
The launch agent should be callable directly. If the model says
|
||||
`repo_id="Qwen3.6-27B-AEON"` because the user used the short UI label, do
|
||||
not force it through `list_serve_presets`; match the saved preset inside
|
||||
the autopilot and let `do_serve_preset` reuse the known-good command.
|
||||
"""
|
||||
q = _cookbook_label_key(query)
|
||||
if not q:
|
||||
return None
|
||||
host = str(host or "")
|
||||
exact_repo = _cookbook_is_exact_repo_id(query)
|
||||
candidates: List[tuple[int, Dict[str, Any]]] = []
|
||||
for p in presets or []:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
name = str(p.get("name") or "")
|
||||
model = str(p.get("model") or p.get("modelId") or "")
|
||||
phost = str(p.get("host") or p.get("remoteHost") or "")
|
||||
haystacks = [_cookbook_label_key(name), _cookbook_label_key(model)]
|
||||
if exact_repo:
|
||||
# If the user gave a real HF repo, only reuse a preset that names
|
||||
# that exact repo/label. A substring match here is dangerous:
|
||||
# cyankiwi/Qwen3.5-122B-A10B-AWQ-8bit must not launch the saved
|
||||
# generic Qwen/Qwen3.5-122B-A10B preset.
|
||||
if not any(h and q == h for h in haystacks):
|
||||
continue
|
||||
else:
|
||||
if not any(h and (q == h or q in h or h in q) for h in haystacks):
|
||||
continue
|
||||
score = 10
|
||||
if host and phost == host:
|
||||
score += 20
|
||||
if q in haystacks:
|
||||
score += 10
|
||||
candidates.append((score, p))
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda item: item[0], reverse=True)
|
||||
return candidates[0][1]
|
||||
|
||||
|
||||
async def _cookbook_servers() -> Dict[str, Any]:
|
||||
"""Return the cookbook's configured servers + the currently-selected
|
||||
default host. Shape: {default_host, hosts: [{host, platform, env, envPath}]}.
|
||||
@@ -200,7 +251,7 @@ async def _ensure_served_endpoint(
|
||||
port = _infer_serve_port(cmd)
|
||||
base_url = f"http://{endpoint_host}:{port}/v1"
|
||||
short_name = model.split("/")[-1] if "/" in model else model
|
||||
is_image = "diffusion_server.py" in (cmd or "")
|
||||
is_image = "diffusion_server.py" in (cmd or "") or "mlx_image_server.py" in (cmd or "")
|
||||
payload = {
|
||||
"name": short_name if not is_image else f"{short_name} (image)",
|
||||
"base_url": base_url,
|
||||
@@ -315,6 +366,7 @@ async def _cookbook_register_task(
|
||||
_MODEL_PROCESS_PATTERNS = [
|
||||
("vLLM", ["vllm.entrypoints", "vllm serve", "/vllm/", "vllm-openai"]),
|
||||
("SGLang", ["sglang.launch_server", "sglang/launch_server"]),
|
||||
("MLX Image", ["mlx_image_server.py", "mflux-generate-qwen", "mflux-generate"]),
|
||||
("MLX", ["mlx_lm.server", "mlx-lm"]),
|
||||
("llama.cpp", ["llama-server", "llama_cpp_server", "llamacppserver"]),
|
||||
("Ollama", ["ollama serve", "ollama runner", "/ollama "]),
|
||||
@@ -358,6 +410,139 @@ def _cookbook_apply_retry_suggestion(cmd: str, suggestion: Dict[str, Any]) -> st
|
||||
return cmd
|
||||
|
||||
|
||||
def _cookbook_engine_from_model_info(repo_id: str, info: Optional[Dict[str, Any]], host_meta: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Choose a conservative serve engine from repo metadata and target host.
|
||||
|
||||
This is intentionally heuristic: the model card / file list tells us the
|
||||
official repo and likely format, while the actual serve command still goes
|
||||
through Cookbook and is diagnosed/retried after launch.
|
||||
"""
|
||||
rid = (repo_id or "").lower()
|
||||
info = info or {}
|
||||
host_meta = host_meta or {}
|
||||
platform = (host_meta.get("platform") or "").lower()
|
||||
tags = {str(t).lower() for t in (info.get("tags") or []) if t is not None}
|
||||
siblings = [str(s).lower() for s in (info.get("siblings") or []) if s is not None]
|
||||
files = " ".join(siblings)
|
||||
is_image = (
|
||||
"diffusers" in tags
|
||||
or "text-to-image" in tags
|
||||
or "image-to-image" in tags
|
||||
or any(k in rid for k in ("qwen-image", "z-image", "flux", "stable-diffusion", "sdxl", "hidream", "boogu", "krea-2"))
|
||||
)
|
||||
is_mlx_image = is_image and ("mlx" in tags or "mlx" in rid or "mlx-community/" in rid)
|
||||
if is_mlx_image:
|
||||
return "mlx_image"
|
||||
if is_image:
|
||||
return "diffusers"
|
||||
if "mlx" in tags or "mlx" in rid or "mlx-community/" in rid or platform in {"macos", "darwin"}:
|
||||
return "mlx"
|
||||
if "gguf" in tags or "gguf" in rid or ".gguf" in files:
|
||||
return "llama.cpp"
|
||||
if "sglang" in tags or "sglang" in rid:
|
||||
return "sglang"
|
||||
if any(q in rid or q in files or q in tags for q in ("awq", "fp8", "gptq", "bnb", "bitsandbytes")):
|
||||
return "vllm"
|
||||
return "vllm"
|
||||
|
||||
|
||||
def _cookbook_default_launch_cmd(repo_id: str, engine: str, *, port: int = 8000, info: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Build a simple first-attempt command for a selected engine."""
|
||||
engine = (engine or "vllm").lower()
|
||||
port = int(port or 8000)
|
||||
if engine in {"mlx", "mlx-lm", "mlx_lm"}:
|
||||
return f"python3 -m mlx_lm.server --model {repo_id} --host 0.0.0.0 --port {port}"
|
||||
if engine in {"mlx_image", "mlx-image", "mflux"}:
|
||||
return f"python3 scripts/mlx_image_server.py --model {repo_id} --host 0.0.0.0 --port {port}"
|
||||
if engine in {"diffusers", "diffusion", "image"}:
|
||||
return f"python3 scripts/diffusion_server.py --model {repo_id} --host 0.0.0.0 --port {port}"
|
||||
if engine in {"sglang", "sgl"}:
|
||||
return f"python3 -m sglang.launch_server --model-path {repo_id} --host 0.0.0.0 --port {port}"
|
||||
if engine in {"llama.cpp", "llamacpp", "llama"}:
|
||||
siblings = [str(s) for s in ((info or {}).get("siblings") or []) if str(s).lower().endswith(".gguf")]
|
||||
if siblings:
|
||||
# llama-server accepts HF repo + filename separately on recent builds.
|
||||
return f"llama-server -hf {repo_id} -hfr {siblings[0]} --host 0.0.0.0 --port {port}"
|
||||
return f"llama-server -hf {repo_id} --host 0.0.0.0 --port {port}"
|
||||
return f"vllm serve {repo_id} --host 0.0.0.0 --port {port}"
|
||||
|
||||
|
||||
async def _cookbook_hf_model_info(repo_id: str) -> Dict[str, Any]:
|
||||
"""Fetch lightweight official Hugging Face metadata for launch planning.
|
||||
|
||||
Uses the public HF API directly so this works even when huggingface_hub is
|
||||
not installed in Odysseus. Failures return a structured warning rather than
|
||||
blocking launch; cached/private/offline models can still be served.
|
||||
"""
|
||||
import httpx
|
||||
from routes.cookbook_helpers import load_stored_hf_token
|
||||
|
||||
repo_id = (repo_id or "").strip().strip("/")
|
||||
if not repo_id:
|
||||
return {"error": "repo_id is required"}
|
||||
headers: Dict[str, str] = {"Accept": "application/json"}
|
||||
token = load_stored_hf_token()
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
url = f"https://huggingface.co/api/models/{repo_id}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=20, follow_redirects=True) as client:
|
||||
resp = await client.get(url, headers=headers)
|
||||
if resp.status_code >= 400:
|
||||
return {
|
||||
"repo_id": repo_id,
|
||||
"url": f"https://huggingface.co/{repo_id}",
|
||||
"error": f"HF metadata lookup returned HTTP {resp.status_code}",
|
||||
}
|
||||
data = resp.json() if resp.content else {}
|
||||
except Exception as e:
|
||||
return {
|
||||
"repo_id": repo_id,
|
||||
"url": f"https://huggingface.co/{repo_id}",
|
||||
"error": f"HF metadata lookup failed: {e}",
|
||||
}
|
||||
siblings = []
|
||||
for s in data.get("siblings") or []:
|
||||
if isinstance(s, dict) and s.get("rfilename"):
|
||||
siblings.append(s["rfilename"])
|
||||
return {
|
||||
"repo_id": repo_id,
|
||||
"url": f"https://huggingface.co/{repo_id}",
|
||||
"pipeline_tag": data.get("pipeline_tag") or "",
|
||||
"library_name": data.get("library_name") or "",
|
||||
"tags": data.get("tags") or [],
|
||||
"sha": data.get("sha") or "",
|
||||
"private": bool(data.get("private")),
|
||||
"gated": data.get("gated"),
|
||||
"siblings": siblings[:500],
|
||||
"cardData": data.get("cardData") or {},
|
||||
}
|
||||
|
||||
|
||||
def _cookbook_host_meta(host: str, servers: Dict[str, Any]) -> Dict[str, Any]:
|
||||
for item in servers.get("hosts") or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if (item.get("host") or "") == (host or ""):
|
||||
return item
|
||||
return {}
|
||||
|
||||
|
||||
def _cookbook_find_task(tasks: List[Dict[str, Any]], session_id: str) -> Optional[Dict[str, Any]]:
|
||||
for task in tasks or []:
|
||||
if not isinstance(task, dict):
|
||||
continue
|
||||
if task.get("session_id") == session_id or task.get("sessionId") == session_id or task.get("id") == session_id:
|
||||
return task
|
||||
return None
|
||||
|
||||
|
||||
def _cookbook_phase(task: Optional[Dict[str, Any]]) -> str:
|
||||
if not task:
|
||||
return "unknown"
|
||||
return str(task.get("phase") or task.get("status") or "unknown").lower()
|
||||
|
||||
|
||||
def _scan_running_model_processes() -> List[Dict[str, Any]]:
|
||||
"""Scan /proc for running model server processes. Linux-only; returns
|
||||
[] on other platforms or if /proc isn't accessible. Each match returns
|
||||
|
||||
+29
-2
@@ -32,8 +32,35 @@ async def do_edit_image(content: str, owner: Optional[str] = None) -> Dict:
|
||||
async with httpx.AsyncClient(timeout=120) as client:
|
||||
resp = await client.post(f"{_INTERNAL_BASE}/api/gallery/{action}", json=payload)
|
||||
data = resp.json()
|
||||
if data.get("success") or data.get("id"):
|
||||
return {"output": f"Image edited ({action}). New image ID: {data.get('id', '?')}", "exit_code": 0}
|
||||
new_id = data.get("id") or data.get("image_id")
|
||||
if data.get("success") or new_id:
|
||||
result = {
|
||||
"output": f"Image edited ({action}). New image ID: {new_id or '?'}",
|
||||
"exit_code": 0,
|
||||
}
|
||||
if new_id:
|
||||
result["image_id"] = new_id
|
||||
try:
|
||||
from src.database import GalleryImage, SessionLocal
|
||||
db = SessionLocal()
|
||||
try:
|
||||
q = db.query(GalleryImage).filter(GalleryImage.id == new_id)
|
||||
if owner:
|
||||
q = q.filter(GalleryImage.owner == owner)
|
||||
img = q.first()
|
||||
if img and img.filename:
|
||||
result.update({
|
||||
"image_url": f"/api/generated-image/{img.filename}",
|
||||
"image_prompt": img.prompt or args.get("prompt") or action,
|
||||
"image_model": img.model or "edit_image",
|
||||
"image_size": img.size or "",
|
||||
"image_quality": img.quality or "",
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
return {"error": data.get("error", f"{action} failed"), "exit_code": 1}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "exit_code": 1}
|
||||
|
||||
+38
-15
@@ -280,7 +280,30 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
except ValueError:
|
||||
return {"error": "Invalid JSON arguments", "exit_code": 1}
|
||||
|
||||
if not args.get("action") and any(args.get(k) is not None for k in ("task", "description", "schedule", "time", "day_of_week")):
|
||||
args["action"] = "create"
|
||||
if args.get("task") and not args.get("name"):
|
||||
args["name"] = args["task"]
|
||||
if args.get("task") and not args.get("prompt"):
|
||||
args["prompt"] = args["task"]
|
||||
action = args.get("action", "list")
|
||||
if args.get("description") and not args.get("prompt"):
|
||||
args["prompt"] = args["description"]
|
||||
if args.get("time") and not args.get("scheduled_time"):
|
||||
args["scheduled_time"] = args["time"]
|
||||
if args.get("day_of_week") is not None and args.get("scheduled_day") is None:
|
||||
day = str(args.get("day_of_week")).strip().lower()
|
||||
days = {
|
||||
"monday": 0, "mon": 0,
|
||||
"tuesday": 1, "tue": 1, "tues": 1,
|
||||
"wednesday": 2, "wed": 2,
|
||||
"thursday": 3, "thu": 3, "thur": 3, "thurs": 3,
|
||||
"friday": 4, "fri": 4,
|
||||
"saturday": 5, "sat": 5,
|
||||
"sunday": 6, "sun": 6,
|
||||
}
|
||||
if day in days:
|
||||
args["scheduled_day"] = days[day]
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if action == "list":
|
||||
@@ -288,21 +311,21 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
if owner:
|
||||
q = q.filter(ScheduledTask.owner == owner)
|
||||
tasks = q.order_by(ScheduledTask.created_at.desc()).all()
|
||||
task_list = []
|
||||
for t in tasks:
|
||||
task_list.append({
|
||||
"id": t.id, "name": t.name, "status": t.status,
|
||||
"task_type": t.task_type or "llm",
|
||||
"action": t.action,
|
||||
"trigger_type": t.trigger_type or "schedule",
|
||||
"schedule": t.schedule,
|
||||
"trigger_event": t.trigger_event,
|
||||
"trigger_count": t.trigger_count,
|
||||
"next_run": t.next_run.isoformat() + "Z" if t.next_run else None,
|
||||
"last_run": t.last_run.isoformat() + "Z" if t.last_run else None,
|
||||
"run_count": t.run_count or 0,
|
||||
})
|
||||
return {"response": f"Found {len(task_list)} tasks", "tasks": task_list, "exit_code": 0}
|
||||
if not tasks:
|
||||
return {"response": "No scheduled tasks found.", "exit_code": 0}
|
||||
|
||||
lines = [f"Found {len(tasks)} tasks:"]
|
||||
for idx, t in enumerate(tasks, 1):
|
||||
bits = [t.status or "unknown"]
|
||||
if t.schedule:
|
||||
bits.append(str(t.schedule))
|
||||
if t.scheduled_time:
|
||||
bits.append(str(t.scheduled_time))
|
||||
if t.next_run:
|
||||
bits.append(f"next {t.next_run.isoformat()}Z")
|
||||
detail = ", ".join(bits)
|
||||
lines.append(f"{idx}. {t.name} ({t.id}) — {detail}")
|
||||
return {"response": "\n".join(lines), "exit_code": 0}
|
||||
|
||||
elif action == "create":
|
||||
task_type = args.get("task_type", "llm")
|
||||
|
||||
Reference in New Issue
Block a user