Merge pull request #130 from jbergant/fix/qwen35-model-catalog-mlx-129

fix: correct Qwen3.5 model sizes and MLX repos in catalog
This commit is contained in:
Jon Saad-Falcon
2026-03-26 17:50:44 -07:00
committed by GitHub
10 changed files with 104 additions and 125 deletions
+12 -33
View File
@@ -35,17 +35,13 @@ def _check_python_version() -> CheckResult:
version_str = f"{ver.major}.{ver.minor}.{ver.micro}"
if (ver.major, ver.minor) >= (3, 10):
return CheckResult("Python version", "ok", version_str)
return CheckResult(
"Python version", "fail", f"{version_str} (requires >= 3.10)"
)
return CheckResult("Python version", "fail", f"{version_str} (requires >= 3.10)")
def _check_config_exists() -> CheckResult:
"""Check that the config file exists."""
if DEFAULT_CONFIG_PATH.exists():
return CheckResult(
"Config file", "ok", str(DEFAULT_CONFIG_PATH)
)
return CheckResult("Config file", "ok", str(DEFAULT_CONFIG_PATH))
return CheckResult(
"Config file",
"warn",
@@ -57,16 +53,12 @@ def _check_config_exists() -> CheckResult:
def _check_config_parses() -> CheckResult:
"""Check that the config file parses successfully."""
if not DEFAULT_CONFIG_PATH.exists():
return CheckResult(
"Config parsing", "warn", "Skipped (no config file)"
)
return CheckResult("Config parsing", "warn", "Skipped (no config file)")
try:
load_config()
return CheckResult("Config parsing", "ok", "Config loaded successfully")
except Exception as exc:
return CheckResult(
"Config parsing", "fail", f"Parse error: {exc}"
)
return CheckResult("Config parsing", "fail", f"Parse error: {exc}")
def _ensure_engines_imported() -> None:
@@ -102,22 +94,16 @@ def _check_engines() -> List[CheckResult]:
try:
engine = _discovery._make_engine(key, config)
if engine.health():
results.append(
CheckResult(f"Engine: {key}", "ok", "Reachable")
)
results.append(CheckResult(f"Engine: {key}", "ok", "Reachable"))
else:
results.append(
CheckResult(f"Engine: {key}", "warn", "Unreachable")
)
results.append(CheckResult(f"Engine: {key}", "warn", "Unreachable"))
except Exception as exc:
results.append(
CheckResult(f"Engine: {key}", "warn", f"Unreachable ({exc})")
)
if not results:
results.append(
CheckResult("Engines", "warn", "No engines registered")
)
results.append(CheckResult("Engines", "warn", "No engines registered"))
return results
@@ -154,7 +140,7 @@ def _check_models() -> List[CheckResult]:
f"Models: {key}",
"warn",
"No models available",
details="Pull a model (e.g. `ollama pull qwen3.5:3b`).",
details="Pull a model (e.g. `ollama pull qwen3.5:2b`).",
)
)
except Exception:
@@ -168,9 +154,7 @@ def _check_default_model() -> CheckResult:
try:
config = load_config()
except Exception:
return CheckResult(
"Default model", "warn", "Skipped (config unavailable)"
)
return CheckResult("Default model", "warn", "Skipped (config unavailable)")
default_model = config.intelligence.default_model
if not default_model:
@@ -221,9 +205,7 @@ def _check_optional_deps() -> List[CheckResult]:
for pkg, install_hint, description in optional_packages:
try:
__import__(pkg)
results.append(
CheckResult(f"Optional: {description}", "ok", "Installed")
)
results.append(CheckResult(f"Optional: {description}", "ok", "Installed"))
except Exception:
results.append(
CheckResult(
@@ -266,8 +248,7 @@ def _check_nodejs() -> CheckResult:
"warn",
f"{version_str} (requires >= v22)",
details=(
"Upgrade Node.js for ClaudeCodeAgent and WhatsApp "
"Baileys support."
"Upgrade Node.js for ClaudeCodeAgent and WhatsApp Baileys support."
),
)
except Exception as exc:
@@ -335,7 +316,5 @@ def doctor(as_json: bool) -> None:
warn_count = sum(1 for c in checks if c.status == "warn")
fail_count = sum(1 for c in checks if c.status == "fail")
console.print()
console.print(
f" {ok_count} passed, {warn_count} warnings, {fail_count} failures"
)
console.print(f" {ok_count} passed, {warn_count} warnings, {fail_count} failures")
console.print()
+1 -1
View File
@@ -40,6 +40,6 @@ def hint_no_model(model_name: Optional[str] = None) -> str:
)
return (
"[yellow]Hint:[/yellow] No models available.\n"
" Pull a model first: [bold]ollama pull qwen3.5:3b[/bold]\n"
" Pull a model first: [bold]ollama pull qwen3.5:2b[/bold]\n"
" Run [bold]jarvis model list[/bold] to see available models."
)
+1 -1
View File
@@ -64,7 +64,7 @@ def _detect_running_engines() -> list[str]:
def _next_steps_text(engine: str, model: str = "") -> str:
"""Return engine-specific next-steps guidance after init."""
pull_model = model or "qwen3.5:3b"
pull_model = model or "qwen3.5:2b"
steps: dict[str, str] = {
"ollama": (
"Next steps:\n"
+3 -5
View File
@@ -106,8 +106,7 @@ def quickstart(force: bool) -> None:
console.print("[bold cyan][2/5][/bold cyan] Writing config...")
if DEFAULT_CONFIG_PATH.exists() and not force:
console.print(
f" [dim]Config already exists at"
f" {DEFAULT_CONFIG_PATH} (skip)[/dim]"
f" [dim]Config already exists at {DEFAULT_CONFIG_PATH} (skip)[/dim]"
)
else:
toml_content = generate_default_toml(hw)
@@ -144,7 +143,7 @@ def quickstart(force: bool) -> None:
if not _check_model_available(active_engine):
console.print(" [yellow]No models found.[/yellow]")
console.print(
" Pull a model first (e.g. [bold]ollama pull qwen3.5:3b[/bold])."
" Pull a model first (e.g. [bold]ollama pull qwen3.5:2b[/bold])."
)
raise SystemExit(1)
console.print(" [green]Models available.[/green]")
@@ -157,6 +156,5 @@ def quickstart(force: bool) -> None:
console.print()
console.print(
'[bold green]Setup complete![/bold green]'
' Try: [bold]jarvis ask "Hello"[/bold]'
'[bold green]Setup complete![/bold green] Try: [bold]jarvis ask "Hello"[/bold]'
)
@@ -32,9 +32,9 @@ name = "mlx-community/Qwen2.5-7B-4bit"
engine = "mlx"
param_count_b = 7.0
# Ollama GGUF — pull with: ollama pull qwen3.5:3b
# Ollama GGUF — pull with: ollama pull qwen3.5:2b
[[models]]
name = "qwen3.5:3b"
name = "qwen3.5:2b"
engine = "ollama"
# ── Benchmarks ────────────────────────────────────────────────────────────────
+37 -24
View File
@@ -40,48 +40,62 @@ BUILTIN_MODELS: List[ModelSpec] = [
# Local models — Qwen3.5 (MoE)
# -----------------------------------------------------------------------
ModelSpec(
model_id="qwen3.5:3b",
name="Qwen3.5 3B",
parameter_count_b=3.0,
active_parameter_count_b=0.6,
model_id="qwen3.5:0.8b",
name="Qwen3.5 0.8B",
parameter_count_b=0.8,
active_parameter_count_b=0.15,
context_length=131072,
supported_engines=("ollama", "vllm", "llamacpp", "sglang", "mlx"),
provider="alibaba",
metadata={
"architecture": "moe",
"hf_repo": "Qwen/Qwen3.5-3B",
"gguf_file": "qwen3.5-3b-q4_k_m.gguf",
"mlx_repo": "mlx-community/Qwen3.5-3B-4bit",
"hf_repo": "Qwen/Qwen3.5-0.8B",
"mlx_repo": "mlx-community/Qwen3.5-0.8B-OptiQ-4bit",
},
),
ModelSpec(
model_id="qwen3.5:8b",
name="Qwen3.5 8B",
parameter_count_b=8.0,
active_parameter_count_b=1.0,
model_id="qwen3.5:2b",
name="Qwen3.5 2B",
parameter_count_b=2.0,
active_parameter_count_b=0.4,
context_length=131072,
supported_engines=("ollama", "vllm", "llamacpp", "sglang", "mlx"),
provider="alibaba",
metadata={
"architecture": "moe",
"hf_repo": "Qwen/Qwen3.5-8B",
"gguf_file": "qwen3.5-8b-q4_k_m.gguf",
"mlx_repo": "mlx-community/Qwen3.5-8B-4bit",
"hf_repo": "Qwen/Qwen3.5-2B",
"mlx_repo": "mlx-community/Qwen3.5-2B-OptiQ-4bit",
},
),
ModelSpec(
model_id="qwen3.5:14b",
name="Qwen3.5 14B",
parameter_count_b=14.0,
active_parameter_count_b=2.0,
model_id="qwen3.5:9b",
name="Qwen3.5 9B",
parameter_count_b=9.0,
active_parameter_count_b=1.5,
context_length=131072,
supported_engines=("ollama", "vllm", "llamacpp", "sglang", "mlx"),
provider="alibaba",
metadata={
"architecture": "moe",
"hf_repo": "Qwen/Qwen3.5-14B",
"gguf_file": "qwen3.5-14b-q4_k_m.gguf",
"mlx_repo": "mlx-community/Qwen3.5-14B-4bit",
"hf_repo": "Qwen/Qwen3.5-9B",
"gguf_file": "qwen3.5-9b-q4_k_m.gguf",
"mlx_repo": "mlx-community/Qwen3.5-9B-MLX-4bit",
},
),
ModelSpec(
model_id="qwen3.5:27b",
name="Qwen3.5 27B",
parameter_count_b=27.0,
active_parameter_count_b=3.0,
context_length=131072,
min_vram_gb=16.0,
supported_engines=("ollama", "vllm", "llamacpp", "sglang", "mlx"),
provider="alibaba",
metadata={
"architecture": "moe",
"hf_repo": "Qwen/Qwen3.5-27B",
"gguf_file": "qwen3.5-27b-q4_k_m.gguf",
"mlx_repo": "mlx-community/Qwen3.5-27B-4bit-DWQ",
},
),
ModelSpec(
@@ -235,7 +249,7 @@ BUILTIN_MODELS: List[ModelSpec] = [
"architecture": "moe",
"hf_repo": "Qwen/Qwen3.5-4B",
"gguf_file": "qwen3.5-4b-q4_k_m.gguf",
"mlx_repo": "mlx-community/Qwen3.5-4B-4bit",
"mlx_repo": "mlx-community/Qwen3.5-4B-OptiQ-4bit",
},
),
ModelSpec(
@@ -490,8 +504,7 @@ BUILTIN_MODELS: List[ModelSpec] = [
metadata={
"architecture": "moe",
"hf_repo": (
"TeichAI/GLM-4.7-Flash-Claude-"
"Opus-4.5-High-Reasoning-Distill-GGUF"
"TeichAI/GLM-4.7-Flash-Claude-Opus-4.5-High-Reasoning-Distill-GGUF"
),
"teacher": "Claude Opus 4.5",
"quantization": "GGUF Q4_K_M / Q8_0",
+13 -29
View File
@@ -23,9 +23,7 @@ class TestInitShowsNextSteps:
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"),
):
result = CliRunner().invoke(
cli, ["init", "--engine", "llamacpp", _NO_DL]
)
result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp", _NO_DL])
assert result.exit_code == 0
assert "Getting Started" in result.output
assert "jarvis ask" in result.output
@@ -40,9 +38,7 @@ class TestInitShowsNextSteps:
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"),
):
result = CliRunner().invoke(
cli, ["init", "--engine", "llamacpp", _NO_DL]
)
result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp", _NO_DL])
assert result.exit_code == 0
assert "[engine]" in result.output
assert "[intelligence]" in result.output
@@ -57,12 +53,12 @@ class TestNextStepsOllama:
assert "jarvis doctor" in text
def test_next_steps_ollama_with_model(self) -> None:
text = _next_steps_text("ollama", "qwen3.5:14b")
assert "ollama pull qwen3.5:14b" in text
text = _next_steps_text("ollama", "qwen3.5:27b")
assert "ollama pull qwen3.5:27b" in text
def test_next_steps_ollama_default_model(self) -> None:
text = _next_steps_text("ollama")
assert "ollama pull qwen3.5:3b" in text
assert "ollama pull qwen3.5:2b" in text
class TestNextStepsVllm:
@@ -102,9 +98,7 @@ class TestMinimalConfig:
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"),
):
result = CliRunner().invoke(
cli, ["init", "--engine", "ollama", _NO_DL]
)
result = CliRunner().invoke(cli, ["init", "--engine", "ollama", _NO_DL])
assert result.exit_code == 0
content = config_path.read_text()
# Minimal config should be short
@@ -158,9 +152,7 @@ class TestInitDownloadPrompt:
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"),
):
result = CliRunner().invoke(
cli, ["init", "--engine", "ollama", _NO_DL]
)
result = CliRunner().invoke(cli, ["init", "--engine", "ollama", _NO_DL])
assert result.exit_code == 0
assert "Download" not in result.output
@@ -175,13 +167,10 @@ class TestInitEmptyModelFallback:
mock.patch("openjarvis.cli.init_cmd.recommend_model", return_value=""),
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"),
):
result = CliRunner().invoke(
cli, ["init", "--engine", "llamacpp"]
)
result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp"])
assert result.exit_code == 0
assert (
"Not enough memory" in result.output
or "not enough memory" in result.output
"Not enough memory" in result.output or "not enough memory" in result.output
)
@@ -200,9 +189,7 @@ class TestNextStepsExoNexa:
class TestInitDownloadDispatch:
def test_init_ollama_download_calls_ollama_pull(
self, tmp_path: Path
) -> None:
def test_init_ollama_download_calls_ollama_pull(self, tmp_path: Path) -> None:
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
@@ -228,9 +215,7 @@ class TestInitDownloadDispatch:
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"),
):
result = CliRunner().invoke(
cli, ["init", "--engine", "vllm"], input="y\n"
)
result = CliRunner().invoke(cli, ["init", "--engine", "vllm"], input="y\n")
assert result.exit_code == 0
assert "automatically" in result.output
@@ -245,12 +230,11 @@ class TestInitPrivacyHook:
mock.patch("openjarvis.cli.init_cmd.PrivacyScanner") as MockScanner,
):
from openjarvis.cli.scan_cmd import ScanResult
instance = MockScanner.return_value
instance.run_quick.return_value = [
ScanResult("FileVault", "ok", "FileVault enabled", "darwin"),
]
result = CliRunner().invoke(
cli, ["init", "--engine", "llamacpp", _NO_DL]
)
result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp", _NO_DL])
assert result.exit_code == 0
assert "jarvis scan" in result.output
+8 -7
View File
@@ -15,6 +15,7 @@ class TestOllamaPull:
def test_ollama_pull_success(self) -> None:
import io
console = Console(file=io.StringIO())
mock_lines = [
'{"status": "pulling manifest"}',
@@ -28,7 +29,7 @@ class TestOllamaPull:
mock_resp.__exit__ = mock.MagicMock(return_value=False)
with mock.patch("httpx.stream", return_value=mock_resp):
result = ollama_pull("http://localhost:11434", "qwen3.5:3b", console)
result = ollama_pull("http://localhost:11434", "qwen3.5:2b", console)
assert result is True
def test_ollama_pull_connect_error(self) -> None:
@@ -38,7 +39,7 @@ class TestOllamaPull:
console = Console(file=io.StringIO())
with mock.patch("httpx.stream", side_effect=httpx.ConnectError("refused")):
result = ollama_pull("http://localhost:11434", "qwen3.5:3b", console)
result = ollama_pull("http://localhost:11434", "qwen3.5:2b", console)
assert result is False
@@ -58,14 +59,14 @@ class TestPullCliMultiEngine:
mock_run.return_value = mock.MagicMock(returncode=0)
result = runner.invoke(
cli, ["model", "pull", "qwen3.5:8b", "--engine", "llamacpp"]
cli, ["model", "pull", "qwen3.5:9b", "--engine", "llamacpp"]
)
assert result.exit_code == 0
mock_run.assert_called_once()
call_args = mock_run.call_args[0][0]
assert "huggingface-cli" in call_args
assert "qwen3.5-8b-q4_k_m.gguf" in call_args
assert "qwen3.5-9b-q4_k_m.gguf" in call_args
def test_pull_mlx_uses_huggingface_cli(self) -> None:
from openjarvis.cli import cli
@@ -80,14 +81,14 @@ class TestPullCliMultiEngine:
mock_run.return_value = mock.MagicMock(returncode=0)
result = runner.invoke(
cli, ["model", "pull", "qwen3.5:8b", "--engine", "mlx"]
cli, ["model", "pull", "qwen3.5:9b", "--engine", "mlx"]
)
assert result.exit_code == 0
mock_run.assert_called_once()
call_args = mock_run.call_args[0][0]
assert "huggingface-cli" in call_args
assert "mlx-community/Qwen3.5-8B-4bit" in call_args
assert "mlx-community/Qwen3.5-9B-MLX-4bit" in call_args
def test_pull_llamacpp_huggingface_cli_not_found(self) -> None:
from openjarvis.cli import cli
@@ -101,7 +102,7 @@ class TestPullCliMultiEngine:
mock_cfg.return_value.engine.ollama_host = None
result = runner.invoke(
cli, ["model", "pull", "qwen3.5:8b", "--engine", "llamacpp"]
cli, ["model", "pull", "qwen3.5:9b", "--engine", "llamacpp"]
)
assert result.exit_code != 0
+12 -8
View File
@@ -27,7 +27,7 @@ class TestRecommendModelGpu:
result = recommend_model(hw, "ollama")
# 14B * 0.5 * 1.1 = 7.7 GB; available = 8 * 0.9 = 7.2 → too big
# 8B * 0.5 * 1.1 = 4.4 GB; available = 7.2 → fits
assert result == "qwen3.5:8b"
assert result == "qwen3.5:9b"
def test_4gb_gpu_picks_qwen35_4b(self) -> None:
hw = HardwareInfo(
@@ -47,7 +47,7 @@ class TestRecommendModelGpu:
)
result = recommend_model(hw, "ollama")
# 3B * 0.5 * 1.1 = 1.65 GB; available = 2 * 0.9 = 1.8 → fits
assert result == "qwen3.5:3b"
assert result == "qwen3.5:2b"
def test_multi_gpu_picks_larger_model(self) -> None:
hw = HardwareInfo(
@@ -80,8 +80,9 @@ class TestRecommendModelCpuOnly:
hw = HardwareInfo(platform="linux", ram_gb=16.0, gpu=None)
result = recommend_model(hw, "llamacpp")
# available = (16 - 4) * 0.8 = 9.6 GB
# 14B * 0.5 * 1.1 = 7.7 → fits
assert result == "qwen3.5:14b"
# 27B * 0.5 * 1.1 = 14.85 → too big
# 9B * 0.5 * 1.1 = 4.95 → fits
assert result == "qwen3.5:9b"
def test_cpu_only_8gb_ram(self) -> None:
hw = HardwareInfo(platform="linux", ram_gb=8.0, gpu=None)
@@ -127,7 +128,7 @@ class TestRecommendModelMlx:
gpu=GpuInfo(vendor="apple", name="Apple M1", vram_gb=8.0, count=1),
)
result = recommend_model(hw, "mlx")
assert result == "qwen3.5:8b"
assert result == "qwen3.5:9b"
def test_apple_silicon_16gb_mlx(self) -> None:
hw = HardwareInfo(
@@ -136,7 +137,10 @@ class TestRecommendModelMlx:
gpu=GpuInfo(vendor="apple", name="Apple M2", vram_gb=16.0, count=1),
)
result = recommend_model(hw, "mlx")
assert result == "qwen3.5:14b"
# available = 16 * 0.9 = 14.4 GB
# 27B * 0.5 * 1.1 = 14.85 → too big
# 9B * 0.5 * 1.1 = 4.95 → fits
assert result == "qwen3.5:9b"
def test_apple_silicon_32gb_mlx(self) -> None:
hw = HardwareInfo(
@@ -145,7 +149,7 @@ class TestRecommendModelMlx:
gpu=GpuInfo(vendor="apple", name="Apple M2 Pro", vram_gb=32.0, count=1),
)
result = recommend_model(hw, "mlx")
assert result == "qwen3.5:14b"
assert result == "qwen3.5:27b"
def test_apple_silicon_64gb_mlx(self) -> None:
hw = HardwareInfo(
@@ -154,4 +158,4 @@ class TestRecommendModelMlx:
gpu=GpuInfo(vendor="apple", name="Apple M2 Max", vram_gb=64.0, count=1),
)
result = recommend_model(hw, "mlx")
assert result == "qwen3.5:14b"
assert result == "qwen3.5:27b"
@@ -186,26 +186,26 @@ class TestCloudModelSpecs:
class TestQwen35ModelSpecs:
"""Verify Qwen3.5 MoE model entries."""
def test_qwen35_3b(self) -> None:
spec = _get_spec("qwen3.5:3b")
assert spec.parameter_count_b == 3.0
assert spec.active_parameter_count_b == 0.6
def test_qwen35_2b(self) -> None:
spec = _get_spec("qwen3.5:2b")
assert spec.parameter_count_b == 2.0
assert spec.active_parameter_count_b == 0.4
assert spec.context_length == 131072
assert spec.provider == "alibaba"
assert spec.metadata["architecture"] == "moe"
for e in ("ollama", "vllm", "llamacpp", "sglang"):
assert e in spec.supported_engines
def test_qwen35_8b(self) -> None:
spec = _get_spec("qwen3.5:8b")
assert spec.parameter_count_b == 8.0
assert spec.active_parameter_count_b == 1.0
def test_qwen35_9b(self) -> None:
spec = _get_spec("qwen3.5:9b")
assert spec.parameter_count_b == 9.0
assert spec.active_parameter_count_b == 1.5
assert spec.context_length == 131072
def test_qwen35_14b(self) -> None:
spec = _get_spec("qwen3.5:14b")
assert spec.parameter_count_b == 14.0
assert spec.active_parameter_count_b == 2.0
def test_qwen35_27b(self) -> None:
spec = _get_spec("qwen3.5:27b")
assert spec.parameter_count_b == 27.0
assert spec.active_parameter_count_b == 3.0
def test_qwen35_35b(self) -> None:
spec = _get_spec("qwen3.5:35b")
@@ -300,9 +300,9 @@ class TestModelDiscovery:
"gpt-oss:120b",
"glm-4.7-flash",
"trinity-mini",
"qwen3.5:3b",
"qwen3.5:8b",
"qwen3.5:14b",
"qwen3.5:2b",
"qwen3.5:9b",
"qwen3.5:27b",
"qwen3.5:35b",
"qwen3.5:122b",
"qwen3.5:397b",