From f7d2cce86b88f195b263e1fb7d553b22b8510f62 Mon Sep 17 00:00:00 2001 From: Jana Bergant Date: Thu, 26 Mar 2026 08:29:31 +0100 Subject: [PATCH] fix: correct Qwen3.5 model sizes and MLX repos in catalog The model catalog listed non-existent Qwen3.5 sizes (3B, 8B, 14B) and pointed to MLX community repos that don't exist, causing `jarvis init` to recommend models that cannot be downloaded on Apple Silicon. Replace with the actual Qwen3.5 model family sizes (0.8B, 2B, 9B, 27B) and verified mlx-community repo URLs from HuggingFace. Fixes #129 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/openjarvis/cli/doctor_cmd.py | 45 ++++---------- src/openjarvis/cli/hints.py | 2 +- src/openjarvis/cli/init_cmd.py | 36 ++++++----- src/openjarvis/cli/quickstart_cmd.py | 8 +-- src/openjarvis/evals/configs/smoke_apple.toml | 4 +- src/openjarvis/intelligence/model_catalog.py | 61 +++++++++++-------- tests/cli/test_init_guidance.py | 42 ++++--------- tests/cli/test_model_pull.py | 15 ++--- tests/core/test_recommend_model.py | 20 +++--- .../test_model_catalog_extended.py | 30 ++++----- 10 files changed, 123 insertions(+), 140 deletions(-) diff --git a/src/openjarvis/cli/doctor_cmd.py b/src/openjarvis/cli/doctor_cmd.py index f79a536b..dbfe2280 100644 --- a/src/openjarvis/cli/doctor_cmd.py +++ b/src/openjarvis/cli/doctor_cmd.py @@ -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() diff --git a/src/openjarvis/cli/hints.py b/src/openjarvis/cli/hints.py index 9c8617cf..792bd0b1 100644 --- a/src/openjarvis/cli/hints.py +++ b/src/openjarvis/cli/hints.py @@ -36,6 +36,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." ) diff --git a/src/openjarvis/cli/init_cmd.py b/src/openjarvis/cli/init_cmd.py index 8d729b7e..8eaa4bfe 100644 --- a/src/openjarvis/cli/init_cmd.py +++ b/src/openjarvis/cli/init_cmd.py @@ -25,8 +25,14 @@ from openjarvis.core.config import ( # Engines supported by ``jarvis init --engine``. _SUPPORTED_ENGINES = [ - "ollama", "vllm", "sglang", "llamacpp", "mlx", "lmstudio", - "exo", "nexa", + "ollama", + "vllm", + "sglang", + "llamacpp", + "mlx", + "lmstudio", + "exo", + "nexa", ] @@ -57,7 +63,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" @@ -70,7 +76,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: f" ollama pull {pull_model}\n" "\n" " 3. Try it out:\n" - " jarvis ask \"Hello\"\n" + ' jarvis ask "Hello"\n' "\n" " Run `jarvis doctor` to verify your setup." ), @@ -82,7 +88,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " vllm serve Qwen/Qwen3-4B\n" "\n" " 2. Try it out:\n" - " jarvis ask \"Hello\"\n" + ' jarvis ask "Hello"\n' "\n" " Run `jarvis doctor` to verify your setup." ), @@ -94,7 +100,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " llama-server -m path/to/model.gguf\n" "\n" " 2. Try it out:\n" - " jarvis ask \"Hello\"\n" + ' jarvis ask "Hello"\n' "\n" " Run `jarvis doctor` to verify your setup." ), @@ -106,7 +112,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " python -m sglang.launch_server --model-path Qwen/Qwen3-8B\n" "\n" " 2. Try it out:\n" - " jarvis ask \"Hello\"\n" + ' jarvis ask "Hello"\n' "\n" " Run `jarvis doctor` to verify your setup." ), @@ -118,7 +124,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " mlx_lm.server --model mlx-community/Qwen2.5-7B-4bit\n" "\n" " 2. Try it out:\n" - " jarvis ask \"Hello\"\n" + ' jarvis ask "Hello"\n' "\n" " Run `jarvis doctor` to verify your setup." ), @@ -131,7 +137,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " 2. Load a model and start the local server (port 1234)\n" "\n" " 3. Try it out:\n" - " jarvis ask \"Hello\"\n" + ' jarvis ask "Hello"\n' "\n" " Run `jarvis doctor` to verify your setup." ), @@ -141,7 +147,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " pip install exo\n" " exo\n\n" " 2. Try it out:\n" - " jarvis ask \"Hello\"\n\n" + ' jarvis ask "Hello"\n\n' " Run `jarvis doctor` to verify your setup." ), "nexa": ( @@ -150,7 +156,7 @@ def _next_steps_text(engine: str, model: str = "") -> str: " pip install nexaai\n" " nexa server\n\n" " 2. Try it out:\n" - " jarvis ask \"Hello\"\n\n" + ' jarvis ask "Hello"\n\n' " Run `jarvis doctor` to verify your setup." ), } @@ -177,6 +183,7 @@ def _quick_privacy_check(console: Console) -> None: def _do_download(engine: str, model: str, spec, console: Console) -> None: """Dispatch model download based on engine type.""" import os + if engine == "ollama": host = os.environ.get("OLLAMA_HOST", "http://localhost:11434").rstrip("/") ollama_pull(host, model, console) @@ -267,9 +274,7 @@ def init( console.print("[bold]Detecting running inference engines...[/bold]") running = _detect_running_engines() if running: - console.print( - f" Found running: [green]{', '.join(running)}[/green]" - ) + console.print(f" Found running: [green]{', '.join(running)}[/green]") else: console.print(" No running engines detected.") @@ -341,8 +346,7 @@ def init( soul_path = DEFAULT_CONFIG_DIR / "SOUL.md" if not soul_path.exists(): soul_path.write_text( - "# Agent Persona\n\n" - "You are Jarvis, a helpful personal AI assistant.\n" + "# Agent Persona\n\nYou are Jarvis, a helpful personal AI assistant.\n" ) memory_path = DEFAULT_CONFIG_DIR / "MEMORY.md" diff --git a/src/openjarvis/cli/quickstart_cmd.py b/src/openjarvis/cli/quickstart_cmd.py index 67c8db1b..06d6f4aa 100644 --- a/src/openjarvis/cli/quickstart_cmd.py +++ b/src/openjarvis/cli/quickstart_cmd.py @@ -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]' ) diff --git a/src/openjarvis/evals/configs/smoke_apple.toml b/src/openjarvis/evals/configs/smoke_apple.toml index 478fb014..a058577c 100644 --- a/src/openjarvis/evals/configs/smoke_apple.toml +++ b/src/openjarvis/evals/configs/smoke_apple.toml @@ -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 ──────────────────────────────────────────────────────────────── diff --git a/src/openjarvis/intelligence/model_catalog.py b/src/openjarvis/intelligence/model_catalog.py index ee36976f..af561c6a 100644 --- a/src/openjarvis/intelligence/model_catalog.py +++ b/src/openjarvis/intelligence/model_catalog.py @@ -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", diff --git a/tests/cli/test_init_guidance.py b/tests/cli/test_init_guidance.py index acca3c0c..dfde2c37 100644 --- a/tests/cli/test_init_guidance.py +++ b/tests/cli/test_init_guidance.py @@ -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 diff --git a/tests/cli/test_model_pull.py b/tests/cli/test_model_pull.py index 3a724950..13f74067 100644 --- a/tests/cli/test_model_pull.py +++ b/tests/cli/test_model_pull.py @@ -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 diff --git a/tests/core/test_recommend_model.py b/tests/core/test_recommend_model.py index 61600b69..1a5cc5aa 100644 --- a/tests/core/test_recommend_model.py +++ b/tests/core/test_recommend_model.py @@ -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" diff --git a/tests/intelligence/test_model_catalog_extended.py b/tests/intelligence/test_model_catalog_extended.py index bb5e8d38..f869810b 100644 --- a/tests/intelligence/test_model_catalog_extended.py +++ b/tests/intelligence/test_model_catalog_extended.py @@ -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",