hugpy-engine 0.3.0__tar.gz → 0.3.2__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.
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/PKG-INFO +1 -1
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/pyproject.toml +1 -1
- hugpy_engine-0.3.2/src/hugpy_engine/__init__.py +49 -0
- hugpy_engine-0.3.2/src/hugpy_engine/categories.py +132 -0
- hugpy_engine-0.3.2/src/hugpy_engine/config/main.py +84 -0
- hugpy_engine-0.3.2/src/hugpy_engine/config/models/models_config.py +190 -0
- hugpy_engine-0.3.2/src/hugpy_engine/config/models/models_default.py +150 -0
- hugpy_engine-0.3.2/src/hugpy_engine/dispatch/__init__.py +36 -0
- hugpy_engine-0.3.2/src/hugpy_engine/dispatch/dispatch.py +1237 -0
- hugpy_engine-0.3.2/src/hugpy_engine/fit/__init__.py +0 -0
- hugpy_engine-0.3.2/src/hugpy_engine/fit/types.py +458 -0
- hugpy_engine-0.3.2/src/hugpy_engine/llama/__init__.py +0 -0
- hugpy_engine-0.3.2/src/hugpy_engine/llama/runners/__init__.py +0 -0
- hugpy_engine-0.3.2/src/hugpy_engine/llama/runners/fitevict_runner.py +197 -0
- hugpy_engine-0.3.2/src/hugpy_engine/name_match.py +287 -0
- hugpy_engine-0.3.2/src/hugpy_engine/placement.py +471 -0
- hugpy_engine-0.3.2/src/hugpy_engine/py.typed +0 -0
- hugpy_engine-0.3.2/src/hugpy_engine/resolvers/__init__.py +22 -0
- hugpy_engine-0.3.2/src/hugpy_engine/resolvers/assure_model_key.py +618 -0
- hugpy_engine-0.3.2/src/hugpy_engine/resolvers/categories/__init__.py +22 -0
- hugpy_engine-0.3.2/src/hugpy_engine/resolvers/categories/builders.py +173 -0
- hugpy_engine-0.3.2/src/hugpy_engine/resolvers/categories/frameworks.py +75 -0
- hugpy_engine-0.3.2/src/hugpy_engine/resolvers/model_resolver.py +525 -0
- hugpy_engine-0.3.2/src/hugpy_engine/schemas/__init__.py +0 -0
- hugpy_engine-0.3.2/src/hugpy_engine/schemas/metadata_schemas.py +63 -0
- hugpy_engine-0.3.2/src/hugpy_engine/schemas/model_schemas.py +101 -0
- hugpy_engine-0.3.2/src/hugpy_engine/schemas/runner_schemas.py +33 -0
- hugpy_engine-0.3.2/src/hugpy_engine/task_deps.py +67 -0
- hugpy_engine-0.3.2/src/hugpy_engine/tasks.py +349 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine.egg-info/PKG-INFO +1 -1
- hugpy_engine-0.3.2/src/hugpy_engine.egg-info/SOURCES.txt +39 -0
- hugpy_engine-0.3.0/src/hugpy_engine/__init__.py +0 -41
- hugpy_engine-0.3.0/src/hugpy_engine.egg-info/SOURCES.txt +0 -13
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/README.md +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/setup.cfg +0 -0
- {hugpy_engine-0.3.0/src/hugpy_engine/schemas → hugpy_engine-0.3.2/src/hugpy_engine/config}/__init__.py +0 -0
- /hugpy_engine-0.3.0/src/hugpy_engine/py.typed → /hugpy_engine-0.3.2/src/hugpy_engine/config/models/__init__.py +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine/schemas/chat_schemas.py +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine/schemas/event_schemas.py +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine/schemas/task_schemas.py +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine.egg-info/dependency_links.txt +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine.egg-info/requires.txt +0 -0
- {hugpy_engine-0.3.0 → hugpy_engine-0.3.2}/src/hugpy_engine.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hugpy-engine
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hugpy-engine"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "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"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Hugpy inference engine — the one-call prompt API, revived.
|
|
2
|
+
|
|
3
|
+
``execute_prompt(*args, **kwargs)`` returns the output you expected for a task:
|
|
4
|
+
positional args are inferred by type (str -> a model id or a prompt, int ->
|
|
5
|
+
max_new_tokens, float -> temperature then top_p, bool -> do_sample, list ->
|
|
6
|
+
messages), the request is resolved to a model + task, and the call is handed to
|
|
7
|
+
the box's fitevict front door through hugpy_core's relay (which picks the
|
|
8
|
+
worker and owns evict-to-fit, load, serving and the call log).
|
|
9
|
+
|
|
10
|
+
Chat/vision run today; a task whose runner has no front-door endpoint yet
|
|
11
|
+
resolves to a clean, named error (fail-as-data).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from importlib.metadata import version as _package_version
|
|
17
|
+
|
|
18
|
+
__version__ = _package_version("hugpy-engine")
|
|
19
|
+
except Exception: # pragma: no cover - source tree without metadata
|
|
20
|
+
__version__ = "0.0.0+unknown"
|
|
21
|
+
|
|
22
|
+
from hugpy_engine.dispatch.dispatch import (
|
|
23
|
+
execute_chat_stream,
|
|
24
|
+
execute_prompt,
|
|
25
|
+
execute_prompt_stream,
|
|
26
|
+
infer_arg_name,
|
|
27
|
+
normalize_prompt_kwargs,
|
|
28
|
+
runner_for,
|
|
29
|
+
)
|
|
30
|
+
from hugpy_engine.resolvers import resolve, resolve_model_key
|
|
31
|
+
from hugpy_engine.config.models.models_config import (
|
|
32
|
+
MODEL_REGISTRY,
|
|
33
|
+
get_model_registry,
|
|
34
|
+
refresh_registry,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"MODEL_REGISTRY",
|
|
39
|
+
"execute_chat_stream",
|
|
40
|
+
"execute_prompt",
|
|
41
|
+
"execute_prompt_stream",
|
|
42
|
+
"get_model_registry",
|
|
43
|
+
"infer_arg_name",
|
|
44
|
+
"normalize_prompt_kwargs",
|
|
45
|
+
"refresh_registry",
|
|
46
|
+
"resolve",
|
|
47
|
+
"resolve_model_key",
|
|
48
|
+
"runner_for",
|
|
49
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Model-catalog domain constants: task vocabulary, servable pairs, defaults.
|
|
2
|
+
|
|
3
|
+
The ``DEFAULT_*_MODEL`` values here are the env-configured defaults from
|
|
4
|
+
``hugpy_platform.constants``; ``config.models.models_default`` reconciles them
|
|
5
|
+
against the live registry after discovery and patches ``TASK_DEFAULTS`` /
|
|
6
|
+
``MEDIA_DEFAULTS`` in place.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from hugpy_platform.constants import (
|
|
10
|
+
DEFAULT_CHAT_MODEL,
|
|
11
|
+
DEFAULT_DEPTH_MODEL,
|
|
12
|
+
DEFAULT_EMBED_MODEL,
|
|
13
|
+
DEFAULT_VISION_MODEL,
|
|
14
|
+
DEFAULT_WHISPER_MODEL,
|
|
15
|
+
DEFAULT_DETECT_MODEL,
|
|
16
|
+
DEFAULT_IMAGEGEN_MODEL,
|
|
17
|
+
DEFAULT_IMG_CLASSIFY_MODEL,
|
|
18
|
+
DEFAULT_KEYWORDS_MODEL,
|
|
19
|
+
DEFAULT_SEGMENT_MODEL,
|
|
20
|
+
DEFAULT_SUMMARIZE_MODEL,
|
|
21
|
+
)
|
|
22
|
+
# pipeline_tag -> task vocabulary. Every value is HF-derivable.
|
|
23
|
+
HF_TASK_TO_TASKS = {
|
|
24
|
+
"text-generation": ["text-generation"],
|
|
25
|
+
"image-text-to-text": ["image-text-to-text", "text-generation"],
|
|
26
|
+
"automatic-speech-recognition": ["automatic-speech-recognition"],
|
|
27
|
+
# SPEECH OUT (chatterbox seat). Its absence here is why a row whose card
|
|
28
|
+
# says ``pipeline_tag: text-to-speech`` fell through to _base_tasks'
|
|
29
|
+
# conservative floor and advertised ["text-generation"] — a voice-cloning
|
|
30
|
+
# model offering itself as a chat model. The pair
|
|
31
|
+
# ("transformers","text-to-speech") is in RUNNER_PAIRS below, so the task is
|
|
32
|
+
# servable, not merely visible.
|
|
33
|
+
"text-to-speech": ["text-to-speech"],
|
|
34
|
+
"text-summarization": ["text-summarization"],
|
|
35
|
+
"text2text-generation": ["text-summarization", "text2text-generation"],
|
|
36
|
+
# KeyBERT rides any sentence-transformers model, so embedding models
|
|
37
|
+
# also serve keyword-extraction.
|
|
38
|
+
"feature-extraction": ["feature-extraction", "sentence-similarity", "keyword-extraction"],
|
|
39
|
+
"sentence-similarity": ["feature-extraction", "sentence-similarity", "keyword-extraction"],
|
|
40
|
+
"text-to-image": ["text-to-image"],
|
|
41
|
+
# VIDEO. A full video pipeline (Wan/LTX/Cog/Hunyuan/Mochi/…) is SERVEABLE:
|
|
42
|
+
# the runners live in hugpy_video's studio registry, and the
|
|
43
|
+
# ("transformers","text-to-video")/("transformers","image-to-video")
|
|
44
|
+
# RUNNER_PAIRS below make derive_model_config_row flag it serveable so it is
|
|
45
|
+
# graded like any other model (no-grader until a video suite exists) instead
|
|
46
|
+
# of the blanket "unservable/pipeline component" it read as before
|
|
47
|
+
# (2026-09-24). Before 2026-07-27 a Wan T2V model fell through _base_tasks'
|
|
48
|
+
# "conservative floor" and advertised ["text-generation"]: a video diffusion
|
|
49
|
+
# model offering itself as a chat model. A single-file GGUF video transformer
|
|
50
|
+
# is NOT a full pipeline — it stays a pipeline-component
|
|
51
|
+
# (models_config._correct_pipeline_component).
|
|
52
|
+
"text-to-video": ["text-to-video"],
|
|
53
|
+
"image-to-video": ["image-to-video"],
|
|
54
|
+
# NOTE: "image-to-image" is intentionally NOT a DISCOVERY pipeline_tag key.
|
|
55
|
+
# A raw HF pipeline_tag=image-to-image (native EDIT models like
|
|
56
|
+
# Qwen-Image-Edit) is still not auto-advertised from that tag alone. Instead,
|
|
57
|
+
# img2img capability is DERIVED at the config layer
|
|
58
|
+
# (models_config._derive_tasks): any image-GENERATION checkpoint — an
|
|
59
|
+
# SD/SDXL/flux-class diffusers model or a comfy SD-lineage checkpoint — serves
|
|
60
|
+
# image-to-image from the SAME weights it serves text-to-image with
|
|
61
|
+
# (AutoPipelineForImage2Image / the comfy image-conditioned graph), so it
|
|
62
|
+
# gains "image-to-image" whenever it advertises "text-to-image". (Operator
|
|
63
|
+
# ruling 2026-07-05: primary_task / pipeline_tag is NOT a definitive capability
|
|
64
|
+
# marker — plenty of models do img2img without saying so.) The
|
|
65
|
+
# ("transformers","image-to-image") / ("comfy","image-to-image") RUNNER_PAIRS
|
|
66
|
+
# entries below back that derivation.
|
|
67
|
+
# Vision-analysis family — HF pipeline tags map 1:1.
|
|
68
|
+
"depth-estimation": ["depth-estimation"],
|
|
69
|
+
"object-detection": ["object-detection"],
|
|
70
|
+
"image-classification": ["image-classification"],
|
|
71
|
+
"image-segmentation": ["image-segmentation"],
|
|
72
|
+
}
|
|
73
|
+
RUNNER_PAIRS = {
|
|
74
|
+
("transformers", "text-generation"), ("gguf", "text-generation"),
|
|
75
|
+
("transformers", "image-text-to-text"), ("gguf", "image-text-to-text"),
|
|
76
|
+
("transformers", "automatic-speech-recognition"),
|
|
77
|
+
("transformers", "text-to-speech"),
|
|
78
|
+
("transformers", "text-summarization"), ("transformers", "text2text-generation"),
|
|
79
|
+
("transformers", "feature-extraction"), ("transformers", "sentence-similarity"),
|
|
80
|
+
("transformers", "text-to-image"), ("transformers", "image-to-image"),
|
|
81
|
+
# VIDEO (2026-09-24). Full diffusers video pipelines served by hugpy_video's
|
|
82
|
+
# studio runner registry (RunnerSpec for WAN/LTX/Hunyuan/Cog/Mochi/OpenSora/
|
|
83
|
+
# SkyReels, T2V + I2V + VACE). Declaring the pairs here is what flips a full
|
|
84
|
+
# video pipeline from serveable=False ("adapter / pipeline component") to a
|
|
85
|
+
# normal graded row. Where hugpy-video is not wired on a box,
|
|
86
|
+
# model_resolver._plugin_missing reports it as an uninstalled runner plugin
|
|
87
|
+
# and resolve() refuses at the point of use — it never fails import, and
|
|
88
|
+
# neither task is in TASK_DEFAULTS so no chat/media route can pick a video
|
|
89
|
+
# model.
|
|
90
|
+
("transformers", "text-to-video"), ("transformers", "image-to-video"),
|
|
91
|
+
# ComfyUI engine rows (slice B) — the checkpoint lives in the WORKER's own
|
|
92
|
+
# ComfyUI install; hugpy holds no files for these.
|
|
93
|
+
("comfy", "text-to-image"), ("comfy", "image-to-image"),
|
|
94
|
+
("transformers", "keyword-extraction"),
|
|
95
|
+
("transformers", "depth-estimation"), ("transformers", "object-detection"),
|
|
96
|
+
("transformers", "image-classification"), ("transformers", "image-segmentation"),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Frameworks whose ("<framework>","image-to-image") runner+builder pair is wired
|
|
100
|
+
# above. DERIVED from RUNNER_PAIRS so it can't drift (same discipline as
|
|
101
|
+
# frameworks.KNOWN_TASKS_REGISTRY). A generative-image checkpoint on one of these
|
|
102
|
+
# frameworks serves img2img from the SAME weights it serves text-to-image with,
|
|
103
|
+
# so models_config._derive_tasks advertises "image-to-image" for it whenever it
|
|
104
|
+
# advertises "text-to-image". Today this is {"transformers", "comfy"}.
|
|
105
|
+
IMG2IMG_CAPABLE_FRAMEWORKS: frozenset = frozenset(
|
|
106
|
+
framework for (framework, task) in RUNNER_PAIRS if task == "image-to-image"
|
|
107
|
+
)
|
|
108
|
+
MEDIA_DEFAULTS: Dict[str, str] = {
|
|
109
|
+
"document": DEFAULT_CHAT_MODEL,
|
|
110
|
+
"code": DEFAULT_CHAT_MODEL,
|
|
111
|
+
"text": DEFAULT_CHAT_MODEL,
|
|
112
|
+
"image": DEFAULT_VISION_MODEL,
|
|
113
|
+
"audio": DEFAULT_WHISPER_MODEL,
|
|
114
|
+
"video": DEFAULT_WHISPER_MODEL,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
TASK_DEFAULTS: Dict[str, str] = {
|
|
118
|
+
"text-generation": DEFAULT_CHAT_MODEL,
|
|
119
|
+
"image-text-to-text": DEFAULT_VISION_MODEL,
|
|
120
|
+
"automatic-speech-recognition": DEFAULT_WHISPER_MODEL,
|
|
121
|
+
"text-summarization": DEFAULT_SUMMARIZE_MODEL,
|
|
122
|
+
"text2text-generation": DEFAULT_SUMMARIZE_MODEL,
|
|
123
|
+
"feature-extraction": DEFAULT_EMBED_MODEL,
|
|
124
|
+
"sentence-similarity": DEFAULT_EMBED_MODEL,
|
|
125
|
+
"text-to-image": DEFAULT_IMAGEGEN_MODEL,
|
|
126
|
+
"image-to-image": DEFAULT_IMAGEGEN_MODEL,
|
|
127
|
+
"keyword-extraction": DEFAULT_KEYWORDS_MODEL,
|
|
128
|
+
"depth-estimation": DEFAULT_DEPTH_MODEL,
|
|
129
|
+
"object-detection": DEFAULT_DETECT_MODEL,
|
|
130
|
+
"image-classification": DEFAULT_IMG_CLASSIFY_MODEL,
|
|
131
|
+
"image-segmentation": DEFAULT_SEGMENT_MODEL,
|
|
132
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Registry lookups over the live MODEL_REGISTRY (hugpy_next cut 2, lean).
|
|
2
|
+
|
|
3
|
+
The heavy disk/path resolution of the legacy config.main is DROPPED — hugpy
|
|
4
|
+
hosts all weights and the box's fitevict front door owns every load, so the
|
|
5
|
+
engine never resolves a model directory. These helpers answer "what are the
|
|
6
|
+
facts of model X" from the registry (sourced from hugpy_core's catalog).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from hugpy_engine.config.models.models_config import (
|
|
13
|
+
MODEL_REGISTRY,
|
|
14
|
+
get_model_registry,
|
|
15
|
+
refresh_registry,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def list_models():
|
|
20
|
+
refresh_registry()
|
|
21
|
+
return list(MODEL_REGISTRY.keys())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_model_key(model_key, registry, prefer=None):
|
|
25
|
+
"""Map a possibly-bare model_key to a concrete registry key (exact, then a
|
|
26
|
+
unique ``owner~name`` tail, then the ``-GGUF`` format equivalent)."""
|
|
27
|
+
if model_key in registry:
|
|
28
|
+
return model_key
|
|
29
|
+
|
|
30
|
+
def _bare(k):
|
|
31
|
+
return k.split("~", 1)[1] if "~" in k else k
|
|
32
|
+
|
|
33
|
+
candidates = sorted(k for k in registry if _bare(k) == model_key)
|
|
34
|
+
if not candidates:
|
|
35
|
+
try:
|
|
36
|
+
from hugpy_engine.fit.types import key_equivalent
|
|
37
|
+
candidates = sorted(k for k in registry if key_equivalent(_bare(k), model_key))
|
|
38
|
+
except Exception: # noqa: BLE001
|
|
39
|
+
candidates = []
|
|
40
|
+
if not candidates:
|
|
41
|
+
return None
|
|
42
|
+
if len(candidates) == 1:
|
|
43
|
+
return candidates[0]
|
|
44
|
+
for p in (prefer or []):
|
|
45
|
+
if p in candidates:
|
|
46
|
+
return p
|
|
47
|
+
for c in candidates:
|
|
48
|
+
if _bare(c) == _bare(p):
|
|
49
|
+
return c
|
|
50
|
+
return candidates[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_model_config(model_key: str = None, dict_return=False, return_dict=False,
|
|
54
|
+
key: str = None, prefer=None):
|
|
55
|
+
model_key = model_key or key
|
|
56
|
+
refresh_registry()
|
|
57
|
+
registry = get_model_registry(dict_return=dict_return, return_dict=return_dict)
|
|
58
|
+
resolved = _resolve_model_key(model_key, MODEL_REGISTRY, prefer=prefer)
|
|
59
|
+
if resolved is None:
|
|
60
|
+
raise KeyError(f"Unknown model: {model_key}")
|
|
61
|
+
return registry[resolved]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_model_path(key: str) -> Optional[str]:
|
|
65
|
+
"""The model's folder as recorded on its registry row. Not load-bearing (the
|
|
66
|
+
front door routes by model_key); best-effort for callers that still ask."""
|
|
67
|
+
cfg = get_model_config(key)
|
|
68
|
+
return getattr(cfg, "folder", None)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def list_model_options():
|
|
72
|
+
refresh_registry()
|
|
73
|
+
return {
|
|
74
|
+
key: {
|
|
75
|
+
"name": cfg.name,
|
|
76
|
+
"hub_id": cfg.hub_id,
|
|
77
|
+
"folder": cfg.folder,
|
|
78
|
+
"tasks": cfg.tasks,
|
|
79
|
+
"framework": cfg.framework,
|
|
80
|
+
"filename": cfg.filename,
|
|
81
|
+
"port": cfg.port,
|
|
82
|
+
}
|
|
83
|
+
for key, cfg in MODEL_REGISTRY.items()
|
|
84
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""models_config.py — the registry SOURCE (hugpy_next cut 2 seam).
|
|
2
|
+
|
|
3
|
+
Operator ruling "hugpy.json is the model init": per-model facts (framework,
|
|
4
|
+
tasks, primary_task, hub_id, name) are stamped in each model's hugpy.json. The
|
|
5
|
+
worker stamps them, derives the model KEY per box (``owner~repo`` on an owner
|
|
6
|
+
collision, else bare ``repo`` — fitevict/adapters/llama_cpp.discover_models),
|
|
7
|
+
and relays both the key and the parsed hugpy.json to hugpy_core, which exposes
|
|
8
|
+
them at ``GET {core}/models`` (catalog_rows). That endpoint is the ONE place
|
|
9
|
+
that carries BOTH the core id (the id ``/v1/models`` serves and ``pick()``
|
|
10
|
+
routes on) AND each model's hugpy.json facts — the core id is not derivable from
|
|
11
|
+
a single hugpy.json on disk (the collision qualifier is per-box state), so the
|
|
12
|
+
registry is keyed from core, never re-derived from disk.
|
|
13
|
+
|
|
14
|
+
So: MODEL_REGISTRY is built by reading hugpy_core's catalog, keyed by the core
|
|
15
|
+
id, mapping each row's hugpy.json facts through ``derive_model_config_row`` into
|
|
16
|
+
``ModelConfig``. Read lazily, TTL-cached, fail-open: core unreachable / no
|
|
17
|
+
workers online -> the registry keeps whatever it last had (empty at worst), and
|
|
18
|
+
resolution then refuses cleanly ("no online worker has it" / "no chat default").
|
|
19
|
+
|
|
20
|
+
There is NO curated ``MODELS`` fleet and NO disk discovery/classification here
|
|
21
|
+
(operator: no hardcoded model names; every model already carries its task).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
import urllib.request
|
|
31
|
+
from typing import Dict, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
from hugpy_engine.schemas.model_schemas import ModelConfig
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# No curated staples (operator: no hardcoded model names as defaults). Kept as an
|
|
38
|
+
# empty dict because a few lifted call sites still read ``MODELS`` (e.g.
|
|
39
|
+
# validate_registry's "is this a code-declared staple" check, media defaults).
|
|
40
|
+
MODELS: Dict[str, dict] = {}
|
|
41
|
+
|
|
42
|
+
# The live registry, keyed by the core id. Updated IN PLACE by refresh_registry
|
|
43
|
+
# so modules that imported these names by reference always see the current set.
|
|
44
|
+
MODEL_REGISTRY: Dict[str, ModelConfig] = {}
|
|
45
|
+
MODEL_REGISTRY_DICT: Dict[str, dict] = {}
|
|
46
|
+
|
|
47
|
+
_CORE_URL = (os.environ.get("HUGPY_CORE_URL") or "http://127.0.0.1:7020").rstrip("/")
|
|
48
|
+
_TTL_S = float(os.environ.get("HUGPY_REGISTRY_TTL_S", "30"))
|
|
49
|
+
_TIMEOUT_S = float(os.environ.get("HUGPY_REGISTRY_TIMEOUT_S", "10"))
|
|
50
|
+
_lock = threading.RLock()
|
|
51
|
+
_last_fetch = 0.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _dedup(seq) -> list:
|
|
55
|
+
"""Order-preserving de-dup, dropping falsy entries."""
|
|
56
|
+
out: list = []
|
|
57
|
+
for x in seq or []:
|
|
58
|
+
if x and x not in out:
|
|
59
|
+
out.append(x)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def derive_model_config_row(name, row) -> "Tuple[Optional[dict], Optional[str]]":
|
|
64
|
+
"""One hugpy_core catalog row -> a ModelConfig-ready dict, or (None, reason).
|
|
65
|
+
|
|
66
|
+
``name`` is the core id (the registry key). ``row`` is a catalog_rows entry;
|
|
67
|
+
its ``hugpy_json`` sub-dict is authoritative for framework/tasks/
|
|
68
|
+
primary_task/hub_id/name (operator: hugpy.json is the model init), with the
|
|
69
|
+
row's own top-level fields as the fallback the catalog already flattened.
|
|
70
|
+
|
|
71
|
+
framework == the wrapper's engine (gguf, transformers, comfy, ...): one
|
|
72
|
+
concept, kept verbatim. tasks are the plain HF pipeline-tag strings straight
|
|
73
|
+
from hugpy.json, no renaming. A null ``tasks`` (seen on some GGUF markers)
|
|
74
|
+
falls back to ``[primary_task]`` so every model has at least its own task.
|
|
75
|
+
"""
|
|
76
|
+
hj = row.get("hugpy_json") if isinstance(row.get("hugpy_json"), dict) else {}
|
|
77
|
+
|
|
78
|
+
framework = hj.get("framework") or row.get("framework")
|
|
79
|
+
if isinstance(framework, (list, tuple)):
|
|
80
|
+
framework = framework[0] if framework else None
|
|
81
|
+
framework = str(framework or "").strip().lower() or None
|
|
82
|
+
if not framework:
|
|
83
|
+
return None, f"{name}: no framework/engine recorded"
|
|
84
|
+
|
|
85
|
+
tasks = hj.get("tasks") if isinstance(hj.get("tasks"), list) else None
|
|
86
|
+
if not tasks:
|
|
87
|
+
tasks = row.get("tasks") if isinstance(row.get("tasks"), list) else None
|
|
88
|
+
primary = hj.get("primary_task") or row.get("primary_task") or row.get("task")
|
|
89
|
+
tasks = _dedup(tasks or ([primary] if primary else []))
|
|
90
|
+
if not tasks:
|
|
91
|
+
return None, f"{name}: no tasks and no primary_task recorded"
|
|
92
|
+
if not primary or primary not in tasks:
|
|
93
|
+
primary = tasks[0]
|
|
94
|
+
else:
|
|
95
|
+
# primary must be present; already is. Keep order with primary first is
|
|
96
|
+
# not required (ModelConfig only checks membership).
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
hub_id = hj.get("hub_id") or row.get("hub_id") or name
|
|
100
|
+
disp_name = hj.get("name") or row.get("name") or name
|
|
101
|
+
# folder is a required ModelConfig field; it is not load-bearing here (the
|
|
102
|
+
# fitevict front door routes by model_key, not folder), so the hub_id is a
|
|
103
|
+
# fine stable value.
|
|
104
|
+
folder = str(hub_id or name)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"name": disp_name,
|
|
108
|
+
"model_key": name,
|
|
109
|
+
"hub_id": str(hub_id),
|
|
110
|
+
"folder": folder,
|
|
111
|
+
"framework": framework,
|
|
112
|
+
"tasks": tasks,
|
|
113
|
+
"primary_task": primary,
|
|
114
|
+
"filename": hj.get("filename") or row.get("file"),
|
|
115
|
+
"include": hj.get("include"),
|
|
116
|
+
"model_max_length": hj.get("model_max_length") or row.get("model_max_length"),
|
|
117
|
+
}, None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _fetch_catalog() -> "Optional[list]":
|
|
121
|
+
"""GET {core}/models (catalog_rows). None on any failure (fail-open)."""
|
|
122
|
+
url = _CORE_URL + "/models"
|
|
123
|
+
try:
|
|
124
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
125
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT_S) as r:
|
|
126
|
+
data = json.loads(r.read().decode("utf-8") or "null")
|
|
127
|
+
except Exception as exc: # noqa: BLE001 — the registry read never raises
|
|
128
|
+
logger.warning("registry: could not read %s (%s); keeping the last "
|
|
129
|
+
"known model set (%d models)", url, exc, len(MODEL_REGISTRY))
|
|
130
|
+
return None
|
|
131
|
+
if isinstance(data, dict):
|
|
132
|
+
data = data.get("data") or data.get("models") or []
|
|
133
|
+
return data if isinstance(data, list) else []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def refresh_registry(force: bool = False) -> None:
|
|
137
|
+
"""Rebuild MODEL_REGISTRY / MODEL_REGISTRY_DICT from hugpy_core's catalog.
|
|
138
|
+
|
|
139
|
+
TTL-cached (HUGPY_REGISTRY_TTL_S) so a burst of calls hits core once. Updates
|
|
140
|
+
the dicts IN PLACE. Fail-open: a failed/empty fetch leaves the current set
|
|
141
|
+
untouched rather than emptying a working registry."""
|
|
142
|
+
global _last_fetch
|
|
143
|
+
now = time.time()
|
|
144
|
+
with _lock:
|
|
145
|
+
if not force and MODEL_REGISTRY and (now - _last_fetch) < _TTL_S:
|
|
146
|
+
return
|
|
147
|
+
rows = _fetch_catalog()
|
|
148
|
+
_last_fetch = now
|
|
149
|
+
if rows is None:
|
|
150
|
+
return # keep the last known set
|
|
151
|
+
fresh: Dict[str, ModelConfig] = {}
|
|
152
|
+
fresh_dict: Dict[str, dict] = {}
|
|
153
|
+
dropped = []
|
|
154
|
+
for row in rows:
|
|
155
|
+
if not isinstance(row, dict):
|
|
156
|
+
continue
|
|
157
|
+
key = row.get("model_key") or row.get("id") or row.get("name")
|
|
158
|
+
if not key:
|
|
159
|
+
continue
|
|
160
|
+
values, why = derive_model_config_row(key, row)
|
|
161
|
+
if values is None:
|
|
162
|
+
dropped.append((key, why))
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
cfg = ModelConfig(**values)
|
|
166
|
+
except Exception as exc: # noqa: BLE001 — a bad row never breaks the set
|
|
167
|
+
dropped.append((key, f"ModelConfig rejected: {exc}"))
|
|
168
|
+
continue
|
|
169
|
+
fresh[key] = cfg
|
|
170
|
+
fresh_dict[key] = cfg.to_dict()
|
|
171
|
+
if not fresh and MODEL_REGISTRY:
|
|
172
|
+
# Core answered but yielded nothing usable (all workers offline mid
|
|
173
|
+
# refresh): keep the last good set rather than blanking resolution.
|
|
174
|
+
logger.info("registry: catalog refresh yielded 0 usable rows; "
|
|
175
|
+
"keeping %d previous models", len(MODEL_REGISTRY))
|
|
176
|
+
return
|
|
177
|
+
MODEL_REGISTRY.clear(); MODEL_REGISTRY.update(fresh)
|
|
178
|
+
MODEL_REGISTRY_DICT.clear(); MODEL_REGISTRY_DICT.update(fresh_dict)
|
|
179
|
+
if dropped:
|
|
180
|
+
logger.info("registry: %d/%d catalog rows not servable here: %s",
|
|
181
|
+
len(dropped), len(rows),
|
|
182
|
+
", ".join(f"{k} ({w})" for k, w in dropped[:6]))
|
|
183
|
+
logger.info("registry: refreshed from %s — %d models",
|
|
184
|
+
_CORE_URL, len(MODEL_REGISTRY))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_model_registry(dict_return: bool = False, return_dict: bool = False):
|
|
188
|
+
if dict_return or return_dict:
|
|
189
|
+
return MODEL_REGISTRY_DICT
|
|
190
|
+
return MODEL_REGISTRY
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Default-model reconciliation (hugpy_next cut 2, lean).
|
|
2
|
+
|
|
3
|
+
TASK_DEFAULTS / MEDIA_DEFAULTS are seeded in categories.py from the configured
|
|
4
|
+
``DEFAULT_*`` constants. Those seeds are names a fresh box may not actually
|
|
5
|
+
serve, so they are RECONCILED against the live registry (sourced from
|
|
6
|
+
hugpy_core's catalog, keyed by core id): keep the configured default when the
|
|
7
|
+
registry holds it, else stand in a registry model that serves the task group.
|
|
8
|
+
|
|
9
|
+
Every registry row here is a model a worker is serving RIGHT NOW (it came from
|
|
10
|
+
the live catalog), so "installed" is implicit — there is no disk check and no
|
|
11
|
+
``route_destination`` import. ``reconcile_defaults()`` and
|
|
12
|
+
``refresh_task_registries()`` are re-callable so resolve() heals the dicts from
|
|
13
|
+
the current model set on every request.
|
|
14
|
+
"""
|
|
15
|
+
from typing import Dict
|
|
16
|
+
|
|
17
|
+
from abstract_essentials import make_list
|
|
18
|
+
from hugpy_engine.categories import MEDIA_DEFAULTS, TASK_DEFAULTS
|
|
19
|
+
from hugpy_engine.config.models.models_config import MODEL_REGISTRY
|
|
20
|
+
from hugpy_engine.schemas.model_schemas import ModelConfig
|
|
21
|
+
from hugpy_platform.constants import (
|
|
22
|
+
DEFAULT_AGENT_BRAIN,
|
|
23
|
+
DEFAULT_CHAT_MODEL,
|
|
24
|
+
DEFAULT_EMBED_MODEL,
|
|
25
|
+
DEFAULT_VISION_MODEL,
|
|
26
|
+
DEFAULT_WHISPER_MODEL,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_context_tokens():
|
|
31
|
+
return {k: v.model_max_length for k, v in MODEL_REGISTRY.items()}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_context_tokens_for_model(model_key: str) -> int:
|
|
35
|
+
return get_context_tokens().get(model_key, 8192)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_models_dict_by_tasks(tasks=None):
|
|
39
|
+
tasks = make_list(tasks or [])
|
|
40
|
+
return {k: v for k, v in MODEL_REGISTRY.items()
|
|
41
|
+
if any(t in tasks for t in v.tasks)}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_models_dict_by_names(names=None):
|
|
45
|
+
names = make_list(names or [])
|
|
46
|
+
out = {}
|
|
47
|
+
for k, v in MODEL_REGISTRY.items():
|
|
48
|
+
for name in names:
|
|
49
|
+
if name in v.name:
|
|
50
|
+
out[k] = v
|
|
51
|
+
break
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _reconcile_default(configured: str, registry: Dict[str, ModelConfig],
|
|
56
|
+
prefer_task: str | None = None) -> str:
|
|
57
|
+
"""Keep ``configured`` when the registry holds it; otherwise stand in a
|
|
58
|
+
registry model that serves ``prefer_task`` (then key order for determinism).
|
|
59
|
+
Returns ``configured`` unchanged when the registry is empty, so the honest
|
|
60
|
+
"not in MODEL_REGISTRY" refusal downstream stays accurate."""
|
|
61
|
+
if not registry:
|
|
62
|
+
return configured
|
|
63
|
+
if configured in registry:
|
|
64
|
+
return configured
|
|
65
|
+
pool = registry
|
|
66
|
+
if prefer_task:
|
|
67
|
+
preferred = {k: v for k, v in registry.items() if prefer_task in v.tasks}
|
|
68
|
+
pool = preferred or registry
|
|
69
|
+
if not pool:
|
|
70
|
+
return configured
|
|
71
|
+
return min(pool)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Seeds from constants.py; healed by reconcile_defaults() against the live set.
|
|
75
|
+
_SEED = {
|
|
76
|
+
"chat": DEFAULT_CHAT_MODEL,
|
|
77
|
+
"vision": DEFAULT_VISION_MODEL,
|
|
78
|
+
"whisper": DEFAULT_WHISPER_MODEL,
|
|
79
|
+
"embed": DEFAULT_EMBED_MODEL,
|
|
80
|
+
"brain": DEFAULT_AGENT_BRAIN,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Task-filtered views (updated IN PLACE — other modules hold them by reference).
|
|
84
|
+
CHAT_MODELS_REGISTRY: Dict[str, ModelConfig] = {}
|
|
85
|
+
VISION_MODELS_REGISTRY: Dict[str, ModelConfig] = {}
|
|
86
|
+
WHISPER_MODELS_REGISTRY: Dict[str, ModelConfig] = {}
|
|
87
|
+
EMBED_MODELS_REGISTRY: Dict[str, ModelConfig] = {}
|
|
88
|
+
|
|
89
|
+
_TASK_REGISTRY_GROUPS = (
|
|
90
|
+
(CHAT_MODELS_REGISTRY, ["text-generation", "text-generation-inference", "text2text-generation"]),
|
|
91
|
+
(VISION_MODELS_REGISTRY, ["image-text-to-text", "text-to-image"]),
|
|
92
|
+
(WHISPER_MODELS_REGISTRY, ["automatic-speech-recognition", "speech-recognition"]),
|
|
93
|
+
(EMBED_MODELS_REGISTRY, ["feature-extraction", "sentence-similarity", "sentence-transformers"]),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def refresh_task_registries() -> None:
|
|
98
|
+
"""Rebuild the task-filtered registries in place from the live MODEL_REGISTRY
|
|
99
|
+
(update-then-prune so a concurrent reader never sees an empty dict)."""
|
|
100
|
+
for registry, tasks in _TASK_REGISTRY_GROUPS:
|
|
101
|
+
fresh = get_models_dict_by_tasks(tasks=tasks)
|
|
102
|
+
registry.update(fresh)
|
|
103
|
+
for stale in [k for k in registry if k not in fresh]:
|
|
104
|
+
registry.pop(stale, None)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# The reconciled defaults (module globals, rebound by reconcile_defaults()).
|
|
108
|
+
DEFAULT_CHAT_MODEL = _SEED["chat"]
|
|
109
|
+
DEFAULT_VISION_MODEL = _SEED["vision"]
|
|
110
|
+
DEFAULT_WHISPER_MODEL = _SEED["whisper"]
|
|
111
|
+
DEFAULT_EMBED_MODEL = _SEED["embed"]
|
|
112
|
+
DEFAULT_AGENT_BRAIN = _SEED["brain"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def reconcile_defaults() -> None:
|
|
116
|
+
"""Recompute the DEFAULT_* model keys from the live registry and HEAL
|
|
117
|
+
TASK_DEFAULTS / MEDIA_DEFAULTS in place so a task-only / media-only / bare
|
|
118
|
+
request resolves to a real core id instead of a dangling configured name."""
|
|
119
|
+
global DEFAULT_CHAT_MODEL, DEFAULT_VISION_MODEL, DEFAULT_WHISPER_MODEL
|
|
120
|
+
global DEFAULT_EMBED_MODEL, DEFAULT_AGENT_BRAIN
|
|
121
|
+
refresh_task_registries()
|
|
122
|
+
DEFAULT_CHAT_MODEL = _reconcile_default(_SEED["chat"], CHAT_MODELS_REGISTRY,
|
|
123
|
+
prefer_task="text-generation")
|
|
124
|
+
DEFAULT_AGENT_BRAIN = _reconcile_default(_SEED["brain"], CHAT_MODELS_REGISTRY,
|
|
125
|
+
prefer_task="text-generation")
|
|
126
|
+
DEFAULT_VISION_MODEL = _reconcile_default(_SEED["vision"], VISION_MODELS_REGISTRY,
|
|
127
|
+
prefer_task="image-text-to-text")
|
|
128
|
+
DEFAULT_WHISPER_MODEL = _reconcile_default(_SEED["whisper"], WHISPER_MODELS_REGISTRY,
|
|
129
|
+
prefer_task="automatic-speech-recognition")
|
|
130
|
+
DEFAULT_EMBED_MODEL = _reconcile_default(_SEED["embed"], EMBED_MODELS_REGISTRY,
|
|
131
|
+
prefer_task="feature-extraction")
|
|
132
|
+
reconciled = {
|
|
133
|
+
_SEED["chat"]: DEFAULT_CHAT_MODEL,
|
|
134
|
+
_SEED["vision"]: DEFAULT_VISION_MODEL,
|
|
135
|
+
_SEED["whisper"]: DEFAULT_WHISPER_MODEL,
|
|
136
|
+
_SEED["embed"]: DEFAULT_EMBED_MODEL,
|
|
137
|
+
}
|
|
138
|
+
for defaults, keys_are_tasks in ((TASK_DEFAULTS, True), (MEDIA_DEFAULTS, False)):
|
|
139
|
+
for key, model in list(defaults.items()):
|
|
140
|
+
replacement = reconciled.get(model)
|
|
141
|
+
if not replacement or replacement == model:
|
|
142
|
+
continue
|
|
143
|
+
if keys_are_tasks and replacement in MODEL_REGISTRY \
|
|
144
|
+
and key not in MODEL_REGISTRY[replacement].tasks:
|
|
145
|
+
continue # the stand-in serves the group but not this exact task
|
|
146
|
+
defaults[key] = replacement
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Heal once at import (registry may be empty here -> seeds, re-healed per request).
|
|
150
|
+
reconcile_defaults()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Request dispatch: runner cache, execution, streaming, eviction."""
|
|
2
|
+
from hugpy_engine.dispatch.dispatch import (
|
|
3
|
+
LoadRefusal,
|
|
4
|
+
clear,
|
|
5
|
+
ensure_headroom_for_load,
|
|
6
|
+
evict,
|
|
7
|
+
execute_chat_stream,
|
|
8
|
+
execute_prompt,
|
|
9
|
+
execute_prompt_stream,
|
|
10
|
+
infer_arg_name,
|
|
11
|
+
last_used_snapshot,
|
|
12
|
+
loaded_disk_detail,
|
|
13
|
+
loaded_model_keys,
|
|
14
|
+
loading_model_keys,
|
|
15
|
+
no_makeroom_active,
|
|
16
|
+
normalize_prompt_kwargs,
|
|
17
|
+
runner_for,
|
|
18
|
+
set_evict_reason,
|
|
19
|
+
set_evictable,
|
|
20
|
+
set_fit_check,
|
|
21
|
+
set_make_room,
|
|
22
|
+
set_post_evict_hook,
|
|
23
|
+
stream_runner,
|
|
24
|
+
supported_task_keys,
|
|
25
|
+
touch_model,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"LoadRefusal", "clear", "ensure_headroom_for_load", "evict",
|
|
30
|
+
"execute_chat_stream", "execute_prompt", "execute_prompt_stream",
|
|
31
|
+
"infer_arg_name", "last_used_snapshot", "loaded_disk_detail",
|
|
32
|
+
"loaded_model_keys", "loading_model_keys", "no_makeroom_active",
|
|
33
|
+
"normalize_prompt_kwargs", "runner_for", "set_evict_reason", "set_evictable",
|
|
34
|
+
"set_fit_check", "set_make_room", "set_post_evict_hook", "stream_runner",
|
|
35
|
+
"supported_task_keys", "touch_model",
|
|
36
|
+
]
|