From f843f793d249f093d1a984e7cd17048edf46cbd8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 23:09:27 -0700 Subject: [PATCH 1/5] Unify model resolution behind one fallback ladder run, serve, transcribe and download now share a single ensure_runnable_bundle resolver (local path -> cache -> prebuilt download -> local convert+transpile), so any entry point builds a model on demand instead of erroring or using a partial path. Demote convert and transpile to an advanced section in --help. Signed-off-by: github-actions[bot] --- python/cactus/cli/__init__.py | 60 ++++++++++++++++++--------------- python/cactus/cli/download.py | 7 ++-- python/cactus/cli/model.py | 29 ++++++++++++++++ python/cactus/cli/run.py | 50 +++++++-------------------- python/cactus/cli/serve.py | 10 ++++-- python/cactus/cli/transcribe.py | 24 ++++++------- 6 files changed, 97 insertions(+), 83 deletions(-) diff --git a/python/cactus/cli/__init__.py b/python/cactus/cli/__init__.py index ac8708fc6..19c77e9ac 100644 --- a/python/cactus/cli/__init__.py +++ b/python/cactus/cli/__init__.py @@ -108,11 +108,43 @@ def create_parser(): --token HuggingFace token (gated models) --reconvert force reconversion from source - cactus download [model] download a pre-built bundle (default: {DEFAULT_MODEL_ID}) + cactus download [model] fetch a prebuilt bundle, else build locally (default: {DEFAULT_MODEL_ID}) --bits 1|2|3|4 CQ quantization (default: 4) --platform {_PLATFORM_PIPE:<22} target accelerator (default: cpu) --token HuggingFace token + cactus serve [model] OpenAI-compatible local HTTP server + --host bind address (default: 127.0.0.1) + --port port (default: 8080) + + cactus list list local converted weights and bundles + + cactus build build cactus libraries + --apple Apple (iOS/macOS) + --android Android + --python shared lib for Python FFI + + cactus test run the test suite + --component kernels | graph | engine | all + (default: all) + --model default: {DEFAULT_MODEL_ID} + --transcription-model default: {DEFAULT_TRANSCRIPTION_MODEL_ID} + --suite run a single test suite from any + component (kernels, graph, or engine) + --list list components and suites + --ios run on connected iPhone + --android run on connected Android + --enable-telemetry send cloud telemetry (off by default) + + cactus clean delete build artifacts + cactus --help show this help + + ----------------------------------------------------------------- + + Advanced / build pipeline — the two manual steps below run + automatically inside run, serve, transcribe and download; reach for + them only to control the build (custom flags, LoRA, NPU, debugging): + cactus convert [dir] convert HuggingFace weights to CQ --bits 1|2|3|4 CQ quantization (default: 4) --token HuggingFace token @@ -151,32 +183,6 @@ def create_parser(): --npu-audio-quantize 0|4|8 audio encoder quant (default int8) --npu-vision-quantize 0|4|8 vision encoder quant (default fp16) - cactus serve [model] OpenAI-compatible local HTTP server - --host bind address (default: 127.0.0.1) - --port port (default: 8080) - - cactus list list local converted weights and bundles - - cactus build build cactus libraries - --apple Apple (iOS/macOS) - --android Android - --python shared lib for Python FFI - - cactus test run the test suite - --component kernels | graph | engine | all - (default: all) - --model default: {DEFAULT_MODEL_ID} - --transcription-model default: {DEFAULT_TRANSCRIPTION_MODEL_ID} - --suite run a single test suite from any - component (kernels, graph, or engine) - --list list components and suites - --ios run on connected iPhone - --android run on connected Android - --enable-telemetry send cloud telemetry (off by default) - - cactus clean delete build artifacts - cactus --help show this help - ----------------------------------------------------------------- """ ) diff --git a/python/cactus/cli/download.py b/python/cactus/cli/download.py index 57d4f482c..6727e8b6f 100644 --- a/python/cactus/cli/download.py +++ b/python/cactus/cli/download.py @@ -63,11 +63,12 @@ def download_bundle(model_id: str, *, bits: int = 4, platform: str | None = None def cmd_download(args) -> int: + from .model import ensure_runnable_bundle + try: platform = resolve_platform(args.platform) - download_bundle(args.model_id, bits=args.bits, platform=platform, token=args.token) + ensure_runnable_bundle(args.model_id, bits=args.bits, platform=platform, token=args.token) return 0 except (RuntimeError, OSError, ValueError) as e: - print_color(RED, f"Download failed: {e}") - print_color(YELLOW, f"Try: cactus convert {args.model_id} --bits {args.bits} && cactus transpile {args.model_id}") + print_color(RED, f"Failed to prepare {args.model_id}: {e}") return 1 diff --git a/python/cactus/cli/model.py b/python/cactus/cli/model.py index 0a310e7e4..7b135b1f6 100644 --- a/python/cactus/cli/model.py +++ b/python/cactus/cli/model.py @@ -184,6 +184,35 @@ class TranspileOptions: cache_context_length: str | int | None = None +def ensure_runnable_bundle(model_id, *, bits=4, platform=None, token=None, + reconvert=False, transpile=None): + """Resolve a runnable bundle via the full fallback ladder, building if needed. + + Rungs, in order: (1) a local bundle path, (2) a cached prior build, + (3) a prebuilt bundle on HuggingFace, (4) local convert + transpile. + Raises RuntimeError if every rung fails. + """ + from .download import download_bundle, get_bundle_dir + + local = resolve_bundle_dir(model_id) + if local is not None: + return local + + cached = get_bundle_dir(model_id, bits=bits, platform=platform) + if _has_transpiled_bundle(cached) and not reconvert: + return cached + + if not reconvert: + try: + return download_bundle(model_id, bits=bits, platform=platform, + token=token, reconvert=reconvert) + except (RuntimeError, OSError) as exc: + print_color(YELLOW, f"No prebuilt bundle ({exc}); building locally") + + return ensure_bundle(model_id, bits=bits, token=token, + reconvert=reconvert, transpile=transpile or TranspileOptions()) + + def ensure_bundle(model_id, *, bits=4, token=None, reconvert=False, output_dir=None, transpile=None): from .download import get_weights_dir diff --git a/python/cactus/cli/run.py b/python/cactus/cli/run.py index f69399039..7ddc0f6cf 100644 --- a/python/cactus/cli/run.py +++ b/python/cactus/cli/run.py @@ -19,13 +19,20 @@ def cmd_run(args) -> int: if args.result_json: args.result_json = str(Path(args.result_json).expanduser()) + from .model import TranspileOptions, ensure_runnable_bundle + platform = resolve_platform(args.platform) - bundle_dir = _resolve_or_fetch_bundle( - args.model_id, bits=args.bits, platform=platform, - token=args.token, reconvert=args.reconvert, - image=args.image, audio=args.audio, - ) - if bundle_dir is None: + try: + bundle_dir = ensure_runnable_bundle( + args.model_id, bits=args.bits, platform=platform, + token=args.token, reconvert=args.reconvert, + transpile=TranspileOptions( + image_files=[args.image] if args.image else None, + audio_file=args.audio, + ), + ) + except RuntimeError as exc: + print_color(RED, f"Model setup failed: {exc}") return 1 binary = resolve_binary("run") @@ -55,34 +62,3 @@ def cmd_run(args) -> int: print_color(GREEN, f"Running: {bundle_dir}") print() return subprocess.run(cmd).returncode - - -def _resolve_or_fetch_bundle(model_id, *, bits, platform, token, reconvert, image, audio): - from .download import download_bundle, get_bundle_dir - from .model import TranspileOptions, ensure_bundle, resolve_bundle_dir - - local = resolve_bundle_dir(model_id) - if local is not None: - return local - - cached = get_bundle_dir(model_id, bits=bits, platform=platform) - if (cached / "components" / "manifest.json").exists() and not reconvert: - return cached - - if not reconvert: - try: - return download_bundle(model_id, bits=bits, platform=platform, token=token, reconvert=reconvert) - except (RuntimeError, OSError) as exc: - print_color(YELLOW, f"HF download unavailable ({exc}); building locally") - - try: - return ensure_bundle( - model_id, bits=bits, token=token, reconvert=reconvert, - transpile=TranspileOptions( - image_files=[image] if image else None, - audio_file=audio, - ), - ) - except RuntimeError as exc: - print_color(RED, f"Model setup failed: {exc}") - return None diff --git a/python/cactus/cli/serve.py b/python/cactus/cli/serve.py index c7812fd18..2c642cb67 100644 --- a/python/cactus/cli/serve.py +++ b/python/cactus/cli/serve.py @@ -27,9 +27,13 @@ def cmd_serve(args): """Start the OpenAI-compatible HTTP server.""" model_path, model_name = _resolve_model_arg(args.model) if args.model and model_path is None: - print_color(RED, f"Error: model not found: {args.model}") - print("Prepare a v2 bundle first with `cactus run ` (or `cactus convert ` then `cactus transpile `).") - return 1 + from .model import ensure_runnable_bundle + try: + built = ensure_runnable_bundle(args.model) + except RuntimeError as exc: + print_color(RED, f"Error: could not prepare {args.model}: {exc}") + return 1 + model_path, model_name = built, built.name if model_path is not None and not _is_valid_bundle(model_path): print_color(RED, f"Error: not a valid v2 Cactus bundle: {model_path}") print("Expected config.txt and components/manifest.json.") diff --git a/python/cactus/cli/transcribe.py b/python/cactus/cli/transcribe.py index 870430838..23989d007 100644 --- a/python/cactus/cli/transcribe.py +++ b/python/cactus/cli/transcribe.py @@ -6,7 +6,7 @@ def cmd_transcribe(args): - from .model import ensure_bundle, resolve_bundle_dir, TranspileOptions + from .model import ensure_runnable_bundle, TranspileOptions audio_path = Path(args.audio_file).expanduser() if not audio_path.is_file(): @@ -24,18 +24,16 @@ def cmd_transcribe(args): apply_cloud_api_key_env() - bundle_dir = resolve_bundle_dir(args.model_id) - if bundle_dir is None: - try: - bundle_dir = ensure_bundle( - args.model_id, - token=args.token, - reconvert=args.reconvert, - transpile=TranspileOptions(audio_file=args.audio_file), - ) - except RuntimeError as e: - print_color(RED, f"Model setup failed: {e}") - return 1 + try: + bundle_dir = ensure_runnable_bundle( + args.model_id, + token=args.token, + reconvert=args.reconvert, + transpile=TranspileOptions(audio_file=args.audio_file), + ) + except RuntimeError as e: + print_color(RED, f"Model setup failed: {e}") + return 1 binary = resolve_binary("transcribe") if binary is None: From e915d2ad0b0f0bf8d0759045b899ace3a30d1852 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 23:09:31 -0700 Subject: [PATCH 2/5] Show CQ quantization level in cactus list Read the precision code from each tensor header and report the dominant CQ level (e.g. CQ4) as a column, since bit-width is stored per-tensor rather than in any single config field. Signed-off-by: github-actions[bot] --- python/cactus/cli/list.py | 50 +++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/python/cactus/cli/list.py b/python/cactus/cli/list.py index aa9534753..53e1735e5 100644 --- a/python/cactus/cli/list.py +++ b/python/cactus/cli/list.py @@ -1,8 +1,12 @@ +import itertools import stat as _stat +import struct from pathlib import Path from .common import BLUE, CYAN, print_color, transpiled_root, weights_root +_PREC_TO_BITS = {3: 1, 4: 2, 5: 3, 6: 4} + def _read_config(path): pairs = ( @@ -20,6 +24,44 @@ def _human_size(n): n /= 1024 +def _cq_precision(weights_file): + """Read the precision code at byte offset 48 of a Cactus tensor header.""" + try: + with weights_file.open("rb") as f: + head = f.read(52) + except OSError: + return None + if len(head) < 52 or head[:4] != b"CACT": + return None + return struct.unpack_from("= sample_cap or scanned >= scan_cap: + break + if not counts: + return "—" + return f"CQ{max(counts, key=counts.get)}" + + def _dir_size(path): total = 0 for f in path.rglob("*"): @@ -41,7 +83,7 @@ def _categorize(roots): if not p.is_dir() or not (p / "config.txt").is_file(): continue cfg = _read_config(p / "config.txt") - entry = (p, cfg.get("model_type", "?"), _dir_size(p)) + entry = (p, cfg.get("model_type", "?"), _quant_label(p), _dir_size(p)) if (p / "components" / "manifest.json").is_file(): transpiled.append(entry) else: @@ -54,9 +96,9 @@ def _print_section(title: str, color: str, entries): if not entries: print(" (none)") return - width = max(len(p.name) for p, _, _ in entries) - for p, model_type, size in entries: - print(f" {p.name:<{width}} {model_type:<12} {_human_size(size):>10} {p.parent}") + width = max(len(p.name) for p, _, _, _ in entries) + for p, model_type, quant, size in entries: + print(f" {p.name:<{width}} {model_type:<12} {quant:<5} {_human_size(size):>10} {p.parent}") def cmd_list(_args): From 8731dc011d8166fbb317cf5e1fbfb4a1527ddc41 Mon Sep 17 00:00:00 2001 From: HenryNdubuaku Date: Thu, 11 Jun 2026 17:50:32 +0100 Subject: [PATCH 3/5] Update error message for missing bundle in resolve_bundle_dir function --- cactus-engine/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cactus-engine/test.sh b/cactus-engine/test.sh index a5da20aa6..e8b1915d9 100755 --- a/cactus-engine/test.sh +++ b/cactus-engine/test.sh @@ -33,7 +33,7 @@ resolve_bundle_dir() { dir="$PROJECT_ROOT/weights/$(basename "$model" | tr '[:upper:]' '[:lower:]')" fi if [ ! -f "$dir/components/manifest.json" ]; then - echo "Bundle missing at $dir. Run: cactus transpile $model" >&2 + echo "Bundle missing at $dir. Run: cactus download $model" >&2 exit 1 fi echo "$dir" From 3cb53d945dbde6d86acfff548e4c717ee545cba2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Jun 2026 12:59:15 -0700 Subject: [PATCH 4/5] Route cactus test through the unified bundle resolver Signed-off-by: github-actions[bot] --- cactus-engine/test.sh | 22 ++++++---------------- python/cactus/cli/download.py | 2 +- python/cactus/cli/list.py | 1 - python/cactus/cli/run.py | 2 +- python/cactus/cli/test.py | 15 +++++++++++++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cactus-engine/test.sh b/cactus-engine/test.sh index e8b1915d9..25cfc92ec 100755 --- a/cactus-engine/test.sh +++ b/cactus-engine/test.sh @@ -20,27 +20,17 @@ while [[ $# -gt 0 ]]; do esac done -resolve_bundle_dir() { - local model="$1" label="$2" - if [ -z "$model" ]; then - echo "Error: --$label is required. Run engine tests via the CLI (cactus test ...) which sets it." >&2 +require_bundle() { + local dir="$1" label="$2" + if [ -z "$dir" ] || [ ! -f "$dir/components/manifest.json" ]; then + echo "Error: --$label must point to a prepared bundle. Run engine tests via 'cactus test', which prepares them." >&2 exit 2 fi - local dir - if [[ "$model" == /* || "$model" == ./* ]]; then - dir="$model" - else - dir="$PROJECT_ROOT/weights/$(basename "$model" | tr '[:upper:]' '[:lower:]')" - fi - if [ ! -f "$dir/components/manifest.json" ]; then - echo "Bundle missing at $dir. Run: cactus download $model" >&2 - exit 1 - fi echo "$dir" } -BUNDLE_DIR="$(resolve_bundle_dir "$CACTUS_TEST_MODEL" "model")" -TRANSCRIPTION_BUNDLE_DIR="$(resolve_bundle_dir "$CACTUS_TEST_TRANSCRIPTION_MODEL" "transcription-model")" +BUNDLE_DIR="$(require_bundle "$CACTUS_TEST_MODEL" "model")" +TRANSCRIPTION_BUNDLE_DIR="$(require_bundle "$CACTUS_TEST_TRANSCRIPTION_MODEL" "transcription-model")" if [ "$IOS_MODE" = true ]; then export CACTUS_TEST_MODEL="$BUNDLE_DIR" CACTUS_TEST_TRANSCRIPTION_MODEL="$TRANSCRIPTION_BUNDLE_DIR" CACTUS_TEST_SUITE="$SUITE" diff --git a/python/cactus/cli/download.py b/python/cactus/cli/download.py index 6727e8b6f..4e5a8f5ae 100644 --- a/python/cactus/cli/download.py +++ b/python/cactus/cli/download.py @@ -2,7 +2,7 @@ from pathlib import Path from .common import ( - BLUE, GREEN, RED, YELLOW, + BLUE, GREEN, RED, SUPPORTED_PLATFORMS, print_color, transpiled_root, weights_root, ) diff --git a/python/cactus/cli/list.py b/python/cactus/cli/list.py index 53e1735e5..8df562cc7 100644 --- a/python/cactus/cli/list.py +++ b/python/cactus/cli/list.py @@ -1,7 +1,6 @@ import itertools import stat as _stat import struct -from pathlib import Path from .common import BLUE, CYAN, print_color, transpiled_root, weights_root diff --git a/python/cactus/cli/run.py b/python/cactus/cli/run.py index 7ddc0f6cf..68228c7c3 100644 --- a/python/cactus/cli/run.py +++ b/python/cactus/cli/run.py @@ -2,7 +2,7 @@ import subprocess from pathlib import Path -from .common import GREEN, RED, YELLOW, apply_cloud_api_key_env, print_color, resolve_binary +from .common import GREEN, RED, apply_cloud_api_key_env, print_color, resolve_binary def cmd_run(args) -> int: diff --git a/python/cactus/cli/test.py b/python/cactus/cli/test.py index 4a347d9e3..349891038 100644 --- a/python/cactus/cli/test.py +++ b/python/cactus/cli/test.py @@ -29,8 +29,8 @@ def _component_args(component, args): if args.suite: cmd.extend(["--suite", args.suite]) if component == "engine": - cmd.extend(["--model", args.model_id or DEFAULT_MODEL_ID]) - cmd.extend(["--transcription-model", args.transcription_model_id or DEFAULT_TRANSCRIPTION_MODEL_ID]) + cmd.extend(["--model", args.model_id]) + cmd.extend(["--transcription-model", args.transcription_model_id]) if args.android: cmd.append("--android") if args.ios: @@ -65,6 +65,17 @@ def cmd_test(args): else: targets = ("kernels", "graph", "engine") if args.component == "all" else (args.component,) + if "engine" in targets: + from .model import ensure_runnable_bundle + try: + args.model_id = str(ensure_runnable_bundle(args.model_id or DEFAULT_MODEL_ID)) + args.transcription_model_id = str( + ensure_runnable_bundle(args.transcription_model_id or DEFAULT_TRANSCRIPTION_MODEL_ID) + ) + except RuntimeError as exc: + print_color(RED, f"Model setup failed: {exc}") + return 1 + apply_cloud_api_key_env() env = os.environ.copy() if args.enable_telemetry: From 670c231a0128e0e72349079e4ec43ee3990bda7d Mon Sep 17 00:00:00 2001 From: Karen Mosoyan Date: Mon, 15 Jun 2026 13:17:59 -0700 Subject: [PATCH 5/5] fixed pytest monkeypatch --- python/cactus/transpile/README.md | 4 ++-- python/tests/test_cli_run.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/python/cactus/transpile/README.md b/python/cactus/transpile/README.md index e2af2deb2..52053dda9 100644 --- a/python/cactus/transpile/README.md +++ b/python/cactus/transpile/README.md @@ -60,7 +60,7 @@ Two important details: ### `cactus run` `cactus run ` is the single user-facing entry. Resolution -order (`python/cactus/cli/run.py:_resolve_or_fetch_bundle`): +order (`python/cactus/cli/model.py:ensure_runnable_bundle`): 1. `` is an existing path with `components/manifest.json` → run as-is. 2. Cached download at `transpiled/-cq[-]/` → run. @@ -990,7 +990,7 @@ cactus run /path/to/transpiled/model \ --result-json ./result.json ``` -Internally, `cli/run.py:_resolve_or_fetch_bundle` checks whether the positional +Internally, `cli/model.py:ensure_runnable_bundle` checks whether the positional argument is a local path with `components/manifest.json` and dispatches the native `run` binary against it. diff --git a/python/tests/test_cli_run.py b/python/tests/test_cli_run.py index f3722d138..660ffc84c 100644 --- a/python/tests/test_cli_run.py +++ b/python/tests/test_cli_run.py @@ -3,6 +3,7 @@ from types import SimpleNamespace import cactus.cli.common as common_mod +import cactus.cli.model as model_mod import cactus.cli.run as run_mod @@ -32,7 +33,7 @@ def fake_subprocess_run(cmd): return SimpleNamespace(returncode=0) monkeypatch.setattr(run_mod.subprocess, "run", fake_subprocess_run) - monkeypatch.setattr(run_mod, "_resolve_or_fetch_bundle", lambda *a, **k: bundle_dir) + monkeypatch.setattr(model_mod, "ensure_runnable_bundle", lambda *a, **k: bundle_dir) args = Namespace( no_cloud_tele=False,