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 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # 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. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even # 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)): ...@@ -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} 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: def _cancel_download_session(session_id: str) -> bool:
"""Cancel an active download by flagging the session and terminating its worker """Cancel an active download by flagging the session and terminating its worker
process. Returns False if there is no such download session. 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_ ...@@ -1776,13 +1810,28 @@ async def api_model_add_known(request: Request, username: str = Depends(require_
if model_type not in valid: if model_type not in valid:
model_type = "text_models" 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 # 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 # 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. # path string is enough and surfaces as a missing HF model.
if is_gguf: if is_gguf:
entry = {"path": model_id, "source_repo": source_repo} 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: else:
entry = model_id 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. # 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 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 } ...@@ -551,6 +551,7 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
<option value="vulkan">Vulkan</option> <option value="vulkan">Vulkan</option>
<option value="opencl">OpenCL</option> <option value="opencl">OpenCL</option>
<option value="cpu">CPU only</option> <option value="cpu">CPU only</option>
<option value="colibri">colibri (GLM-5.2 C engine)</option>
</select> </select>
</div> </div>
<div class="form-row" style="margin:0"> <div class="form-row" style="margin:0">
......
...@@ -560,6 +560,15 @@ ...@@ -560,6 +560,15 @@
</label> </label>
</div> </div>
<div id="colibri-fields" style="display:none"> <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"> <div class="form-group">
<label class="form-label">Model id / alias</label> <label class="form-label">Model id / alias</label>
<input type="text" id="s-colibri-model-id" class="form-input" placeholder="glm-5.2-colibri"> <input type="text" id="s-colibri-model-id" class="form-input" placeholder="glm-5.2-colibri">
...@@ -809,6 +818,76 @@ async function ds4DownloadDefault(){ ...@@ -809,6 +818,76 @@ async function ds4DownloadDefault(){
btn.disabled = false; 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(){ function toggleHttps(){
document.getElementById('https-fields').style.display = document.getElementById('https-fields').style.display =
document.getElementById('s-https').checked ? 'block' : 'none'; document.getElementById('s-https').checked ? 'block' : 'none';
...@@ -1029,6 +1108,7 @@ async function loadSettings(){ ...@@ -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-repo-url').value = colibri.repo_url ?? 'https://github.com/JustVugg/colibri';
document.getElementById('s-colibri-extra-env').value = colibri.extra_env ?? ''; document.getElementById('s-colibri-extra-env').value = colibri.extra_env ?? '';
toggleColibriFields(); toggleColibriFields();
loadColibriDefaultModels();
}catch(e){ showAlert('error','Failed to load settings: '+e.message); } }catch(e){ showAlert('error','Failed to load settings: '+e.message); }
} }
......
...@@ -191,14 +191,41 @@ class ColibriBackend(ModelBackend): ...@@ -191,14 +191,41 @@ class ColibriBackend(ModelBackend):
self._cfg, model_dir=model_dir, ctx=(self._ctx or None)) self._cfg, model_dir=model_dir, ctx=(self._ctx or None))
@staticmethod @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. """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 colibri model is a directory (int4 container), not a file. Resolution
the model's models.json entry and use its ``path`` when it is a directory, order: a local directory given directly → the model's models.json entry
else the raw name if it is itself a directory. ``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 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: try:
from codai.admin.routes import config_manager from codai.admin.routes import config_manager
md = getattr(config_manager, "models_data", {}) or {} md = getattr(config_manager, "models_data", {}) or {}
...@@ -213,12 +240,24 @@ class ColibriBackend(ModelBackend): ...@@ -213,12 +240,24 @@ class ColibriBackend(ModelBackend):
base = os.path.basename(path.rstrip("/")) base = os.path.basename(path.rstrip("/"))
cands = {path.lower(), base.lower(), str(m.get("alias") or "").lower(), cands = {path.lower(), base.lower(), str(m.get("alias") or "").lower(),
str(m.get("id") 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:
return os.path.abspath(os.path.expanduser(path)) 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: except Exception:
pass 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 @staticmethod
def _colibri_overrides(model_name: str, model_dir: Optional[str]) -> Dict: def _colibri_overrides(model_name: str, model_dir: Optional[str]) -> Dict:
......
...@@ -55,6 +55,7 @@ _SYSTEM_PATHS = ( ...@@ -55,6 +55,7 @@ _SYSTEM_PATHS = (
"/admin/api/model-upload", "/admin/api/model-free-disk", "/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-search", "/admin/api/hf-files", "/admin/api/hf-model-info",
"/admin/api/hf-model-files", "/admin/api/ds4/default-models", "/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-add-known", "/admin/api/model-mark-download",
"/admin/api/model-unmark-download", "/admin/api/model-unmark-download",
# Config-based reads/edits (no GPU): the system worker has config_manager + # 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