Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/modeldock/cli/commands/install_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@

from modeldock.cli.console import print_error
from modeldock.cli.factory import manager_for
from modeldock.domain.model import Category


def _category_help() -> str:
"""Build help text from the domain category descriptions."""
descriptions = "; ".join(f"{item.value} ({item.description})" for item in Category)
return f"Category name. Available: {descriptions}"


def install_category_cmd(
category: str = typer.Argument(..., help="Category name (e.g. coding)"),
category: str = typer.Argument(..., help=_category_help()),
backend: str = typer.Option(None, "--backend", help="Runtime backend"),
debug: bool = typer.Option(False, "--debug", help="Show traceback"),
) -> None:
Expand Down
25 changes: 19 additions & 6 deletions src/modeldock/domain/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,25 @@ def from_value(cls, value: str) -> Capability:
class Category(str, Enum):
"""High-level model categories used for discovery and bulk install."""

CHAT = "chat"
CODING = "coding"
EMBEDDING = "embedding"
VISION = "vision"
REASONING = "reasoning"
INSTRUCT = "instruct"
_description: str

def __new__(cls, value: str, description: str) -> Category:
obj = str.__new__(cls, value)
obj._value_ = value
obj._description = description
return obj

CHAT = ("chat", "General-purpose conversational models")
CODING = ("coding", "Models optimized for code generation and completion")
EMBEDDING = ("embedding", "Models that convert text into vector representations")
VISION = ("vision", "Models that understand images and text")
REASONING = ("reasoning", "Models optimized for multi-step reasoning")
INSTRUCT = ("instruct", "Models tuned to follow instructions")

@property
def description(self) -> str:
"""Return a human-readable description for CLI help and documentation."""
return self._description

@classmethod
def from_value(cls, value: str) -> Category:
Expand Down
14 changes: 13 additions & 1 deletion tests/unit/test_cli_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import re
from pathlib import Path
from typing import Any, Optional

Expand All @@ -12,7 +13,7 @@
import modeldock.cli.factory as factory
from modeldock.cli.app import app
from modeldock.common.errors import ConfigError
from modeldock.domain.model import ModelRef, RuntimeBackend
from modeldock.domain.model import Category, ModelRef, RuntimeBackend

runner = CliRunner()

Expand Down Expand Up @@ -85,6 +86,17 @@ def test_unknown_backend_exits_nonzero(recording_manager: type[_RecordingManager
assert "Unknown backend" in result.output


def test_install_category_help_includes_descriptions() -> None:
result = runner.invoke(app, ["install-category", "--help"])
stripped = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", result.output)
output = " ".join(stripped.replace("│", " ").split())

assert result.exit_code == 0
for category in list(Category):
assert category.value in output
assert category.description in output


def test_global_backend_reaches_subcommands(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("MODELDOCK_DEFAULT_BACKEND", raising=False)
result = runner.invoke(app, ["--backend", "lmstudio", "config", "show"])
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/test_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ def test_category_from_value() -> None:
Category.from_value("nonsense")


def test_category_descriptions() -> None:
# Pin exact wording so accidental rewording is caught in CI.
expected = {
Category.CHAT: "General-purpose conversational models",
Category.CODING: "Models optimized for code generation and completion",
Category.EMBEDDING: "Models that convert text into vector representations",
Category.VISION: "Models that understand images and text",
Category.REASONING: "Models optimized for multi-step reasoning",
Category.INSTRUCT: "Models tuned to follow instructions",
}

assert {category: category.description for category in Category} == expected


def test_backend_from_value() -> None:
assert RuntimeBackend.from_value("OLLAMA") == RuntimeBackend.OLLAMA
assert RuntimeBackend.from_value("vllm") == RuntimeBackend.VLLM
Expand Down