colibri: download the GLM-5.2 container from the Models interface like any other

The GLM-5.2 model is a DIRECTORY (an HF repo of int4 .safetensors shards + config +
tokenizer/MTP head), not a single file/GGUF — so it downloads via the normal
whole-repo snapshot and lives in the HF cache snapshot dir.

- routes: _COLIBRI_DEFAULT_MODELS catalog (mastouri/GLM-5.2-colibri-int4-g64-with-
  int8-mtp) + GET /admin/api/colibri/default-models; extend /admin/api/model-add to
  accept `backend` and `alias` so a downloaded repo registers as a colibri-backed
  model in one step.
- settings.html: a "Download the GLM-5.2 container" picker + button in the colibri
  card — full-repo snapshot (no file_pattern → a directory), then registers it as a
  colibri model addressable by the configured model id.
- backends/colibri: _resolve_container resolves an HF repo id → its local snapshot
  DIRECTORY (snapshot_download local_files_only), so a normally-downloaded model
  serves without any manual path wiring.
- models.html: add "colibri" to the per-model backend dropdown (manual add path).
- app: allow the new catalog endpoint through the no-auth admin allowlist.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
parent e2081488
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.1.69"
__version__ = "0.1.70"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even
......
......@@ -857,6 +857,40 @@ async def api_ds4_default_models(username: str = Depends(require_admin)):
return {"repo": _DS4_DEFAULT_REPO, "gguf_cache_dir": gguf_dir, "models": out}
# The official GLM-5.2 int4 container colibri serves — a full HF repo (a DIRECTORY of
# .safetensors shards + config + tokenizer + MTP head), downloaded like any other HF
# model (whole-repo snapshot). Requires the gs64 build with the int8 MTP head.
_COLIBRI_DEFAULT_MODELS = [
{"key": "glm-5.2-int4-g64-mtp",
"repo": "mastouri/GLM-5.2-colibri-int4-g64-with-int8-mtp",
"label": "GLM-5.2 int4 g64 + int8-MTP (~372 GB) — the colibri container",
"size_gb": 372},
]
@router.get("/admin/api/colibri/default-models", summary="List downloadable colibri default models")
async def api_colibri_default_models(username: str = Depends(require_admin)):
"""Catalog of the official GLM-5.2 container(s) colibri serves. The UI offers
these in a select; downloading one goes through the normal /admin/api/model-download
(a full-repo snapshot into a directory) and registers it as a colibri-backed model."""
model_id = "glm-5.2-colibri"
try:
if config_manager is not None and config_manager.config is not None:
model_id = (config_manager.config.colibri.model_id or model_id).strip()
except Exception:
pass
out = []
for m in _COLIBRI_DEFAULT_MODELS:
present = False
try:
from codai.backends.colibri import ColibriBackend
present = bool(ColibriBackend._hf_snapshot_dir(m["repo"]))
except Exception:
present = False
out.append({**m, "present": present})
return {"models": out, "model_id": model_id}
def _cancel_download_session(session_id: str) -> bool:
"""Cancel an active download by flagging the session and terminating its worker
process. Returns False if there is no such download session.
......@@ -1776,13 +1810,28 @@ async def api_model_add_known(request: Request, username: str = Depends(require_
if model_type not in valid:
model_type = "text_models"
# Optional: pin a serving backend (e.g. "colibri" for a GLM-5.2 container) and/or
# a client-facing alias on the new entry. When either is set the entry must be a
# dict rather than a bare path string.
backend = (data.get("backend") or "").strip()
alias = (data.get("alias") or "").strip()
# GGUF entries must persist source_repo so Re-download has a target (flat GGUF
# files keep no repo info on disk). Plain HF repos re-download by id, so a bare
# path string is enough and surfaces as a missing HF model.
if is_gguf:
entry = {"path": model_id, "source_repo": source_repo}
elif backend or alias:
entry = {"path": model_id}
if source_repo and source_repo != model_id:
entry["source_repo"] = source_repo
else:
entry = model_id
if isinstance(entry, dict):
if backend:
entry["backend"] = backend
if alias:
entry["alias"] = alias
# Dedupe across all categories by path / basename so we don't double-add.
fname = _os.path.basename(model_id) if ("/" in model_id or _os.sep in model_id) else model_id
......
......@@ -551,6 +551,7 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
<option value="vulkan">Vulkan</option>
<option value="opencl">OpenCL</option>
<option value="cpu">CPU only</option>
<option value="colibri">colibri (GLM-5.2 C engine)</option>
</select>
</div>
<div class="form-row" style="margin:0">
......
......@@ -560,6 +560,15 @@
</label>
</div>
<div id="colibri-fields" style="display:none">
<div class="form-group">
<label class="form-label">Download the GLM-5.2 container</label>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center">
<select id="s-colibri-dl-select" class="form-input" style="flex:1;min-width:280px"></select>
<button type="button" id="s-colibri-dl-btn" class="btn btn-secondary" onclick="colibriDownloadDefault()">Download</button>
</div>
<span class="form-hint">Fetches the full HF repo (a directory of int4 shards, ~372&nbsp;GB) like any other model and registers it as a colibri-backed model pointing at the snapshot dir. Keep the HF cache on fast storage (NVMe/ext4). Resumable.</span>
<div id="s-colibri-dl-status" class="form-hint" style="margin-top:.45rem;display:none"></div>
</div>
<div class="form-group">
<label class="form-label">Model id / alias</label>
<input type="text" id="s-colibri-model-id" class="form-input" placeholder="glm-5.2-colibri">
......@@ -809,6 +818,76 @@ async function ds4DownloadDefault(){
btn.disabled = false;
}
}
let _colibriDefaultModels = [];
let _colibriModelId = 'glm-5.2-colibri';
async function loadColibriDefaultModels(){
const sel = document.getElementById('s-colibri-dl-select');
if(!sel) return;
try{
const d = await fetch(ROOT_PATH + '/admin/api/colibri/default-models').then(r=>r.json());
_colibriDefaultModels = d.models || [];
_colibriModelId = d.model_id || _colibriModelId;
sel.innerHTML = _colibriDefaultModels.map(m =>
`<option value="${m.repo}">${m.label}${m.present ? ' ✓ downloaded' : ''}</option>`).join('');
}catch(e){
sel.innerHTML = '<option value="">(failed to load catalog)</option>';
}
}
async function colibriDownloadDefault(){
const sel = document.getElementById('s-colibri-dl-select');
const btn = document.getElementById('s-colibri-dl-btn');
const statusEl = document.getElementById('s-colibri-dl-status');
const repo = sel.value;
const meta = _colibriDefaultModels.find(m => m.repo === repo);
if(!repo || !meta){ return; }
if(meta.present && !confirm('This container is already downloaded. Download again?')) return;
const modelId = (document.getElementById('s-colibri-model-id').value.trim() || _colibriModelId);
btn.disabled = true;
statusEl.style.display = 'block';
statusEl.textContent = 'Starting download (full repo — this is large)…';
try{
// Full-repo snapshot (no file_pattern) → a directory, exactly what colibri loads.
const r = await fetch(ROOT_PATH + '/admin/api/model-download', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({model_id: repo}),
});
if(!r.ok) throw new Error('HTTP '+r.status);
const {session_id} = await r.json();
const es = new EventSource(ROOT_PATH + '/admin/api/download-stream/' + session_id);
es.onmessage = async (ev) => {
let m; try{ m = JSON.parse(ev.data); }catch(_){ return; }
if(m.type === 'progress'){
const pct = (m.percent||0).toFixed(1);
const rate = m.rate ? ' · ' + (m.rate/1048576).toFixed(1) + ' MB/s' : '';
statusEl.textContent = `Downloading GLM-5.2 container: ${pct}%${rate}`;
}else if(m.type === 'info'){
statusEl.textContent = m.message || statusEl.textContent;
}else if(m.type === 'done'){
// Register it as a colibri-backed model, addressable by the model id.
try{
await fetch(ROOT_PATH + '/admin/api/model-add', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({model_id: repo, model_type:'text_models',
backend:'colibri', alias: modelId}),
});
statusEl.textContent = '✅ Downloaded and registered as a colibri model — enable colibri above and it serves as "' + modelId + '".';
}catch(_){
statusEl.textContent = '✅ Downloaded. Add it as a model with backend "colibri" to serve it.';
}
es.close(); btn.disabled = false; loadColibriDefaultModels();
}else if(m.type === 'error'){
statusEl.textContent = '⚠ Download failed: ' + (m.message||'unknown error');
es.close(); btn.disabled = false;
}
};
es.onerror = () => { es.close(); btn.disabled = false; };
}catch(e){
statusEl.textContent = '⚠ Could not start download: ' + e.message;
btn.disabled = false;
}
}
function toggleHttps(){
document.getElementById('https-fields').style.display =
document.getElementById('s-https').checked ? 'block' : 'none';
......@@ -1029,6 +1108,7 @@ async function loadSettings(){
document.getElementById('s-colibri-repo-url').value = colibri.repo_url ?? 'https://github.com/JustVugg/colibri';
document.getElementById('s-colibri-extra-env').value = colibri.extra_env ?? '';
toggleColibriFields();
loadColibriDefaultModels();
}catch(e){ showAlert('error','Failed to load settings: '+e.message); }
}
......
......@@ -191,14 +191,41 @@ class ColibriBackend(ModelBackend):
self._cfg, model_dir=model_dir, ctx=(self._ctx or None))
@staticmethod
def _resolve_container(model_name: str) -> Optional[str]:
def _hf_snapshot_dir(repo_id: str) -> Optional[str]:
"""Local snapshot DIRECTORY of a fully-cached HF repo, or None. colibri needs
the directory (SNAP=<dir>), not a file inside it, so we can't reuse the
file-returning cache resolver — ask huggingface_hub for the snapshot root."""
import os
try:
from huggingface_hub import snapshot_download
hf_dir = None
try:
from codai.models.cache import get_all_cache_dirs
hf_dir = (get_all_cache_dirs() or {}).get("huggingface") or None
except Exception:
hf_dir = None
d = snapshot_download(repo_id, local_files_only=True, cache_dir=hf_dir)
return os.path.abspath(d) if d and os.path.isdir(d) else None
except Exception:
return None
@classmethod
def _resolve_container(cls, model_name: str) -> Optional[str]:
"""Map a requested model name/alias/path to its GLM-5.2 container directory.
The colibri model is a directory (int4 container), not a file — so we look up
the model's models.json entry and use its ``path`` when it is a directory,
else the raw name if it is itself a directory.
The colibri model is a directory (int4 container), not a file. Resolution
order: a local directory given directly → the model's models.json entry
``path`` when it is a local directory → that entry's ``path`` (or the name
itself) resolved as an HF repo id to its local snapshot directory. The last
case is what makes a model downloaded the normal way (an HF repo → snapshot
dir) Just Work.
"""
import os
cand = os.path.expanduser(model_name or "")
if cand and os.path.isdir(cand):
return os.path.abspath(cand)
entry_path = None
try:
from codai.admin.routes import config_manager
md = getattr(config_manager, "models_data", {}) or {}
......@@ -213,12 +240,24 @@ class ColibriBackend(ModelBackend):
base = os.path.basename(path.rstrip("/"))
cands = {path.lower(), base.lower(), str(m.get("alias") or "").lower(),
str(m.get("id") or "").lower()}
if name_l and name_l in cands and os.path.isdir(os.path.expanduser(path)):
if name_l and name_l in cands:
entry_path = path
if path and os.path.isdir(os.path.expanduser(path)):
return os.path.abspath(os.path.expanduser(path))
break
if entry_path is not None:
break
except Exception:
pass
cand = os.path.expanduser(model_name or "")
return os.path.abspath(cand) if cand and os.path.isdir(cand) else None
# HF repo id (from the entry's path, else the requested name) → snapshot dir.
for rid in (entry_path, model_name):
rid = (rid or "").strip()
if rid and "/" in rid and not os.path.isabs(rid):
snap = cls._hf_snapshot_dir(rid)
if snap:
return snap
return None
@staticmethod
def _colibri_overrides(model_name: str, model_dir: Optional[str]) -> Dict:
......
......@@ -55,6 +55,7 @@ _SYSTEM_PATHS = (
"/admin/api/model-upload", "/admin/api/model-free-disk",
"/admin/api/hf-search", "/admin/api/hf-files", "/admin/api/hf-model-info",
"/admin/api/hf-model-files", "/admin/api/ds4/default-models",
"/admin/api/colibri/default-models",
"/admin/api/model-add-known", "/admin/api/model-mark-download",
"/admin/api/model-unmark-download",
# Config-based reads/edits (no GPU): the system worker has config_manager +
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment