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.
Files changed (220) hide show
  1. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/PKG-INFO +12 -1
  2. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/README.md +11 -0
  3. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/_version.py +2 -2
  4. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/pull.py +4 -0
  5. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/catalog.py +2 -0
  6. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/pull.py +30 -0
  7. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/scoring.py +10 -1
  8. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_init.py +9 -0
  9. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/gemma3-12b.yaml +1 -0
  10. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/gemma3-27b.yaml +1 -0
  11. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/gemma3-4b.yaml +1 -0
  12. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/llama3.3-8b.yaml +1 -0
  13. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_catalog.py +45 -0
  14. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_init.py +63 -0
  15. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_pull.py +103 -0
  16. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_scoring.py +53 -0
  17. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/init.sh +0 -0
  18. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/library/architecture.md +0 -0
  19. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/library/environment.md +0 -0
  20. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/library/user-testing.md +0 -0
  21. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/services.yaml +0 -0
  22. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/settings.json +0 -0
  23. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/skills/cli-feature/SKILL.md +0 -0
  24. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/configuration-management.json +0 -0
  25. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/dependency-management.json +0 -0
  26. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/fix-catalog-errors-and-families.json +0 -0
  27. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/fix-deps-binary-and-ansi.json +0 -0
  28. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/fix-scaffolding-data-home.json +0 -0
  29. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/hardware-detection.json +0 -0
  30. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/model-catalog.json +0 -0
  31. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/reviews/project-scaffolding.json +0 -0
  32. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/synthesis.json +0 -0
  33. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/scrutiny/synthesis.round1.json +0 -0
  34. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-config-basic.json +0 -0
  35. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-config-deps.json +0 -0
  36. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-profile-catalog.json +0 -0
  37. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/flows/foundation-setup-profile-core.json +0 -0
  38. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/foundation/user-testing/synthesis.json +0 -0
  39. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/down-command.json +0 -0
  40. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/fix-lifecycle-preflight-and-readonly.json +0 -0
  41. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/fix-lifecycle-process-robustness.json +0 -0
  42. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/fix-lifecycle-typecheck.json +0 -0
  43. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/process-management.json +0 -0
  44. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/status-command.json +0 -0
  45. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/reviews/up-command.json +0 -0
  46. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/synthesis.json +0 -0
  47. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/scrutiny/synthesis.round1.json +0 -0
  48. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g1-deps-up-basics.json +0 -0
  49. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g2-up-startup.json +0 -0
  50. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g3-up-resilience.json +0 -0
  51. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g4-down.json +0 -0
  52. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g5-status.json +0 -0
  53. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r1-g6-cross.json +0 -0
  54. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r2-g1-fixes.json +0 -0
  55. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/flows/r2-g2-cross-blockers.json +0 -0
  56. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/synthesis.json +0 -0
  57. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/lifecycle/user-testing/synthesis.round1.json +0 -0
  58. {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
  59. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/reviews/misc-cross-area-validation.json +0 -0
  60. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/synthesis.json +0 -0
  61. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/scrutiny/synthesis.round1.json +0 -0
  62. {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
  63. {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
  64. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/user-testing/synthesis.json +0 -0
  65. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/misc-cross-area/user-testing/synthesis.round1.json +0 -0
  66. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/fix-ops-lint-errors.json +0 -0
  67. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/fix-ops-scrutiny-issues.json +0 -0
  68. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/fix-ops-typecheck-errors.json +0 -0
  69. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/launchd-integration.json +0 -0
  70. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/log-rotation.json +0 -0
  71. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/logs-command.json +0 -0
  72. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/ops-cross-area-validation.json +0 -0
  73. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/reviews/watchdog-command.json +0 -0
  74. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/synthesis.json +0 -0
  75. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/scrutiny/synthesis.round1.json +0 -0
  76. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g1-log.json +0 -0
  77. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g2-logs-command.json +0 -0
  78. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g3-watch.json +0 -0
  79. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g4-launchd.json +0 -0
  80. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/flows/g5-cross-ops.json +0 -0
  81. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/ops/user-testing/synthesis.json +0 -0
  82. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/community-docs.json +0 -0
  83. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/developing-guide.json +0 -0
  84. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/fix-public-ready-scrutiny.json +0 -0
  85. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/github-actions-ci.json +0 -0
  86. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/reviews/readme-rewrite.json +0 -0
  87. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/synthesis.json +0 -0
  88. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/public-ready/scrutiny/synthesis.round1.json +0 -0
  89. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/fix-init-and-models-issues.json +0 -0
  90. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/fix-recommendation-scoring-issues.json +0 -0
  91. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/fix-scoring-lint.json +0 -0
  92. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/init-command.json +0 -0
  93. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/models-command.json +0 -0
  94. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/recommend-command.json +0 -0
  95. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/reviews/scoring-engine.json +0 -0
  96. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/synthesis.json +0 -0
  97. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/scrutiny/synthesis.round1.json +0 -0
  98. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g1-recommend-budget-ranking.json +0 -0
  99. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g2-recommend-output-integration.json +0 -0
  100. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g3-init-core-routing.json +0 -0
  101. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g4-init-cloud-overwrite.json +0 -0
  102. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g5-init-hardware-summary.json +0 -0
  103. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g6-models-local.json +0 -0
  104. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/g7-models-catalog.json +0 -0
  105. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/r2-g1-recommend.json +0 -0
  106. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/r2-g2-models-catalog-filters.json +0 -0
  107. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/flows/r2-g3-cross-012.json +0 -0
  108. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/synthesis.json +0 -0
  109. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/recommendation/user-testing/synthesis.round1.json +0 -0
  110. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/reviews/bench-command.json +0 -0
  111. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/reviews/fix-tooling-scrutiny-issues.json +0 -0
  112. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/reviews/pull-command.json +0 -0
  113. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/synthesis.json +0 -0
  114. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/scrutiny/synthesis.round1.json +0 -0
  115. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g1-pull-core.json +0 -0
  116. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g2-pull-errors.json +0 -0
  117. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g3-bench-core.json +0 -0
  118. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/g4-bench-advanced.json +0 -0
  119. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r2-g1-pull.json +0 -0
  120. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r2-g2-bench.json +0 -0
  121. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r3-g1-pull.json +0 -0
  122. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r3-g2-bench.json +0 -0
  123. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/flows/r4-g1-bench.json +0 -0
  124. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.json +0 -0
  125. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.round1.json +0 -0
  126. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.round2.json +0 -0
  127. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.factory/validation/tooling/user-testing/synthesis.round3.json +0 -0
  128. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.github/release.yml +0 -0
  129. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.github/workflows/ci.yml +0 -0
  130. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.github/workflows/publish.yml +0 -0
  131. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/.gitignore +0 -0
  132. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/CHANGELOG.md +0 -0
  133. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  134. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/CONTRIBUTING.md +0 -0
  135. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/DEVELOPING.md +0 -0
  136. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/LICENSE +0 -0
  137. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/SECURITY.md +0 -0
  138. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/pyproject.toml +0 -0
  139. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/__init__.py +0 -0
  140. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/__init__.py +0 -0
  141. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/bench.py +0 -0
  142. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/config.py +0 -0
  143. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/down.py +0 -0
  144. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/init.py +0 -0
  145. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/install.py +0 -0
  146. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/logs.py +0 -0
  147. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/main.py +0 -0
  148. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/models.py +0 -0
  149. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/profile.py +0 -0
  150. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/recommend.py +0 -0
  151. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/status.py +0 -0
  152. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/up.py +0 -0
  153. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/cli/watch.py +0 -0
  154. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/__init__.py +0 -0
  155. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/benchmark.py +0 -0
  156. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/config.py +0 -0
  157. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/deps.py +0 -0
  158. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/hardware.py +0 -0
  159. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/launchd.py +0 -0
  160. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/litellm_gen.py +0 -0
  161. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/log_rotation.py +0 -0
  162. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/log_viewer.py +0 -0
  163. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/models.py +0 -0
  164. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/paths.py +0 -0
  165. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/process.py +0 -0
  166. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_down.py +0 -0
  167. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_status.py +0 -0
  168. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/stack_up.py +0 -0
  169. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/core/watchdog.py +0 -0
  170. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/__init__.py +0 -0
  171. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/__init__.py +0 -0
  172. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/deepseek-r1-32b.yaml +0 -0
  173. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/deepseek-r1-8b.yaml +0 -0
  174. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/nemotron-49b.yaml +0 -0
  175. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/nemotron-8b.yaml +0 -0
  176. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3-8b.yaml +0 -0
  177. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-0.8b.yaml +0 -0
  178. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-14b.yaml +0 -0
  179. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-32b.yaml +0 -0
  180. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-3b.yaml +0 -0
  181. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-72b.yaml +0 -0
  182. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/data/catalog/qwen3.5-8b.yaml +0 -0
  183. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/py.typed +0 -0
  184. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/src/mlx_stack/utils/__init__.py +0 -0
  185. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/__init__.py +0 -0
  186. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/conftest.py +0 -0
  187. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/integration/__init__.py +0 -0
  188. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/integration/test_inference_e2e.py +0 -0
  189. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/integration/test_launchd_e2e.py +0 -0
  190. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/__init__.py +0 -0
  191. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_benchmark.py +0 -0
  192. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli.py +0 -0
  193. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_bench.py +0 -0
  194. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_config.py +0 -0
  195. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_down.py +0 -0
  196. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_install.py +0 -0
  197. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_logs.py +0 -0
  198. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_models.py +0 -0
  199. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_profile.py +0 -0
  200. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_recommend.py +0 -0
  201. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_status.py +0 -0
  202. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_up.py +0 -0
  203. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cli_watch.py +0 -0
  204. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_config.py +0 -0
  205. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_cross_area.py +0 -0
  206. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_data_dir.py +0 -0
  207. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_deps.py +0 -0
  208. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_hardware.py +0 -0
  209. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_launchd.py +0 -0
  210. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_lifecycle_fixes.py +0 -0
  211. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_litellm_gen.py +0 -0
  212. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_log_rotation.py +0 -0
  213. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_log_viewer.py +0 -0
  214. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_models.py +0 -0
  215. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_ops_cross_area.py +0 -0
  216. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_paths.py +0 -0
  217. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_process.py +0 -0
  218. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_robustness_fixes.py +0 -0
  219. {mlx_stack-0.2.0 → mlx_stack-0.3.0}/tests/unit/test_watchdog.py +0 -0
  220. {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.2.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.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 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:
@@ -4,6 +4,7 @@ family: Gemma 3
4
4
  params_b: 12.0
5
5
  architecture: transformer
6
6
  min_mlx_lm_version: "0.22.0"
7
+ gated: true
7
8
  sources:
8
9
  int4:
9
10
  hf_repo: mlx-community/gemma-3-12b-it-4bit
@@ -4,6 +4,7 @@ family: Gemma 3
4
4
  params_b: 27.0
5
5
  architecture: transformer
6
6
  min_mlx_lm_version: "0.22.0"
7
+ gated: true
7
8
  sources:
8
9
  int4:
9
10
  hf_repo: mlx-community/gemma-3-27b-it-4bit
@@ -4,6 +4,7 @@ family: Gemma 3
4
4
  params_b: 4.0
5
5
  architecture: transformer
6
6
  min_mlx_lm_version: "0.22.0"
7
+ gated: true
7
8
  sources:
8
9
  int4:
9
10
  hf_repo: mlx-community/gemma-3-4b-it-4bit
@@ -4,6 +4,7 @@ family: Llama 3.3
4
4
  params_b: 8.0
5
5
  architecture: transformer
6
6
  min_mlx_lm_version: "0.22.0"
7
+ gated: true
7
8
  sources:
8
9
  int4:
9
10
  hf_repo: mlx-community/Llama-3.3-8B-Instruct-4bit
@@ -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