hugpy-engine 0.2.3__tar.gz → 0.2.4__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 (191) hide show
  1. {hugpy_engine-0.2.3/src/hugpy_engine.egg-info → hugpy_engine-0.2.4}/PKG-INFO +1 -1
  2. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/get_module.py +50 -2
  3. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/main.py +8 -0
  4. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/models/model_meta.py +10 -2
  5. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/models/models_config.py +176 -0
  6. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/dispatch/activity.py +7 -2
  7. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/__init__.py +2 -1
  8. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/assure_model_key.py +148 -2
  9. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/categories/builders.py +5 -1
  10. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/model_resolver.py +17 -2
  11. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/remote.py +271 -15
  12. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/chat_schemas.py +11 -0
  13. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/metadata_schemas.py +1 -0
  14. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/chat_schemas.py +7 -0
  15. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4/src/hugpy_engine.egg-info}/PKG-INFO +1 -1
  16. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine.egg-info/SOURCES.txt +6 -1
  17. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine.egg-info/scm_file_list.json +5 -0
  18. hugpy_engine-0.2.4/src/hugpy_engine.egg-info/scm_version.json +8 -0
  19. hugpy_engine-0.2.4/tests/known_good/test_call_queue_contract.py +357 -0
  20. hugpy_engine-0.2.4/tests/known_good/test_evict_fit_contract.py +198 -0
  21. hugpy_engine-0.2.4/tests/known_good/test_model_format_contract.py +193 -0
  22. hugpy_engine-0.2.4/tests/known_good/test_model_key_contract.py +204 -0
  23. hugpy_engine-0.2.4/tests/known_good/test_relay_gate_allocation_contract.py +106 -0
  24. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_central_cap_gating.py +17 -0
  25. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_chat_role_alternation.py +36 -8
  26. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_ctx_fit.py +8 -0
  27. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_hot_cache.py +27 -0
  28. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_per_request_alloc.py +5 -0
  29. hugpy_engine-0.2.3/src/hugpy_engine.egg-info/scm_version.json +0 -8
  30. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/LICENSE +0 -0
  31. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/README.md +0 -0
  32. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/docs/ALLOCATION-MODES.md +0 -0
  33. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/pyproject.toml +0 -0
  34. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/setup.cfg +0 -0
  35. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/__init__.py +0 -0
  36. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/alloc_modes.py +0 -0
  37. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/allocation.py +0 -0
  38. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/__init__.py +0 -0
  39. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/call_api.py +0 -0
  40. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/reclassify.py +0 -0
  41. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/reconcile.py +0 -0
  42. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/serve/__init__.py +0 -0
  43. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/serve/serve.py +0 -0
  44. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/serve/serve_cli.py +0 -0
  45. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/apis/systemd_units.py +0 -0
  46. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/backend.py +0 -0
  47. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/backends/__init__.py +0 -0
  48. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/backends/local.py +0 -0
  49. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/catalog.py +0 -0
  50. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/catalog_bridge.py +0 -0
  51. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/categories.py +0 -0
  52. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/chat_context/__init__.py +0 -0
  53. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/chat_context/chat_context.py +0 -0
  54. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/chat_context/context_budget.py +0 -0
  55. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/chat_context/unbounded.py +0 -0
  56. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/__init__.py +0 -0
  57. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/models/__init__.py +0 -0
  58. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/models/models_default.py +0 -0
  59. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/config/models/models_dict.py +0 -0
  60. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/contracts.py +0 -0
  61. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/dispatch/__init__.py +0 -0
  62. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/dispatch/acquire.py +0 -0
  63. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/dispatch/dispatch.py +0 -0
  64. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/draft_models.py +0 -0
  65. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/engines.py +0 -0
  66. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/eviction.py +0 -0
  67. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/generate/__init__.py +0 -0
  68. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/generate/coder.py +0 -0
  69. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/generate/config.py +0 -0
  70. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/generate/generate_runner.py +0 -0
  71. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/gguf_election.py +0 -0
  72. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/__init__.py +0 -0
  73. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/__init__.py +0 -0
  74. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/chat_runner.py +0 -0
  75. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/get.py +0 -0
  76. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/__init__.py +0 -0
  77. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/base_runner.py +0 -0
  78. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/ccp_runner.py +0 -0
  79. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/imports/__init__.py +0 -0
  80. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/imports/config.py +0 -0
  81. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/imports/constants.py +0 -0
  82. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/imports/init_imports.py +0 -0
  83. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/imports/utils.py +0 -0
  84. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/python_runner.py +0 -0
  85. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/runners/src/shard_server.py +0 -0
  86. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/llama/serve.py +0 -0
  87. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/manifest.py +0 -0
  88. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/model_classifier.py +0 -0
  89. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/model_index/__init__.py +0 -0
  90. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/model_index/client.py +0 -0
  91. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/model_index/query_registry.py +0 -0
  92. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/model_index/repositories.py +0 -0
  93. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/model_index/service.py +0 -0
  94. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/name_match.py +0 -0
  95. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/native/__init__.py +0 -0
  96. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/native/build.py +0 -0
  97. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/native/fetch.py +0 -0
  98. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/native/resolve.py +0 -0
  99. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/output_repair.py +0 -0
  100. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/peft_adapters.py +0 -0
  101. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/placement.py +0 -0
  102. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/py.typed +0 -0
  103. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/query.py +0 -0
  104. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/allocator.py +0 -0
  105. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/categories/__init__.py +0 -0
  106. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/categories/frameworks.py +0 -0
  107. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/device_placement.py +0 -0
  108. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/groups.py +0 -0
  109. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/resolvers/model_dict_resolver.py +0 -0
  110. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/routing_diagnostics.py +0 -0
  111. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/runtime.py +0 -0
  112. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/__init__.py +0 -0
  113. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/event_schemas.py +0 -0
  114. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/model_schemas.py +0 -0
  115. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/runner_schemas.py +0 -0
  116. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/schemas/task_schemas.py +0 -0
  117. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/__init__.py +0 -0
  118. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/chunksum_verify.py +0 -0
  119. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/hot_cache.py +0 -0
  120. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/load_failure.py +0 -0
  121. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/model_cache.py +0 -0
  122. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/overrides.py +0 -0
  123. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/policy.py +0 -0
  124. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/profiles.py +0 -0
  125. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/serve.py +0 -0
  126. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/serve_cli.py +0 -0
  127. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/slot_agent.py +0 -0
  128. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/slots.py +0 -0
  129. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/serve/supervisor.py +0 -0
  130. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/spill.py +0 -0
  131. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/task_deps.py +0 -0
  132. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/tasks.py +0 -0
  133. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/utils/__init__.py +0 -0
  134. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/utils/json_scavenge.py +0 -0
  135. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/utils/no_think.py +0 -0
  136. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/__init__.py +0 -0
  137. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/config_schemas.py +0 -0
  138. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/install_schemas.py +0 -0
  139. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/model_schemas.py +0 -0
  140. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/request_schemas.py +0 -0
  141. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine/wire/specs_schemas.py +0 -0
  142. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine.egg-info/dependency_links.txt +0 -0
  143. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine.egg-info/entry_points.txt +0 -0
  144. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine.egg-info/requires.txt +0 -0
  145. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/src/hugpy_engine.egg-info/top_level.txt +0 -0
  146. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/conftest.py +0 -0
  147. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_admission_gate_and_diagnostics.py +0 -0
  148. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_allocation_accuracy.py +0 -0
  149. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_budget_bar_spec.py +0 -0
  150. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_catalog_bridge.py +0 -0
  151. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_chat_request_caller.py +0 -0
  152. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_chat_tool_messages_schema.py +0 -0
  153. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_chunksum_verify.py +0 -0
  154. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_comms_foundations.py +0 -0
  155. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_device_placement.py +0 -0
  156. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_engine_boundary.py +0 -0
  157. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_engine_persisted_resolve.py +0 -0
  158. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_eviction_flow.py +0 -0
  159. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_feasible_distribution_engine.py +0 -0
  160. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_gguf_election.py +0 -0
  161. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_gguf_sharded_effective.py +0 -0
  162. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_image_task_classification.py +0 -0
  163. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_import_policy.py +0 -0
  164. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_load_failure_reason.py +0 -0
  165. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_mmproj_effective_bytes.py +0 -0
  166. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_model_aliases.py +0 -0
  167. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_no_makeroom.py +0 -0
  168. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_no_think.py +0 -0
  169. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_peft_adapter_load.py +0 -0
  170. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_per_device_slot.py +0 -0
  171. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_placement_defaults.py +0 -0
  172. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_provision_failure_honesty.py +0 -0
  173. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_registry_attribution.py +0 -0
  174. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_registry_symmetry.py +0 -0
  175. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_resolve_routing_key_reconcile.py +0 -0
  176. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_runner_registry_heartbeat_starvation.py +0 -0
  177. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_slot_load_stall_health.py +0 -0
  178. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_slot_moe_vram_preflight.py +0 -0
  179. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_slot_placement_projection.py +0 -0
  180. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_slot_port_guard.py +0 -0
  181. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_slot_relaunch.py +0 -0
  182. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_spill_ctx_reserve.py +0 -0
  183. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_spill_vision_fit.py +0 -0
  184. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_tasks_registry.py +0 -0
  185. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_tok_s_plausible_ceiling.py +0 -0
  186. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_transformers_placement.py +0 -0
  187. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_video_model_classification.py +0 -0
  188. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_vision_needs_slot.py +0 -0
  189. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_vram_cliff.py +0 -0
  190. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_worker_error_attribution.py +0 -0
  191. {hugpy_engine-0.2.3 → hugpy_engine-0.2.4}/tests/test_worker_serve_path_central_only.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hugpy-engine
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Hugpy engine: model discovery and classification, allocation and eviction, native llama.cpp and GGUF runners, and the prompt-to-reply inference path with task and placement seams
5
5
  Author-email: putkoff <support@hugpy.ai>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -133,6 +133,25 @@ def get_all_configs(verbose: bool = False, get_code: bool = False,
133
133
 
134
134
  hub_id = clean_hub_id(directory, folder)
135
135
  framework = (marker or {}).get("framework") or infer_framework(directory)
136
+ marked_context = (marker or {}).get("model_max_length")
137
+ if (isinstance(marked_context, (int, float)) and
138
+ not isinstance(marked_context, bool) and marked_context > 0):
139
+ max_model_length = int(marked_context)
140
+ elif framework == "gguf":
141
+ # Legacy registry walk also needs the trained context. The generic
142
+ # JSON scanner has no GGUF metadata and otherwise substitutes the
143
+ # global default for models whose marker predates context capture.
144
+ try:
145
+ from hugpy_engine.config.main import get_gguf_file
146
+ from hugpy_storage.gguf_inspect import gguf_metadata
147
+ gguf_path = get_gguf_file(
148
+ directory, None, prefer=(marker or {}).get("filename"))
149
+ context = (gguf_metadata(gguf_path, (".context_length",))
150
+ .get(".context_length") if gguf_path else None)
151
+ if isinstance(context, (int, float)) and not isinstance(context, bool) and context > 0:
152
+ max_model_length = int(context)
153
+ except Exception as exc: # noqa: BLE001 — best-effort metadata only
154
+ logger.debug("GGUF context unavailable for %s: %s", name, exc)
136
155
  tasks = (marker or {}).get("tasks") or tasks_data.get(name)
137
156
  primary_task = (marker or {}).get("primary_task") or (tasks[0] if tasks else None)
138
157
 
@@ -278,12 +297,36 @@ def resolve_hugpy_marker(directory: str, hub_id: str) -> dict:
278
297
  tasks = marker.get("tasks")
279
298
  if tasks is not None and not isinstance(tasks, list):
280
299
  tasks = [tasks]
281
- return {k: v for k, v in {
300
+ out = {k: v for k, v in {
282
301
  "name": marker.get("name"),
283
302
  "framework": marker.get("framework"),
284
303
  "tasks": tasks,
285
304
  "primary_task": marker.get("primary_task"),
286
305
  }.items() if v is not None}
306
+ context = marker.get("model_max_length")
307
+ if isinstance(context, (int, float)) and not isinstance(context, bool) and context > 0:
308
+ out["model_max_length"] = int(context)
309
+ return out
310
+
311
+
312
+ def resolve_gguf_context(directory: str, hub_id: str) -> dict:
313
+ """Read the selected GGUF's trained context from its header."""
314
+ try:
315
+ from hugpy_storage.hugpy_marker import read_hugpy_marker
316
+ marker = read_hugpy_marker(directory) or {}
317
+ if (marker.get("framework") or infer_framework(directory)) != "gguf":
318
+ return {}
319
+ from hugpy_engine.config.main import get_gguf_file
320
+ from hugpy_storage.gguf_inspect import gguf_metadata
321
+ gguf = get_gguf_file(directory, None, prefer=marker.get("filename"))
322
+ if not gguf:
323
+ return {}
324
+ value = gguf_metadata(gguf, (".context_length",)).get(".context_length")
325
+ if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0:
326
+ return {"model_max_length": int(value)}
327
+ except Exception as exc: # noqa: BLE001 — metadata inspection must not block discovery
328
+ logger.debug("GGUF context unavailable for %s: %s", hub_id, exc)
329
+ return {}
287
330
 
288
331
 
289
332
  # Storage families that imply a framework. "misc" implies nothing — comfy and
@@ -374,6 +417,7 @@ def build_resolver_chain(*, api: Any = None,
374
417
  chain: List[Tuple[str, ResolverFn]] = [
375
418
  ("dir_declaration", resolve_dir_declaration),
376
419
  ("hugpy_marker", resolve_hugpy_marker),
420
+ ("gguf_context", resolve_gguf_context),
377
421
  ("local_config", resolve_local_config),
378
422
  ("local_tokenizer", resolve_local_tokenizer),
379
423
  ]
@@ -441,7 +485,8 @@ def discover_model(save_json: bool = True, verbose: bool = True, use_hub: bool =
441
485
  "architectures": meta.architectures,
442
486
  "model_type": meta.model_type,
443
487
  "max_position_embeddings": meta.max_position_embeddings,
444
- "model_max_length": meta.tokenizer_model_max_length
488
+ "model_max_length": meta.model_max_length
489
+ or meta.tokenizer_model_max_length
445
490
  or meta.max_position_embeddings
446
491
  or DEFAULT_MAX_TOKENS,
447
492
  "parameter_count": meta.parameter_count,
@@ -484,6 +529,9 @@ def discover_models(save_json: bool = True, verbose: bool = True, use_hub: bool
484
529
  meta, meta_sources = enrich(directory, hub_id, chain)
485
530
 
486
531
  row = meta.to_dict()
532
+ row["model_max_length"] = (meta.model_max_length
533
+ or meta.tokenizer_model_max_length
534
+ or meta.max_position_embeddings)
487
535
  # Declared name (hugpy.json) wins; the bare basename is the fallback —
488
536
  # a marker-stamped "comfy-sd-turbo" must not degrade to "sd-turbo" and
489
537
  # collide with the staple's display name.
@@ -101,6 +101,14 @@ def get_model_path(key: str):
101
101
  return env_override
102
102
  cfg = get_model_config(key)
103
103
  path = os.path.join(MODELS_HOME,cfg.folder)
104
+ # A managed copy takes precedence. The worker's external pointer is used
105
+ # only when its Hugpy cache does not hold the model.
106
+ external = (getattr(cfg, "extra", None) or {}).get("external_location")
107
+ if external and not model_looks_downloaded(path, cfg) and (
108
+ os.path.isdir(external) or
109
+ ((getattr(cfg, "extra", None) or {}).get("ollama_model") and
110
+ os.path.isfile(external))):
111
+ return external
104
112
  # Read-through (2026-07-12 hotfix): cfg.folder is CENTRAL's layout — after
105
113
  # the store flattening it names the FLAT dir, but a worker's files may
106
114
  # still sit under a legacy task path. Without the resolver here, every
@@ -142,12 +142,18 @@ def model_meta(cfg: Any, *, vram_bytes: Optional[int] = None,
142
142
  framework = d.get("framework") or ""
143
143
  model_dir = _model_dir(d)
144
144
  dir_bytes = dir_size_bytes(model_dir) # whole dir (every file/variant)
145
+ # Host discovery may know the exact effective checkpoint size even though
146
+ # Central cannot read that worker's path (Ollama blobs and API-served
147
+ # vLLM checkpoints are common examples). Use that measured catalog value
148
+ # when no local directory is available; local GGUF resolution below still
149
+ # takes precedence when the weights are on Central.
150
+ discovered_size = d.get("effective_bytes") or d.get("size_bytes")
145
151
 
146
152
  # A GGUF repo often holds several quantizations but only ONE serves, so the
147
153
  # dir sum badly overstates "the model" — and it flowed straight into the VRAM
148
154
  # recommendation. Resolve the single effective quant (+ its mmproj) exactly as
149
155
  # the runner will, honoring the operator's gguf_file choice, and size by that.
150
- size = dir_bytes
156
+ size = dir_bytes or discovered_size
151
157
  gguf = {}
152
158
  if str(framework).lower() in ("gguf", "llama_cpp") and model_dir:
153
159
  try:
@@ -157,6 +163,8 @@ def model_meta(cfg: Any, *, vram_bytes: Optional[int] = None,
157
163
  gguf = {}
158
164
  if gguf.get("effective_bytes"):
159
165
  size = gguf["effective_bytes"]
166
+ elif discovered_size:
167
+ size = discovered_size
160
168
 
161
169
  selected = selected_err = None
162
170
  gguf_sel = str(select_gguf or "").strip()
@@ -177,7 +185,7 @@ def model_meta(cfg: Any, *, vram_bytes: Optional[int] = None,
177
185
  out = {
178
186
  "model_key": d.get("model_key"),
179
187
  "size_bytes": size, # effective quant for GGUF
180
- "dir_bytes": dir_bytes, # whole-dir footprint (all variants)
188
+ "dir_bytes": dir_bytes or (discovered_size if not model_dir else None),
181
189
  "quant": parse_quant(gguf.get("effective_gguf") or filename),
182
190
  "params_b": parse_params_b(name, hub_id, filename),
183
191
  "ctx_max": ctx_max,
@@ -16,6 +16,8 @@ refresh_registry() explicitly — e.g. on hugpy module startup.
16
16
 
17
17
  import json
18
18
  import os
19
+ import time
20
+ import hashlib
19
21
  from dataclasses import MISSING, fields
20
22
  from typing import Dict
21
23
  from abstract_essentials import get_logFile, safe_dump_to_file, safe_load_from_json
@@ -773,6 +775,7 @@ def derive_model_config_row(name, row):
773
775
  or row.get("tokenizer_model_max_length")
774
776
  or row.get("max_position_embeddings") or DEFAULT_MAX_TOKENS_LOCAL,
775
777
  "filename": row.get("filename"), "include": row.get("include"),
778
+ **({"ollama_model": row["ollama_model"]} if row.get("ollama_model") else {}),
776
779
  "port": row.get("port"), "host": row.get("host"),
777
780
  # SERVEABLE = at least one advertised task has a runner (k61). It used to
778
781
  # mean "EVERY task has one", which made a partially-servable row read as
@@ -813,6 +816,10 @@ def _absorb_disk(staple, disc):
813
816
  for k in ("dir", "folder", "filename"):
814
817
  if disc.get(k):
815
818
  staple[k] = disc[k]
819
+ for k in ("model_max_length", "tokenizer_model_max_length",
820
+ "max_position_embeddings"):
821
+ if disc.get(k) is not None:
822
+ staple[k] = disc[k]
816
823
 
817
824
  def merge_discovery_into_models(discovery, base=None):
818
825
  base = base if base is not None else MODELS
@@ -1492,6 +1499,8 @@ def get_models_dict(models_dict_path=None, dict_return=False, return_dict=False,
1492
1499
  # discovery/path (a fresh walk) forces a rebuild. During the import-time
1493
1500
  # build the globals aren't bound yet -> globals().get() is None -> build.
1494
1501
  if discovery is None and models_dict_path is None:
1502
+ if "_apply_worker_catalog" in globals():
1503
+ _apply_worker_catalog()
1495
1504
  cached = globals().get("MODEL_REGISTRY_DICT" if dict_return else "MODEL_REGISTRY")
1496
1505
  if cached:
1497
1506
  return cached
@@ -1536,6 +1545,171 @@ def get_models_dict(models_dict_path=None, dict_return=False, return_dict=False,
1536
1545
  MODEL_REGISTRY: Dict[str, ModelConfig] = get_models_dict()
1537
1546
  MODEL_REGISTRY_DICT: Dict[str, dict] = get_models_dict(dict_return=True)
1538
1547
 
1548
+ # A worker owns the path to its external weights. Central stores only the
1549
+ # catalog metadata and a per-worker path pointer; it must never resolve that
1550
+ # pointer as a path on Central or offer it as a central transfer source.
1551
+ _WORKER_CATALOG_PATH = os.path.join(os.path.dirname(MODELS_DISCOVERY_PATH),
1552
+ "worker_model_catalog.json")
1553
+ _WORKER_CATALOG_SEEN = 0.0
1554
+ _WORKER_ONLY_KEYS: set[str] = set()
1555
+ _WORKER_TOUCHED_KEYS: set[str] = set()
1556
+
1557
+
1558
+ def _central_copy_present(key: str) -> bool:
1559
+ """A registry row alone does not prove Central owns transferable bytes."""
1560
+ try:
1561
+ from hugpy_engine.config.main import get_model_path
1562
+ from hugpy_storage.model_presence import model_looks_downloaded
1563
+ cfg = MODEL_REGISTRY[key]
1564
+ return model_looks_downloaded(get_model_path(key), cfg)
1565
+ except Exception: # noqa: BLE001 — uncertain means no destructive cold reset
1566
+ return False
1567
+
1568
+
1569
+ def record_worker_models(worker_id: str, rows: dict) -> list[str]:
1570
+ """Persist one host scan, shared by all Central web processes."""
1571
+ import fcntl
1572
+ path = _WORKER_CATALOG_PATH
1573
+ os.makedirs(os.path.dirname(path), exist_ok=True)
1574
+ with open(path + ".lock", "a+") as lock:
1575
+ fcntl.flock(lock, fcntl.LOCK_EX)
1576
+ try:
1577
+ with open(path, encoding="utf-8") as fh:
1578
+ data = json.load(fh)
1579
+ except (OSError, ValueError):
1580
+ data = {}
1581
+ clean = {}
1582
+ for key, row in (rows or {}).items():
1583
+ if not isinstance(key, str) or not isinstance(row, dict):
1584
+ continue
1585
+ location = row.get("external_location")
1586
+ api_url = row.get("api_url")
1587
+ try:
1588
+ from urllib.parse import urlsplit
1589
+ parsed_api = urlsplit(api_url) if isinstance(api_url, str) else None
1590
+ is_api = bool(parsed_api and parsed_api.scheme == "http"
1591
+ and parsed_api.hostname in ("127.0.0.1", "localhost", "::1")
1592
+ and parsed_api.port and not parsed_api.username
1593
+ and not parsed_api.password and parsed_api.path in ("", "/")
1594
+ and not parsed_api.query and not parsed_api.fragment)
1595
+ except ValueError:
1596
+ is_api = False
1597
+ if not ((isinstance(location, str) and os.path.isabs(location)) or is_api):
1598
+ continue
1599
+ hub = str(row.get("hub_id") or "").strip("/").lower()
1600
+ current = MODEL_REGISTRY.get(key)
1601
+ if (not is_api and current is not None and
1602
+ str(current.hub_id or "").strip("/").lower() != hub):
1603
+ owner = str(row.get("hub_id") or "local").split("/", 1)[0]
1604
+ key = f"{owner}~{key}"
1605
+ current = MODEL_REGISTRY.get(key)
1606
+ if ((not is_api and current is not None and
1607
+ str(current.hub_id or "").strip("/").lower() != hub) or
1608
+ (key in clean and clean[key].get("worker_location") != location
1609
+ and clean[key].get("api_url") != api_url)):
1610
+ key += "~" + hashlib.sha256(location.encode()).hexdigest()[:8]
1611
+ clean[key] = {k: row.get(k) for k in (
1612
+ "name", "hub_id", "framework", "tasks", "primary_task",
1613
+ "filename", "model_max_length", "model_key", "size_bytes",
1614
+ "effective_bytes", "ollama_model", "api_url", "served_model",
1615
+ "service_kind") if row.get(k) is not None}
1616
+ clean[key]["model_key"] = key
1617
+ if isinstance(location, str) and os.path.isabs(location):
1618
+ clean[key]["worker_location"] = location
1619
+ old = (data.get(worker_id) or {}).get("models")
1620
+ changed = old != clean
1621
+ if changed:
1622
+ data[worker_id] = {"at": time.time(), "models": clean}
1623
+ tmp = path + f".{os.getpid()}.tmp"
1624
+ with open(tmp, "w", encoding="utf-8") as fh:
1625
+ json.dump(data, fh)
1626
+ os.replace(tmp, path)
1627
+ fcntl.flock(lock, fcntl.LOCK_UN)
1628
+ _apply_worker_catalog(force=changed)
1629
+ # The canonical keys may differ from worker-local scan keys when central
1630
+ # already has a same-named model from another hub. Return those identities
1631
+ # so heartbeat placement uses the same keys as the shared model catalog.
1632
+ return sorted(clean)
1633
+
1634
+
1635
+ def _apply_worker_catalog(force=False) -> None:
1636
+ """Overlay worker-only rows on the regular catalog, preserving local rows."""
1637
+ global _WORKER_CATALOG_SEEN
1638
+ registry = globals().get("MODEL_REGISTRY")
1639
+ registry_dict = globals().get("MODEL_REGISTRY_DICT")
1640
+ if registry is None or registry_dict is None:
1641
+ return # import-time base build
1642
+ try:
1643
+ stamp = os.stat(_WORKER_CATALOG_PATH).st_mtime_ns
1644
+ if not force and stamp == _WORKER_CATALOG_SEEN:
1645
+ return
1646
+ with open(_WORKER_CATALOG_PATH, encoding="utf-8") as fh:
1647
+ data = json.load(fh)
1648
+ except (OSError, ValueError):
1649
+ return
1650
+ locations: dict[str, dict[str, str]] = {}
1651
+ external_apis: dict[str, dict[str, dict]] = {}
1652
+ candidates: dict[str, dict] = {}
1653
+ for worker_id, entry in data.items():
1654
+ for key, row in (entry.get("models") or {}).items():
1655
+ if not isinstance(row, dict):
1656
+ continue
1657
+ if row.get("worker_location"):
1658
+ locations.setdefault(key, {})[worker_id] = row["worker_location"]
1659
+ if row.get("api_url"):
1660
+ external_apis.setdefault(key, {})[worker_id] = {
1661
+ field: row[field] for field in ("api_url", "served_model", "service_kind")
1662
+ if row.get(field) is not None}
1663
+ candidates.setdefault(key, row)
1664
+
1665
+ for key in list(_WORKER_ONLY_KEYS - candidates.keys()):
1666
+ registry.pop(key, None)
1667
+ registry_dict.pop(key, None)
1668
+ _WORKER_ONLY_KEYS.discard(key)
1669
+ for key in _WORKER_TOUCHED_KEYS - candidates.keys():
1670
+ if key in registry_dict:
1671
+ registry_dict[key].pop("worker_locations", None)
1672
+ registry_dict[key].pop("worker_external_apis", None)
1673
+ registry_dict[key].pop("worker_only", None)
1674
+ if key in registry:
1675
+ registry[key].extra.pop("worker_only", None)
1676
+ for key, row in candidates.items():
1677
+ if key not in registry:
1678
+ clean = {k: v for k, v in row.items() if k != "worker_location"}
1679
+ derived, _ = derive_model_config_row(key, clean)
1680
+ if not derived:
1681
+ continue
1682
+ derived["worker_only"] = True
1683
+ for size_key in ("size_bytes", "effective_bytes"):
1684
+ if row.get(size_key) is not None:
1685
+ derived[size_key] = row[size_key]
1686
+ cfg = get_assessed_model_config(derived)
1687
+ if not cfg:
1688
+ continue
1689
+ registry[key] = cfg
1690
+ registry_dict[key] = cfg.to_dict()
1691
+ for size_key in ("size_bytes", "effective_bytes"):
1692
+ if row.get(size_key) is not None:
1693
+ registry_dict[key][size_key] = row[size_key]
1694
+ _WORKER_ONLY_KEYS.add(key)
1695
+ worker_only = key in _WORKER_ONLY_KEYS or not _central_copy_present(key)
1696
+ registry_dict[key]["worker_locations"] = locations.get(key, {})
1697
+ if external_apis.get(key):
1698
+ registry_dict[key]["worker_external_apis"] = external_apis[key]
1699
+ else:
1700
+ registry_dict[key].pop("worker_external_apis", None)
1701
+ registry_dict[key]["worker_only"] = worker_only
1702
+ if worker_only:
1703
+ registry[key].extra["worker_only"] = True
1704
+ else:
1705
+ registry[key].extra.pop("worker_only", None)
1706
+ _WORKER_TOUCHED_KEYS.clear()
1707
+ _WORKER_TOUCHED_KEYS.update(candidates)
1708
+ _WORKER_CATALOG_SEEN = stamp
1709
+
1710
+
1711
+ _apply_worker_catalog(force=True)
1712
+
1539
1713
 
1540
1714
  def get_model_registry(dict_return=False, return_dict=False):
1541
1715
  dict_return = dict_return or return_dict
@@ -1569,6 +1743,8 @@ def refresh_registry(run_discovery=True):
1569
1743
  MODEL_REGISTRY_DICT.update(fresh_dict)
1570
1744
  for stale in [k for k in MODEL_REGISTRY_DICT if k not in fresh_dict]:
1571
1745
  MODEL_REGISTRY_DICT.pop(stale, None)
1746
+ _WORKER_ONLY_KEYS.clear() # fresh is the central base; recalculate ownership
1747
+ _apply_worker_catalog(force=True)
1572
1748
  try:
1573
1749
  from hugpy_engine.config.models.models_default import refresh_task_registries
1574
1750
  refresh_task_registries()
@@ -32,7 +32,10 @@ def format_prompt(messages=None, prompt=None) -> str:
32
32
  return json.dumps(messages, ensure_ascii=False, indent=2, default=str)
33
33
 
34
34
  # view state <- canonical status
35
- _VIEW_STATE = {"pending": "waiting", "processing": "waiting",
35
+ # Keep queue presentation aligned with the canonical job lifecycle. A request
36
+ # assigned to a worker but still pre-filling/loading is processing, not waiting;
37
+ # collapsing the two made a live worker request look orphaned in the console.
38
+ _VIEW_STATE = {"pending": "waiting", "processing": "processing",
36
39
  "streaming": "active"}
37
40
  _QUEUE_KINDS_EXCLUDED = {"download"}
38
41
 
@@ -105,6 +108,8 @@ def counts() -> dict:
105
108
  if d["kind"] not in _QUEUE_KINDS_EXCLUDED]
106
109
  waiting = sum(1 for d in live
107
110
  if _VIEW_STATE.get(d["status"]) == "waiting")
111
+ # Preserve the existing API shape (`active` is the non-waiting bucket),
112
+ # while snapshots distinguish `processing` from token-producing `active`.
108
113
  active = sum(1 for d in live
109
- if _VIEW_STATE.get(d["status"]) == "active")
114
+ if _VIEW_STATE.get(d["status"]) in ("processing", "active"))
110
115
  return {"waiting": waiting, "active": active, "total": waiting + active}
@@ -14,6 +14,7 @@ from hugpy_engine.resolvers.remote import (
14
14
  set_no_worker_diagnostic,
15
15
  set_no_worker_skips,
16
16
  set_placement_provider,
17
+ set_serving_provider,
17
18
  set_worker_provider,
18
19
  )
19
20
 
@@ -21,5 +22,5 @@ __all__ = [
21
22
  "EXTERNAL_TASK_RUNNERS", "Peer", "ensure_staple_weights", "external_runner_for",
22
23
  "get_worker_provider", "peer_for", "resolve", "resolve_model_key",
23
24
  "set_no_worker_diagnostic", "set_no_worker_skips", "set_placement_provider",
24
- "set_worker_provider", "validate_registry",
25
+ "set_serving_provider", "set_worker_provider", "validate_registry",
25
26
  ]
@@ -320,6 +320,102 @@ def _quant_rank(key: str) -> int:
320
320
  return max((_QUANT_BITS[t] for t in toks if t in _QUANT_BITS), default=0)
321
321
 
322
322
 
323
+ # Packaging / quant / framework markers — a name carrying any of these named a
324
+ # representation, not merely a base-name family. Single source for both the
325
+ # serving-override gate (_package_specific) and the logical-identity strip
326
+ # (_logical_id) so the two never drift.
327
+ _FORMAT_TOKENS = frozenset({
328
+ "gguf", "transformers", "transformer", "safetensors", "pytorch",
329
+ "awq", "gptq", "exl2", "exl3", "mlx", "bnb", "4bit", "8bit",
330
+ "fp8", "fp16", "bf16", "f16", "q2", "q3", "q4", "q5", "q6", "q8",
331
+ })
332
+
333
+ # The GGUF engine family (mirrors alloc_modes.GGUF_ENGINES; inlined to keep this
334
+ # resolver import-cheap and cycle-free).
335
+ _GGUF_ENGINES = frozenset({"gguf", "llama_cpp"})
336
+
337
+
338
+ def _package_specific(model_key: str) -> bool:
339
+ """Whether the caller explicitly selected a model packaging/framework.
340
+
341
+ A bare family request (``Qwen3-Coder-Next``) must follow the live serving
342
+ representation, whether that is GGUF or Transformers. ``GGUF`` and the
343
+ common Transformers/package markers are deliberate specificity and must
344
+ remain exact rather than being rewritten to whichever sibling is hot.
345
+ """
346
+ return bool(set(_tokens(model_key)) & _FORMAT_TOKENS)
347
+
348
+
349
+ def _framework_of(key: str):
350
+ """The registry framework for ``key`` (scalar), or None when absent."""
351
+ cfg = MODEL_REGISTRY.get(key)
352
+ fw = getattr(cfg, "framework", None) if cfg is not None else None
353
+ if isinstance(fw, (list, tuple)):
354
+ fw = fw[0] if fw else None
355
+ return fw
356
+
357
+
358
+ def _representation_of(key: str):
359
+ """The gguf-vs-transformers REPRESENTATION of a registry key, read from its
360
+ framework (never a name substring). GGUF family -> "gguf"; any other
361
+ concrete framework -> "transformers"; None when the key is not in the
362
+ registry."""
363
+ fw = _framework_of(key)
364
+ if fw is None:
365
+ return None
366
+ return "gguf" if str(fw).strip().lower() in _GGUF_ENGINES else "transformers"
367
+
368
+
369
+ def _logical_id(key: str) -> str:
370
+ """Base-name identity of a key with packaging/quant tokens dropped, so the
371
+ GGUF and Transformers rows of one model share it (``…-Coder-Next-GGUF`` and
372
+ ``…-Coder-Next`` -> ``coder next``). Used ONLY to pair representation
373
+ siblings under an explicit model_format."""
374
+ return " ".join(t for t in _tokens(_bare_tail(key)) if t not in _FORMAT_TOKENS)
375
+
376
+
377
+ def _owner_of(key: str) -> str:
378
+ """The publisher qualifier of ``key`` (``Owner~Repo`` / ``owner/repo``), or
379
+ its hub_id owner, or "" — the same owner rule _match_tier enforces, so a
380
+ representation redirect never crosses publishers."""
381
+ s = str(key)
382
+ if "~" in s:
383
+ return _slugify(s.split("~", 1)[0])
384
+ hub_id = getattr(MODEL_REGISTRY.get(key), "hub_id", None)
385
+ if hub_id and "/" in str(hub_id):
386
+ return _slugify(str(hub_id).split("/", 1)[0])
387
+ if "/" in s:
388
+ return _slugify(s.split("/", 1)[0])
389
+ return ""
390
+
391
+
392
+ def _redirect_to_format(resolved: str, model_key: str, fmt: str, *, strict: bool):
393
+ """Return the key of the requested REPRESENTATION for the model ``resolved``
394
+ names. When ``resolved`` is already that representation it is returned
395
+ unchanged; otherwise the same-logical-identity, same-owner sibling of
396
+ representation ``fmt`` is chosen (deterministically). When the registry has
397
+ no such representation the resolved key is kept and a warning logged — a
398
+ resolver cannot invent a row that is not on record."""
399
+ if not resolved or _representation_of(resolved) == fmt:
400
+ return resolved
401
+ want_logical = _logical_id(resolved)
402
+ want_owner = _owner_of(resolved)
403
+ siblings = [
404
+ k for k in MODEL_REGISTRY
405
+ if _representation_of(k) == fmt
406
+ and _logical_id(k) == want_logical
407
+ and (not want_owner or _owner_of(k) == want_owner)
408
+ ]
409
+ if not siblings:
410
+ _log.warning("assure_model_key: model_format=%r requested for %r but no "
411
+ "%s representation of %r is on record; kept %s",
412
+ fmt, model_key, fmt, resolved, resolved)
413
+ return resolved
414
+ if len(siblings) == 1:
415
+ return siblings[0]
416
+ return _pick_by_total_order(siblings, strict=strict) or siblings[0]
417
+
418
+
323
419
  def _match_tier(key, values, slug, bare_slug, model_key) -> int:
324
420
  """The strongest identity tier at which ``key`` matches the request, or 0."""
325
421
  if _slugify(key) == slug:
@@ -330,7 +426,26 @@ def _match_tier(key, values, slug, bare_slug, model_key) -> int:
330
426
  folder = getattr(values, "folder", None)
331
427
  if folder and _path_suffix_matches(folder, model_key):
332
428
  return _TIER_FOLDER
429
+ # A qualified request names a publisher, not merely a base-name family.
430
+ # Never let the bare-tail tier silently turn an explicit
431
+ # ``unsloth~Qwen3-Coder-Next-GGUF`` request into the Qwen variant (or vice
432
+ # versa) when one qualified registry row is absent. The qualified owner
433
+ # may be represented by the registry key or by the model's Hub ID while
434
+ # the registry key itself remains bare.
333
435
  if _slugify(_bare_tail(key)) == bare_slug:
436
+ requested_owner = ""
437
+ if "~" in str(model_key):
438
+ requested_owner = str(model_key).split("~", 1)[0].strip()
439
+ elif "/" in str(model_key):
440
+ requested_owner = str(model_key).split("/", 1)[0].strip()
441
+ if requested_owner:
442
+ candidate_owner = ""
443
+ if "~" in str(key):
444
+ candidate_owner = str(key).split("~", 1)[0].strip()
445
+ elif hub_id and "/" in str(hub_id):
446
+ candidate_owner = str(hub_id).split("/", 1)[0].strip()
447
+ if _slugify(candidate_owner) != _slugify(requested_owner):
448
+ return 0
334
449
  return _TIER_BARE
335
450
  return 0
336
451
 
@@ -351,7 +466,29 @@ def _pick_by_total_order(keys, *, strict):
351
466
  return ranked[0]
352
467
 
353
468
 
354
- def assure_model_key(model_key, *, strict: bool = False, _expand_aliases: bool = True):
469
+ def assure_model_key(model_key, *, strict: bool = False, fmt: str = "auto",
470
+ _expand_aliases: bool = True):
471
+ """Resolve a model reference to a canonical registry key.
472
+
473
+ ``fmt`` (model_format: "auto" | "gguf" | "transformers") is the EXPLICIT
474
+ representation override (operator 2026-09-29). "auto" (default) is
475
+ byte-identical to legacy resolution. "gguf"/"transformers" is authoritative:
476
+ the base resolution runs first, then :func:`_redirect_to_format` swaps in the
477
+ same-model, same-owner sibling of the requested representation (ignoring what
478
+ is currently serving and any contradicting name suffix). The GGUF-vs-
479
+ Transformers special-casing downstream keys off the RESOLVED key's framework,
480
+ so nothing beyond the returned key needs to know about ``fmt``.
481
+ """
482
+ resolved = _assure_model_key_base(model_key, strict=strict,
483
+ _expand_aliases=_expand_aliases)
484
+ if resolved and fmt and str(fmt).strip().lower() not in ("", "auto"):
485
+ return _redirect_to_format(resolved, model_key, str(fmt).strip().lower(),
486
+ strict=strict)
487
+ return resolved
488
+
489
+
490
+ def _assure_model_key_base(model_key, *, strict: bool = False,
491
+ _expand_aliases: bool = True):
355
492
  """
356
493
  Resolve a user-provided model key, repo id, manifest slug, folder name,
357
494
  or folder suffix into the canonical key from MODEL_REGISTRY.
@@ -387,7 +524,7 @@ def assure_model_key(model_key, *, strict: bool = False, _expand_aliases: bool =
387
524
  if _expand_aliases:
388
525
  target = _alias_target(model_key)
389
526
  if target and target != model_key:
390
- resolved = assure_model_key(target, strict=strict, _expand_aliases=False)
527
+ resolved = _assure_model_key_base(target, strict=strict, _expand_aliases=False)
391
528
  if resolved is not None:
392
529
  _log.debug("assure_model_key: alias %r -> %r -> %s",
393
530
  model_key, target, resolved)
@@ -410,6 +547,15 @@ def assure_model_key(model_key, *, strict: bool = False, _expand_aliases: bool =
410
547
  top = [k for k, t in tiers.items() if t == top_tier]
411
548
  if len(top) == 1:
412
549
  return top[0]
550
+ # Same-family variants (notably GGUF vs Transformers) are not a real
551
+ # ambiguity when the caller did not name a packaging. Let the normal
552
+ # serving-aware pipeline choose the live representation first. An
553
+ # explicitly qualified request still takes the deterministic exact
554
+ # path below and cannot be redirected across frameworks.
555
+ if not _package_specific(model_key):
556
+ chosen = _pipeline_pick(model_key, top)
557
+ if chosen is not None:
558
+ return chosen
413
559
  winner = _pick_by_total_order(top, strict=strict)
414
560
  if winner is not None:
415
561
  _log.debug("assure_model_key: broke tier-%d tie for %r -> %s (of %s)",
@@ -79,9 +79,13 @@ def _build_chat_request(kwargs: Dict[str, Any], model_key: str) -> ChatRequest:
79
79
  # the keys above; without forwarding it here the /v1 route's derived client
80
80
  # identity never reaches ChatRequest.caller, so every call records as "api".
81
81
  # It is central-only (dropped from model_dump before the worker wire).
82
+ # model_format: central-only representation pin (operator 2026-09-29) — rides
83
+ # ChatRequest so the serving fast-route matches by representation; dropped
84
+ # from the worker wire by ChatRequest's serializer, same as ``caller``.
82
85
  for k in ("max_new_tokens", "temperature", "top_p", "do_sample", "request_id",
83
86
  "unbounded", "max_chunks", "pool", "images", "alloc",
84
- "chat_template_kwargs", "logit_bias", "no_makeroom", "caller"):
87
+ "chat_template_kwargs", "logit_bias", "no_makeroom", "caller",
88
+ "model_format"):
85
89
  if k in kwargs:
86
90
  out[k] = kwargs[k]
87
91
  out.setdefault("request_id", make_request_id())
@@ -180,12 +180,16 @@ def _adopt_vl_tasks_from_disk(model_key, cfg, task):
180
180
  return None
181
181
 
182
182
 
183
+ _MODEL_FORMATS = ("auto", "gguf", "transformers")
184
+
185
+
183
186
  def resolve_model_key(
184
187
  *,
185
188
  model_key: Optional[str] = None,
186
189
  file: Optional[str] = None,
187
190
  media_type: Optional[str] = None,
188
191
  task: Optional[str] = None,
192
+ model_format: Optional[str] = None,
189
193
  ) -> str:
190
194
  """Pick a model_key via explicit resolution chain.
191
195
 
@@ -194,15 +198,25 @@ def resolve_model_key(
194
198
 
195
199
  `task`, when given alongside `model_key`, is validated against
196
200
  cfg.tasks. When given alone, it picks TASK_DEFAULTS[task].
201
+
202
+ `model_format` ("auto" | "gguf" | "transformers", default auto) is the
203
+ explicit representation override — authoritative when choosing between the
204
+ GGUF and Transformers rows of the same model (operator 2026-09-29).
197
205
  """
198
206
  if task is not None and task not in KNOWN_TASKS_REGISTRY:
199
207
  raise KeyError(
200
208
  f"Unknown task={task!r}; known: {sorted(KNOWN_TASKS_REGISTRY)}"
201
209
  )
202
-
210
+ fmt = (model_format or "auto").strip().lower() if model_format else "auto"
211
+ if fmt not in _MODEL_FORMATS:
212
+ raise ValueError(
213
+ f"Unknown model_format={model_format!r}; expected one of "
214
+ f"{list(_MODEL_FORMATS)}"
215
+ )
216
+
203
217
  if model_key is not None:
204
218
  requested = model_key
205
- model_key = assure_model_key(model_key)
219
+ model_key = assure_model_key(model_key, fmt=fmt)
206
220
  if not model_key:
207
221
  # Fail with NEAR MATCHES, not the whole registry. Two cases produce
208
222
  # a None here: nothing matched, or a fuzzy tie the pipeline could
@@ -349,6 +363,7 @@ def resolve(prompt_kwargs: Dict[str, Any]) -> Resolution:
349
363
  file=prompt_kwargs.get("file"),
350
364
  media_type=prompt_kwargs.get("media_type"),
351
365
  task=requested_task,
366
+ model_format=prompt_kwargs.get("model_format"),
352
367
  )
353
368
 
354
369
  cfg = MODEL_REGISTRY[model_key]