Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions codec_cookbook/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""CODEC Cookbook — local-model lifecycle management (scan → recommend →
download → serve → list → stop) for the M1 Ultra workstation.

This is the NON-skill helper package. All OS / subprocess / PM2 / network work
lives here so the six `skills/cookbook_*.py` skill files stay thin (import only
this package + stdlib-safe modules) and therefore pass the `SkillRegistry`
load-time AST safety gate (`codec_config.is_dangerous_skill_code`, which
forbids `os`/`subprocess`/`socket`/... in skill files).

HARD SAFETY CONTRACT (enforced in serve.py):
* Cookbook only ever stops a PM2 process it started, in the `cookbook-`
namespace, after explicit confirm=True.
* It never binds to or stops the protected ports (8083/8090/8094/9223/5678)
or any port currently bound by a non-cookbook process (live `pm2 jlist`
+ socket probe at call time).
* Its own serve range is 8110-8119.
* It never issues docker stop/rm, never changes an existing service's port,
never restarts a running service.
"""

__all__ = ["catalog", "probe", "fit", "serve", "download"]
66 changes: 66 additions & 0 deletions codec_cookbook/args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tiny argument parser shared by the thin cookbook_* skills.

Skills receive a single `task` string (CODEC's `run(task, app, ctx)` contract),
so structured options are parsed out of it here. `re` only — keeps the skill
files AST-safe (no os/subprocess) and DRY.
"""
from __future__ import annotations

import re
from typing import Optional

from . import catalog


def parse_model_id(task: str) -> Optional[str]:
"""First known catalog id that appears as a whole token in `task`."""
if not task:
return None
tokens = re.findall(r"[A-Za-z0-9_.\-]+", task.lower())
known = set(catalog.ids())
for tok in tokens:
if tok in known:
return tok
return None


def parse_context(task: str, default: int = 8192) -> int:
"""`context 8192`, `context=8192`, `ctx 4096`, `context_length: 16384`."""
m = re.search(r"(?:context|ctx)(?:[ _]?length)?\s*[=:]?\s*(\d{3,7})", (task or "").lower())
if m:
try:
return int(m.group(1))
except ValueError:
pass
return default


def parse_flag(task: str, flag: str) -> bool:
"""True if `flag` appears as a whole word, or `flag=true`/`flag:yes`."""
low = (task or "").lower()
if re.search(rf"\b{re.escape(flag)}\b\s*[=:]\s*(?:true|yes|1|on)\b", low):
return True
if re.search(rf"\b{re.escape(flag)}=(?:true|yes|1|on)\b", low):
return True
return bool(re.search(rf"\b{re.escape(flag)}\b", low))


def parse_port(task: str) -> Optional[int]:
"""A bare port number in the Cookbook range (8110-8119) mentioned in `task`."""
for m in re.findall(r"\b(\d{4,5})\b", task or ""):
try:
p = int(m)
except ValueError:
continue
if 8110 <= p <= 8119:
return p
return None


def parse_role(task: str) -> Optional[str]:
"""A recommendation role mentioned in `task` (chat/reason/code/max/fast/tiny)."""
low = (task or "").lower()
for role in ("chat", "reason", "code", "max", "fast", "tiny"):
if re.search(rf"\b{role}\b", low):
return role
return None
7 changes: 7 additions & 0 deletions codec_cookbook/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{"id":"qwen3-30b-a3b","hf_repo":"mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit","backend":"mlx","roles":["chat","reason"],"anchor_gb":17.2},
{"id":"qwen3-coder-30b","hf_repo":"mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit","backend":"mlx","roles":["code"],"anchor_gb":17.2},
{"id":"qwen3-next-80b","hf_repo":"mlx-community/Qwen3-Next-80B-A3B-Instruct-4bit","backend":"mlx","roles":["max"],"anchor_gb":42.0},
{"id":"qwen3-4b","hf_repo":"mlx-community/Qwen3-4B-Instruct-2507-4bit","backend":"mlx","roles":["fast"]},
{"id":"llama32-3b","hf_repo":"mlx-community/Llama-3.2-3B-Instruct-4bit","backend":"mlx","roles":["tiny"]}
]
57 changes: 57 additions & 0 deletions codec_cookbook/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Cookbook model catalog — verified, downloadable options.

Loaded from `catalog.json` (next to this file). The primary `qwen3.6@8083`
model that the live stack serves is intentionally NOT in here — Cookbook never
manages it.
"""
from __future__ import annotations

import json
import os
from functools import lru_cache
from typing import Optional

_CATALOG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "catalog.json")


@lru_cache(maxsize=1)
def _load() -> list[dict]:
with open(_CATALOG_PATH, encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("catalog.json must be a JSON array")
return data


def all_entries() -> list[dict]:
"""Return a copy of every catalog entry."""
return [dict(e) for e in _load()]


def ids() -> list[str]:
"""Return the list of known model ids."""
return [e["id"] for e in _load()]


def get(model_id: Optional[str]) -> dict:
"""Return the catalog entry for `model_id`. Raises KeyError if unknown so
callers fail loud rather than serving a mystery repo."""
if not model_id:
raise KeyError("no model_id given")
for e in _load():
if e["id"] == model_id:
return dict(e)
raise KeyError(f"unknown model id: {model_id!r} (known: {', '.join(ids())})")


def find(model_id: Optional[str]) -> Optional[dict]:
"""Like get() but returns None instead of raising — for arg-parsing paths."""
try:
return get(model_id)
except KeyError:
return None


def by_role(role: str) -> list[dict]:
"""All entries advertising a given role (chat/reason/code/max/fast/tiny)."""
return [dict(e) for e in _load() if role in e.get("roles", [])]
130 changes: 130 additions & 0 deletions codec_cookbook/download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Cookbook model downloads — detached Hugging Face jobs + status polling.

Each download runs as a DETACHED subprocess (start_new_session) so it survives
across skill calls (every skill `run()` is a fresh, short-lived call). The child
writes its own per-repo status file with stdlib only (no dependency on this repo
being importable in the child), and status() reconciles a dead pid to
'interrupted'. Downloads land in the standard HF cache — they never touch the
running stack.
"""
from __future__ import annotations

import json
import logging
import os
import subprocess
import sys
import tempfile
import time

log = logging.getLogger("codec_cookbook.download")

DL_DIR = os.path.expanduser("~/.codec/cookbook/downloads")

# Self-contained child: writes {repo,state,pid,...} to argv[2] via tmp+replace.
_CHILD = r"""
import sys, json, os, time, tempfile
repo, sf = sys.argv[1], sys.argv[2]
def w(state, **kw):
d = {"repo": repo, "state": state, "pid": os.getpid(), "updated_at": time.time()}
d.update(kw)
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(sf), suffix=".tmp")
with os.fdopen(fd, "w") as f:
json.dump(d, f)
os.replace(tmp, sf)
w("running", started_at=time.time())
try:
from huggingface_hub import snapshot_download
p = snapshot_download(repo)
w("done", path=p, finished_at=time.time())
except Exception as e:
w("error", error=str(e)[:500], finished_at=time.time())
"""


def _slug(repo: str) -> str:
return repo.replace("/", "__").replace(":", "_")


def _status_file(repo: str) -> str:
return os.path.join(DL_DIR, _slug(repo) + ".json")


def _read_status(repo: str) -> dict | None:
try:
with open(_status_file(repo), encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return None


def _pid_alive(pid) -> bool:
try:
os.kill(int(pid), 0)
return True
except (OSError, ValueError, TypeError):
return False


def _write_initial(repo: str, pid: int | None = None) -> None:
os.makedirs(DL_DIR, exist_ok=True)
sf = _status_file(repo)
data = {"repo": repo, "state": "starting", "pid": pid, "updated_at": time.time()}
fd, tmp = tempfile.mkstemp(dir=DL_DIR, suffix=".tmp")
with os.fdopen(fd, "w") as f:
json.dump(data, f)
os.replace(tmp, sf)


def start(repo: str) -> dict:
"""Begin (or report) a download of `repo`. Idempotent: if a job is already
running, returns its current status instead of spawning a duplicate."""
if not repo:
return {"state": "error", "error": "no repo given"}
cur = status(repo)
if cur.get("state") in ("starting", "running"):
return cur # already in flight
os.makedirs(DL_DIR, exist_ok=True)
_write_initial(repo)
try:
proc = subprocess.Popen(
[sys.executable, "-c", _CHILD, repo, _status_file(repo)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True, # detach: survives the parent skill call
)
except Exception as e:
return {"repo": repo, "state": "error", "error": f"spawn failed: {e}"}
_write_initial(repo, pid=proc.pid)
return {"repo": repo, "state": "starting", "pid": proc.pid}


def status(repo: str) -> dict:
"""Current download state: not_started | starting | running | done | error |
interrupted. Reconciles a dead pid (child crashed/killed) to 'interrupted'."""
rec = _read_status(repo)
if rec is None:
return {"repo": repo, "state": "not_started"}
state = rec.get("state")
if state in ("starting", "running"):
pid = rec.get("pid")
if pid is not None and not _pid_alive(pid):
return {**rec, "state": "interrupted",
"detail": "download process is no longer running"}
return rec


def list_downloads() -> list[dict]:
"""All known download jobs (one per status file in DL_DIR)."""
out = []
try:
for fn in os.listdir(DL_DIR):
if fn.endswith(".json"):
try:
with open(os.path.join(DL_DIR, fn), encoding="utf-8") as f:
rec = json.load(f)
out.append(status(rec.get("repo", "")))
except (OSError, json.JSONDecodeError):
continue
except OSError:
pass
return out
Loading