mlx-stack 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/PKG-INFO +12 -1
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/README.md +11 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/_version.py +2 -2
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/pull.py +4 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/catalog.py +2 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/pull.py +30 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/scoring.py +10 -1
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_init.py +9 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/gemma3-12b.yaml +1 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/gemma3-27b.yaml +1 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/gemma3-4b.yaml +1 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/llama3.3-8b.yaml +1 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_catalog.py +45 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_init.py +63 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_pull.py +103 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_scoring.py +53 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/init.sh +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/library/architecture.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/library/environment.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/library/user-testing.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/services.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/settings.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/skills/cli-feature/SKILL.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/configuration-management.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/dependency-management.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/fix-catalog-errors-and-families.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/fix-deps-binary-and-ansi.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/fix-scaffolding-data-home.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/hardware-detection.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/model-catalog.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/project-scaffolding.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-config-basic.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-config-deps.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-profile-catalog.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-setup-profile-core.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/down-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/fix-lifecycle-preflight-and-readonly.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/fix-lifecycle-process-robustness.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/fix-lifecycle-typecheck.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/process-management.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/status-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/up-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g1-deps-up-basics.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g2-up-startup.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g3-up-resilience.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g4-down.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g5-status.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g6-cross.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r2-g1-fixes.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r2-g2-cross-blockers.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/reviews/fix-cross-area-test-rigor.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/reviews/misc-cross-area-validation.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/user-testing/flows/r1-g1-cross-flows.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/user-testing/flows/r2-g4-cross-port5050.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/user-testing/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/user-testing/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/fix-ops-lint-errors.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/fix-ops-scrutiny-issues.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/fix-ops-typecheck-errors.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/launchd-integration.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/log-rotation.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/logs-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/ops-cross-area-validation.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/watchdog-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g1-log.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g2-logs-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g3-watch.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g4-launchd.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g5-cross-ops.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/community-docs.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/developing-guide.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/fix-public-ready-scrutiny.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/github-actions-ci.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/readme-rewrite.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/fix-init-and-models-issues.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/fix-recommendation-scoring-issues.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/fix-scoring-lint.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/init-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/models-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/recommend-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/scoring-engine.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g1-recommend-budget-ranking.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g2-recommend-output-integration.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g3-init-core-routing.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g4-init-cloud-overwrite.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g5-init-hardware-summary.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g6-models-local.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g7-models-catalog.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/r2-g1-recommend.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/r2-g2-models-catalog-filters.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/r2-g3-cross-012.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/reviews/bench-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/reviews/fix-tooling-scrutiny-issues.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/reviews/pull-command.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g1-pull-core.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g2-pull-errors.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g3-bench-core.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g4-bench-advanced.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r2-g1-pull.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r2-g2-bench.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r3-g1-pull.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r3-g2-bench.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r4-g1-bench.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.round1.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.round2.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.round3.json +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.github/release.yml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.github/workflows/ci.yml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.github/workflows/publish.yml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.gitignore +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/CHANGELOG.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/CONTRIBUTING.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/DEVELOPING.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/LICENSE +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/SECURITY.md +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/pyproject.toml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/bench.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/config.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/down.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/init.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/install.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/logs.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/main.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/models.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/profile.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/recommend.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/status.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/up.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/watch.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/benchmark.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/config.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/deps.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/hardware.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/launchd.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/litellm_gen.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/log_rotation.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/log_viewer.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/models.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/paths.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/process.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_down.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_status.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_up.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/watchdog.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/deepseek-r1-32b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/deepseek-r1-8b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/nemotron-49b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/nemotron-8b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3-8b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-0.8b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-14b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-32b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-3b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-72b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-8b.yaml +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/py.typed +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/utils/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/conftest.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/integration/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/integration/test_inference_e2e.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/integration/test_launchd_e2e.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/__init__.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_benchmark.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_bench.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_config.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_down.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_install.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_logs.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_models.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_profile.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_recommend.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_status.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_up.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_watch.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_config.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cross_area.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_data_dir.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_deps.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_hardware.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_launchd.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_lifecycle_fixes.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_litellm_gen.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_log_rotation.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_log_viewer.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_models.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_ops_cross_area.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_paths.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_process.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_robustness_fixes.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_watchdog.py +0 -0
- {mlx_stack-0.2.0 → mlx_stack-0.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: CLI control plane for local LLM infrastructure on Apple Silicon
|
|
5
5
|
Project-URL: Homepage, https://github.com/weklund/mlx-stack
|
|
6
6
|
Project-URL: Repository, https://github.com/weklund/mlx-stack
|
|
@@ -326,6 +326,17 @@ The built-in catalog includes 15 models across 5 families:
|
|
|
326
326
|
|
|
327
327
|
Each entry includes benchmark data for common Apple Silicon configurations, quality scores, and capability metadata (tool calling, thinking/reasoning, vision).
|
|
328
328
|
|
|
329
|
+
Some models (Gemma 3, Llama 3.3) are **gated** on HuggingFace and require accepting a license before download. `mlx-stack init --accept-defaults` automatically selects non-gated models so the zero-config path works without authentication. To use gated models:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# 1. Accept the model license on huggingface.co
|
|
333
|
+
# 2. Set your token
|
|
334
|
+
export HF_TOKEN=hf_...
|
|
335
|
+
|
|
336
|
+
# 3. Pull the gated model
|
|
337
|
+
mlx-stack pull gemma3-12b
|
|
338
|
+
```
|
|
339
|
+
|
|
329
340
|
## Architecture Details
|
|
330
341
|
|
|
331
342
|
mlx-stack manages a **tiered local inference stack** with three layers:
|
|
@@ -297,6 +297,17 @@ The built-in catalog includes 15 models across 5 families:
|
|
|
297
297
|
|
|
298
298
|
Each entry includes benchmark data for common Apple Silicon configurations, quality scores, and capability metadata (tool calling, thinking/reasoning, vision).
|
|
299
299
|
|
|
300
|
+
Some models (Gemma 3, Llama 3.3) are **gated** on HuggingFace and require accepting a license before download. `mlx-stack init --accept-defaults` automatically selects non-gated models so the zero-config path works without authentication. To use gated models:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# 1. Accept the model license on huggingface.co
|
|
304
|
+
# 2. Set your token
|
|
305
|
+
export HF_TOKEN=hf_...
|
|
306
|
+
|
|
307
|
+
# 3. Pull the gated model
|
|
308
|
+
mlx-stack pull gemma3-12b
|
|
309
|
+
```
|
|
310
|
+
|
|
300
311
|
## Architecture Details
|
|
301
312
|
|
|
302
313
|
mlx-stack manages a **tiered local inference stack** with three layers:
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -18,6 +18,7 @@ from mlx_stack.core.pull import (
|
|
|
18
18
|
ConversionError,
|
|
19
19
|
DiskSpaceError,
|
|
20
20
|
DownloadError,
|
|
21
|
+
GatedModelError,
|
|
21
22
|
InvalidModelError,
|
|
22
23
|
PullError,
|
|
23
24
|
pull_model,
|
|
@@ -80,6 +81,9 @@ def pull(model: str, quant: str | None, bench: bool, force: bool) -> None:
|
|
|
80
81
|
except DiskSpaceError as exc:
|
|
81
82
|
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
82
83
|
raise SystemExit(1) from None
|
|
84
|
+
except GatedModelError as exc:
|
|
85
|
+
console.print(f"[bold red]Authentication required:[/bold red] {exc}")
|
|
86
|
+
raise SystemExit(1) from None
|
|
83
87
|
except DownloadError as exc:
|
|
84
88
|
console.print(f"[bold red]Download error:[/bold red] {exc}")
|
|
85
89
|
raise SystemExit(1) from None
|
|
@@ -127,6 +127,7 @@ class CatalogEntry:
|
|
|
127
127
|
quality: QualityScores
|
|
128
128
|
benchmarks: dict[str, BenchmarkResult]
|
|
129
129
|
tags: list[str] = field(default_factory=list)
|
|
130
|
+
gated: bool = False
|
|
130
131
|
|
|
131
132
|
|
|
132
133
|
# --------------------------------------------------------------------------- #
|
|
@@ -335,6 +336,7 @@ def _parse_entry(data: dict[str, Any]) -> CatalogEntry:
|
|
|
335
336
|
quality=quality,
|
|
336
337
|
benchmarks=benchmarks,
|
|
337
338
|
tags=list(data.get("tags", [])),
|
|
339
|
+
gated=bool(data.get("gated", False)),
|
|
338
340
|
)
|
|
339
341
|
except (ValueError, TypeError) as exc:
|
|
340
342
|
msg = f"Catalog entry '{model_id}': invalid top-level field value: {exc}"
|
|
@@ -19,6 +19,8 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
21
|
from huggingface_hub import snapshot_download
|
|
22
|
+
from huggingface_hub.errors import GatedRepoError
|
|
23
|
+
from huggingface_hub.utils._auth import get_token
|
|
22
24
|
from rich.console import Console
|
|
23
25
|
|
|
24
26
|
from mlx_stack.core.catalog import CatalogEntry, QuantSource, get_entry_by_id, load_catalog
|
|
@@ -42,6 +44,10 @@ class DownloadError(PullError):
|
|
|
42
44
|
"""Raised when model download fails."""
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
class GatedModelError(DownloadError):
|
|
48
|
+
"""Raised when a gated model requires HuggingFace authentication."""
|
|
49
|
+
|
|
50
|
+
|
|
45
51
|
class ConversionError(PullError):
|
|
46
52
|
"""Raised when mlx_lm conversion fails."""
|
|
47
53
|
|
|
@@ -321,6 +327,14 @@ def _run_download(
|
|
|
321
327
|
"""
|
|
322
328
|
try:
|
|
323
329
|
snapshot_download(repo_id=hf_repo, local_dir=str(local_dir))
|
|
330
|
+
except GatedRepoError:
|
|
331
|
+
msg = (
|
|
332
|
+
f"Access denied for {hf_repo} — this is a gated model.\n"
|
|
333
|
+
f"Your HuggingFace token does not have access.\n"
|
|
334
|
+
f"Accept the model license at: https://huggingface.co/{hf_repo}\n"
|
|
335
|
+
f"Then retry: mlx-stack pull"
|
|
336
|
+
)
|
|
337
|
+
raise GatedModelError(msg) from None
|
|
324
338
|
except Exception as exc:
|
|
325
339
|
msg = f"Download failed for {hf_repo}: {exc}"
|
|
326
340
|
raise DownloadError(msg) from None
|
|
@@ -572,6 +586,22 @@ def pull_model(
|
|
|
572
586
|
)
|
|
573
587
|
raise InvalidModelError(msg)
|
|
574
588
|
|
|
589
|
+
# 1b. Pre-flight auth check for gated models
|
|
590
|
+
if entry.gated and get_token() is None:
|
|
591
|
+
msg = (
|
|
592
|
+
f"Model '{entry.name}' requires HuggingFace authentication "
|
|
593
|
+
f"(gated model).\n\n"
|
|
594
|
+
f"To download gated models:\n"
|
|
595
|
+
f" 1. Accept the model license on HuggingFace\n"
|
|
596
|
+
f" 2. Authenticate using ONE of:\n"
|
|
597
|
+
f" - export HF_TOKEN=hf_... "
|
|
598
|
+
f"(get a token at huggingface.co/settings/tokens)\n"
|
|
599
|
+
f" - huggingface-cli login\n\n"
|
|
600
|
+
f"Or use a non-gated alternative:\n"
|
|
601
|
+
f" mlx-stack models --catalog"
|
|
602
|
+
)
|
|
603
|
+
raise GatedModelError(msg)
|
|
604
|
+
|
|
575
605
|
# 2. Determine quantization
|
|
576
606
|
if quant is None:
|
|
577
607
|
try:
|
|
@@ -410,6 +410,7 @@ def score_and_filter(
|
|
|
410
410
|
budget_gb: float,
|
|
411
411
|
quant: str = _DEFAULT_QUANT,
|
|
412
412
|
saved_benchmarks: dict[str, Any] | None = None,
|
|
413
|
+
exclude_gated: bool = False,
|
|
413
414
|
) -> list[ScoredModel]:
|
|
414
415
|
"""Score all catalog models and filter by memory budget.
|
|
415
416
|
|
|
@@ -423,6 +424,8 @@ def score_and_filter(
|
|
|
423
424
|
budget_gb: Memory budget in GB.
|
|
424
425
|
quant: Quantization level.
|
|
425
426
|
saved_benchmarks: Optional saved benchmark data.
|
|
427
|
+
exclude_gated: If True, exclude gated models that require
|
|
428
|
+
HuggingFace authentication.
|
|
426
429
|
|
|
427
430
|
Returns:
|
|
428
431
|
List of ScoredModel instances within budget, sorted by score descending.
|
|
@@ -439,6 +442,9 @@ def score_and_filter(
|
|
|
439
442
|
scored: list[ScoredModel] = []
|
|
440
443
|
|
|
441
444
|
for entry in catalog:
|
|
445
|
+
if exclude_gated and entry.gated:
|
|
446
|
+
continue
|
|
447
|
+
|
|
442
448
|
try:
|
|
443
449
|
model = score_model(entry, profile, weights, budget_gb, quant, saved_benchmarks)
|
|
444
450
|
except ScoringError:
|
|
@@ -558,6 +564,7 @@ def recommend(
|
|
|
558
564
|
budget_gb_override: float | None = None,
|
|
559
565
|
quant: str = _DEFAULT_QUANT,
|
|
560
566
|
saved_benchmarks: dict[str, Any] | None = None,
|
|
567
|
+
exclude_gated: bool = False,
|
|
561
568
|
) -> RecommendationResult:
|
|
562
569
|
"""Generate a recommendation for the given hardware and intent.
|
|
563
570
|
|
|
@@ -576,6 +583,7 @@ def recommend(
|
|
|
576
583
|
percentage-based calculation.
|
|
577
584
|
quant: Default quantization level.
|
|
578
585
|
saved_benchmarks: Optional saved benchmark data from bench --save.
|
|
586
|
+
exclude_gated: If True, exclude gated models from recommendations.
|
|
579
587
|
|
|
580
588
|
Returns:
|
|
581
589
|
A RecommendationResult with tier assignments and all scored models.
|
|
@@ -596,7 +604,8 @@ def recommend(
|
|
|
596
604
|
|
|
597
605
|
# Score and filter
|
|
598
606
|
scored = score_and_filter(
|
|
599
|
-
catalog, profile, intent, budget_gb, quant, saved_benchmarks
|
|
607
|
+
catalog, profile, intent, budget_gb, quant, saved_benchmarks,
|
|
608
|
+
exclude_gated=exclude_gated,
|
|
600
609
|
)
|
|
601
610
|
|
|
602
611
|
# Assign tiers
|
|
@@ -376,6 +376,7 @@ def run_init(
|
|
|
376
376
|
profile=profile,
|
|
377
377
|
intent=intent,
|
|
378
378
|
budget_pct=budget_pct,
|
|
379
|
+
exclude_gated=True,
|
|
379
380
|
)
|
|
380
381
|
except ScoringError as exc:
|
|
381
382
|
msg = f"Recommendation failed: {exc}"
|
|
@@ -432,6 +433,14 @@ def run_init(
|
|
|
432
433
|
msg = f"Cannot add model '{model_id}': {exc}"
|
|
433
434
|
raise InitError(msg) from None
|
|
434
435
|
|
|
436
|
+
# Warn if the model is gated (requires HuggingFace auth)
|
|
437
|
+
if entry.gated:
|
|
438
|
+
warnings.append(
|
|
439
|
+
f"Model '{model_id}' is gated and requires HuggingFace "
|
|
440
|
+
f"authentication. Set HF_TOKEN or run 'huggingface-cli login' "
|
|
441
|
+
f"before pulling."
|
|
442
|
+
)
|
|
443
|
+
|
|
435
444
|
# Warn if exceeding budget (per spec: warn, not block)
|
|
436
445
|
total_memory = sum(t.model.memory_gb for t in tiers) + scored.memory_gb
|
|
437
446
|
if total_memory > recommendation.memory_budget_gb:
|
|
@@ -19,6 +19,7 @@ from mlx_stack.core.catalog import (
|
|
|
19
19
|
CatalogError,
|
|
20
20
|
QualityScores,
|
|
21
21
|
QuantSource,
|
|
22
|
+
_parse_entry,
|
|
22
23
|
get_entry_by_id,
|
|
23
24
|
load_catalog,
|
|
24
25
|
load_catalog_from_directory,
|
|
@@ -793,3 +794,47 @@ def _make_valid_entry() -> dict:
|
|
|
793
794
|
},
|
|
794
795
|
"tags": ["test"],
|
|
795
796
|
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
# =========================================================================== #
|
|
800
|
+
# Gated field tests
|
|
801
|
+
# =========================================================================== #
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class TestGatedField:
|
|
805
|
+
"""Tests for the CatalogEntry.gated field."""
|
|
806
|
+
|
|
807
|
+
def test_gated_defaults_to_false(self) -> None:
|
|
808
|
+
"""CatalogEntry without gated field defaults to False."""
|
|
809
|
+
data = _make_valid_entry()
|
|
810
|
+
entry = _parse_entry(data)
|
|
811
|
+
assert entry.gated is False
|
|
812
|
+
|
|
813
|
+
def test_gated_true_from_yaml(self) -> None:
|
|
814
|
+
"""CatalogEntry with gated: true parses correctly."""
|
|
815
|
+
data = _make_valid_entry()
|
|
816
|
+
data["gated"] = True
|
|
817
|
+
entry = _parse_entry(data)
|
|
818
|
+
assert entry.gated is True
|
|
819
|
+
|
|
820
|
+
def test_gated_false_explicit(self) -> None:
|
|
821
|
+
"""Explicit gated: false parses correctly."""
|
|
822
|
+
data = _make_valid_entry()
|
|
823
|
+
data["gated"] = False
|
|
824
|
+
entry = _parse_entry(data)
|
|
825
|
+
assert entry.gated is False
|
|
826
|
+
|
|
827
|
+
def test_shipped_catalog_gated_models(self) -> None:
|
|
828
|
+
"""Shipped catalog has exactly 4 gated models."""
|
|
829
|
+
catalog = load_catalog()
|
|
830
|
+
gated = [e for e in catalog if e.gated]
|
|
831
|
+
gated_ids = {e.id for e in gated}
|
|
832
|
+
assert gated_ids == {"gemma3-4b", "gemma3-12b", "gemma3-27b", "llama3.3-8b"}
|
|
833
|
+
|
|
834
|
+
def test_shipped_catalog_non_gated_models(self) -> None:
|
|
835
|
+
"""Shipped catalog non-gated models all have gated=False."""
|
|
836
|
+
catalog = load_catalog()
|
|
837
|
+
non_gated = [e for e in catalog if not e.gated]
|
|
838
|
+
assert len(non_gated) == len(catalog) - 4
|
|
839
|
+
for entry in non_gated:
|
|
840
|
+
assert entry.gated is False
|
|
@@ -98,6 +98,7 @@ def _make_entry(
|
|
|
98
98
|
benchmarks: dict[str, BenchmarkResult] | None = None,
|
|
99
99
|
tags: list[str] | None = None,
|
|
100
100
|
memory_gb: float = 5.5,
|
|
101
|
+
gated: bool = False,
|
|
101
102
|
) -> CatalogEntry:
|
|
102
103
|
"""Create a CatalogEntry for testing."""
|
|
103
104
|
if benchmarks is None:
|
|
@@ -130,6 +131,7 @@ def _make_entry(
|
|
|
130
131
|
),
|
|
131
132
|
benchmarks=benchmarks,
|
|
132
133
|
tags=tags or [],
|
|
134
|
+
gated=gated,
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
|
|
@@ -1192,3 +1194,64 @@ class TestTotalEstimatedMemory:
|
|
|
1192
1194
|
assert result["total_memory_gb"] > 0
|
|
1193
1195
|
# Total memory should be reasonable (less than total system memory)
|
|
1194
1196
|
assert result["total_memory_gb"] < profile.memory_gb
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
# =========================================================================== #
|
|
1200
|
+
# Gated model exclusion tests
|
|
1201
|
+
# =========================================================================== #
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
class TestGatedModelExclusion:
|
|
1205
|
+
"""Tests that gated models are excluded from default init."""
|
|
1206
|
+
|
|
1207
|
+
def test_init_excludes_gated_models(self, mlx_stack_home: Path) -> None:
|
|
1208
|
+
"""Default init excludes gated models from tier assignments."""
|
|
1209
|
+
profile = _make_profile()
|
|
1210
|
+
_write_profile(mlx_stack_home, profile)
|
|
1211
|
+
|
|
1212
|
+
# Create catalog where the best model is gated
|
|
1213
|
+
catalog = [
|
|
1214
|
+
_make_entry(
|
|
1215
|
+
model_id="gated-best",
|
|
1216
|
+
name="Gated Best",
|
|
1217
|
+
quality_overall=99,
|
|
1218
|
+
gated=True,
|
|
1219
|
+
),
|
|
1220
|
+
_make_entry(
|
|
1221
|
+
model_id="open-good",
|
|
1222
|
+
name="Open Good",
|
|
1223
|
+
quality_overall=70,
|
|
1224
|
+
gated=False,
|
|
1225
|
+
),
|
|
1226
|
+
]
|
|
1227
|
+
|
|
1228
|
+
with patch("mlx_stack.core.stack_init.load_catalog", return_value=catalog), \
|
|
1229
|
+
patch("mlx_stack.core.stack_init.load_profile", return_value=profile):
|
|
1230
|
+
result = run_init(intent="balanced", force=True)
|
|
1231
|
+
|
|
1232
|
+
tier_model_ids = {t["model"] for t in result["stack"]["tiers"]}
|
|
1233
|
+
assert "gated-best" not in tier_model_ids
|
|
1234
|
+
assert "open-good" in tier_model_ids
|
|
1235
|
+
|
|
1236
|
+
def test_add_gated_model_warns(self, mlx_stack_home: Path) -> None:
|
|
1237
|
+
"""Adding a gated model via --add produces a warning."""
|
|
1238
|
+
profile = _make_profile()
|
|
1239
|
+
_write_profile(mlx_stack_home, profile)
|
|
1240
|
+
|
|
1241
|
+
catalog = [
|
|
1242
|
+
_make_entry(model_id="open-model", name="Open Model"),
|
|
1243
|
+
_make_entry(model_id="gated-model", name="Gated Model", gated=True),
|
|
1244
|
+
]
|
|
1245
|
+
|
|
1246
|
+
with patch("mlx_stack.core.stack_init.load_catalog", return_value=catalog), \
|
|
1247
|
+
patch("mlx_stack.core.stack_init.load_profile", return_value=profile):
|
|
1248
|
+
result = run_init(
|
|
1249
|
+
intent="balanced",
|
|
1250
|
+
add_models=["gated-model"],
|
|
1251
|
+
force=True,
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
warnings = result["warnings"]
|
|
1255
|
+
gated_warnings = [w for w in warnings if "gated" in w.lower()]
|
|
1256
|
+
assert len(gated_warnings) >= 1
|
|
1257
|
+
assert "HuggingFace authentication" in gated_warnings[0]
|
|
@@ -35,6 +35,7 @@ from mlx_stack.core.pull import (
|
|
|
35
35
|
ConversionError,
|
|
36
36
|
DiskSpaceError,
|
|
37
37
|
DownloadError,
|
|
38
|
+
GatedModelError,
|
|
38
39
|
InvalidModelError,
|
|
39
40
|
ModelInventoryEntry,
|
|
40
41
|
PullError,
|
|
@@ -65,6 +66,7 @@ def _make_entry(
|
|
|
65
66
|
disk_size_gb: float = 4.5,
|
|
66
67
|
disk_size_gb_int8: float = 8.5,
|
|
67
68
|
disk_size_gb_bf16: float = 16.0,
|
|
69
|
+
gated: bool = False,
|
|
68
70
|
) -> CatalogEntry:
|
|
69
71
|
"""Create a CatalogEntry for testing."""
|
|
70
72
|
return CatalogEntry(
|
|
@@ -101,6 +103,7 @@ def _make_entry(
|
|
|
101
103
|
"m4-max-128": BenchmarkResult(prompt_tps=140.0, gen_tps=77.0, memory_gb=5.5),
|
|
102
104
|
},
|
|
103
105
|
tags=["balanced", "agent-ready"],
|
|
106
|
+
gated=gated,
|
|
104
107
|
)
|
|
105
108
|
|
|
106
109
|
|
|
@@ -1244,3 +1247,103 @@ class TestPullModelsIntegration:
|
|
|
1244
1247
|
assert result.exit_code == 0
|
|
1245
1248
|
# The model directory or catalog name should appear
|
|
1246
1249
|
assert "qwen3.5-8b-4bit" in result.output or "Qwen 3.5 8B" in result.output
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
# =========================================================================== #
|
|
1253
|
+
# Gated model handling tests
|
|
1254
|
+
# =========================================================================== #
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
class TestGatedModelHandling:
|
|
1258
|
+
"""Tests for gated model pre-flight check and error handling."""
|
|
1259
|
+
|
|
1260
|
+
@patch("mlx_stack.core.pull.get_token", return_value=None)
|
|
1261
|
+
def test_gated_model_without_token_raises(
|
|
1262
|
+
self,
|
|
1263
|
+
mock_token: MagicMock,
|
|
1264
|
+
mlx_stack_home: Path,
|
|
1265
|
+
) -> None:
|
|
1266
|
+
"""Gated model without HF token raises GatedModelError."""
|
|
1267
|
+
import pytest
|
|
1268
|
+
|
|
1269
|
+
catalog = [_make_entry(gated=True)]
|
|
1270
|
+
with pytest.raises(GatedModelError, match="requires HuggingFace authentication"):
|
|
1271
|
+
pull_model("qwen3.5-8b", quant="int4", catalog=catalog)
|
|
1272
|
+
|
|
1273
|
+
@patch("mlx_stack.core.pull.download_model")
|
|
1274
|
+
@patch("mlx_stack.core.pull.check_disk_space", return_value=(True, 100.0))
|
|
1275
|
+
@patch("mlx_stack.core.pull.get_token", return_value="hf_test_token")
|
|
1276
|
+
def test_gated_model_with_token_proceeds(
|
|
1277
|
+
self,
|
|
1278
|
+
mock_token: MagicMock,
|
|
1279
|
+
mock_space: MagicMock,
|
|
1280
|
+
mock_download: MagicMock,
|
|
1281
|
+
mlx_stack_home: Path,
|
|
1282
|
+
) -> None:
|
|
1283
|
+
"""Gated model with valid token proceeds to download."""
|
|
1284
|
+
catalog = [_make_entry(gated=True)]
|
|
1285
|
+
result = pull_model("qwen3.5-8b", quant="int4", catalog=catalog)
|
|
1286
|
+
assert result.already_existed is False
|
|
1287
|
+
mock_download.assert_called_once()
|
|
1288
|
+
|
|
1289
|
+
@patch("mlx_stack.core.pull.get_token", return_value=None)
|
|
1290
|
+
def test_non_gated_model_skips_token_check(
|
|
1291
|
+
self,
|
|
1292
|
+
mock_token: MagicMock,
|
|
1293
|
+
mlx_stack_home: Path,
|
|
1294
|
+
) -> None:
|
|
1295
|
+
"""Non-gated model does not check for token."""
|
|
1296
|
+
catalog = [_make_entry(gated=False)]
|
|
1297
|
+
with patch("mlx_stack.core.pull.download_model"):
|
|
1298
|
+
with patch("mlx_stack.core.pull.check_disk_space", return_value=(True, 100.0)):
|
|
1299
|
+
pull_model("qwen3.5-8b", quant="int4", catalog=catalog)
|
|
1300
|
+
# get_token was not called (non-gated path doesn't reach the check)
|
|
1301
|
+
mock_token.assert_not_called()
|
|
1302
|
+
|
|
1303
|
+
@patch("mlx_stack.core.pull.snapshot_download")
|
|
1304
|
+
def test_gated_repo_error_caught(
|
|
1305
|
+
self,
|
|
1306
|
+
mock_snapshot: MagicMock,
|
|
1307
|
+
tmp_path: Path,
|
|
1308
|
+
) -> None:
|
|
1309
|
+
"""GatedRepoError from snapshot_download becomes GatedModelError."""
|
|
1310
|
+
from io import StringIO
|
|
1311
|
+
from unittest.mock import MagicMock as Mock
|
|
1312
|
+
|
|
1313
|
+
import pytest
|
|
1314
|
+
from huggingface_hub.errors import GatedRepoError as HfGatedRepoError
|
|
1315
|
+
from rich.console import Console
|
|
1316
|
+
|
|
1317
|
+
from mlx_stack.core.pull import _run_download
|
|
1318
|
+
|
|
1319
|
+
# GatedRepoError requires a response object
|
|
1320
|
+
mock_response = Mock()
|
|
1321
|
+
mock_response.status_code = 403
|
|
1322
|
+
mock_response.headers = {}
|
|
1323
|
+
mock_response.url = "https://huggingface.co/test/repo"
|
|
1324
|
+
mock_snapshot.side_effect = HfGatedRepoError(
|
|
1325
|
+
"gated repo", response=mock_response
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
local_dir = tmp_path / "model"
|
|
1329
|
+
local_dir.mkdir()
|
|
1330
|
+
console = Console(file=StringIO())
|
|
1331
|
+
|
|
1332
|
+
with pytest.raises(GatedModelError, match="gated model"):
|
|
1333
|
+
_run_download("test/repo", local_dir, console)
|
|
1334
|
+
|
|
1335
|
+
@patch("mlx_stack.core.pull.get_token", return_value=None)
|
|
1336
|
+
@patch("mlx_stack.core.pull.load_catalog")
|
|
1337
|
+
def test_cli_gated_error_shows_auth_required(
|
|
1338
|
+
self,
|
|
1339
|
+
mock_catalog: MagicMock,
|
|
1340
|
+
mock_token: MagicMock,
|
|
1341
|
+
mlx_stack_home: Path,
|
|
1342
|
+
) -> None:
|
|
1343
|
+
"""CLI shows 'Authentication required' for gated model errors."""
|
|
1344
|
+
mock_catalog.return_value = [_make_entry(gated=True)]
|
|
1345
|
+
|
|
1346
|
+
runner = CliRunner()
|
|
1347
|
+
result = runner.invoke(cli, ["pull", "qwen3.5-8b"])
|
|
1348
|
+
assert result.exit_code == 1
|
|
1349
|
+
assert "Authentication required" in result.output
|
|
@@ -69,6 +69,7 @@ def _make_entry(
|
|
|
69
69
|
thinking: bool = False,
|
|
70
70
|
benchmarks: dict[str, BenchmarkResult] | None = None,
|
|
71
71
|
tags: list[str] | None = None,
|
|
72
|
+
gated: bool = False,
|
|
72
73
|
) -> CatalogEntry:
|
|
73
74
|
"""Helper to create a CatalogEntry for testing."""
|
|
74
75
|
if benchmarks is None:
|
|
@@ -103,6 +104,7 @@ def _make_entry(
|
|
|
103
104
|
),
|
|
104
105
|
benchmarks=benchmarks,
|
|
105
106
|
tags=tags or ["balanced"],
|
|
107
|
+
gated=gated,
|
|
106
108
|
)
|
|
107
109
|
|
|
108
110
|
|
|
@@ -1401,3 +1403,54 @@ class TestIntentDifferentiation:
|
|
|
1401
1403
|
# The balanced-model should win because its composite score is higher
|
|
1402
1404
|
# even though slow-quality has higher raw quality
|
|
1403
1405
|
assert standard.model.entry.id == "balanced-model"
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
# =========================================================================== #
|
|
1409
|
+
# Gated model filtering tests
|
|
1410
|
+
# =========================================================================== #
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
class TestExcludeGated:
|
|
1414
|
+
"""Tests for exclude_gated parameter in score_and_filter."""
|
|
1415
|
+
|
|
1416
|
+
def test_exclude_gated_filters_gated_models(self, m4_max_128_profile: HardwareProfile) -> None:
|
|
1417
|
+
"""Gated models are excluded when exclude_gated=True."""
|
|
1418
|
+
open_model = _make_entry(model_id="open-model", name="Open Model")
|
|
1419
|
+
gated_model = _make_entry(model_id="gated-model", name="Gated Model", gated=True)
|
|
1420
|
+
|
|
1421
|
+
scored = score_and_filter(
|
|
1422
|
+
[open_model, gated_model], m4_max_128_profile, "balanced", 51.2,
|
|
1423
|
+
exclude_gated=True,
|
|
1424
|
+
)
|
|
1425
|
+
scored_ids = {m.entry.id for m in scored}
|
|
1426
|
+
assert "open-model" in scored_ids
|
|
1427
|
+
assert "gated-model" not in scored_ids
|
|
1428
|
+
|
|
1429
|
+
def test_exclude_gated_false_includes_all(self, m4_max_128_profile: HardwareProfile) -> None:
|
|
1430
|
+
"""All models included when exclude_gated=False (default)."""
|
|
1431
|
+
open_model = _make_entry(model_id="open-model", name="Open Model")
|
|
1432
|
+
gated_model = _make_entry(model_id="gated-model", name="Gated Model", gated=True)
|
|
1433
|
+
|
|
1434
|
+
scored = score_and_filter(
|
|
1435
|
+
[open_model, gated_model], m4_max_128_profile, "balanced", 51.2,
|
|
1436
|
+
exclude_gated=False,
|
|
1437
|
+
)
|
|
1438
|
+
scored_ids = {m.entry.id for m in scored}
|
|
1439
|
+
assert "open-model" in scored_ids
|
|
1440
|
+
assert "gated-model" in scored_ids
|
|
1441
|
+
|
|
1442
|
+
def test_recommend_exclude_gated(self, m4_max_128_profile: HardwareProfile) -> None:
|
|
1443
|
+
"""Gated models excluded from tier assignments via recommend()."""
|
|
1444
|
+
open_model = _make_entry(model_id="open-model", name="Open Model")
|
|
1445
|
+
gated_model = _make_entry(
|
|
1446
|
+
model_id="gated-model", name="Gated Model",
|
|
1447
|
+
quality_overall=99, gated=True,
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
result = recommend(
|
|
1451
|
+
[open_model, gated_model], m4_max_128_profile,
|
|
1452
|
+
exclude_gated=True,
|
|
1453
|
+
)
|
|
1454
|
+
tier_ids = {t.model.entry.id for t in result.tiers}
|
|
1455
|
+
assert "gated-model" not in tier_ids
|
|
1456
|
+
assert "open-model" in tier_ids
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/synthesis.round1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/synthesis.json
RENAMED
|
File without changes
|
{mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/down-command.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|