hugpy-engine 0.3.2__tar.gz → 0.3.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.
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/PKG-INFO +1 -1
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/pyproject.toml +1 -1
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/categories.py +7 -16
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/config/models/models_config.py +43 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/model_resolver.py +50 -22
- hugpy_engine-0.3.4/src/hugpy_engine/task_endpoints.py +239 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine.egg-info/PKG-INFO +1 -1
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine.egg-info/SOURCES.txt +1 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/README.md +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/setup.cfg +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/config/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/config/main.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/config/models/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/config/models/models_default.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/dispatch/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/dispatch/dispatch.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/fit/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/fit/types.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/llama/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/llama/runners/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/llama/runners/fitevict_runner.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/name_match.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/placement.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/py.typed +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/assure_model_key.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/categories/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/categories/builders.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/categories/frameworks.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/__init__.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/chat_schemas.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/event_schemas.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/metadata_schemas.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/model_schemas.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/runner_schemas.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/schemas/task_schemas.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/task_deps.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/tasks.py +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine.egg-info/dependency_links.txt +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine.egg-info/requires.txt +0 -0
- {hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/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.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
|
|
@@ -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.4"
|
|
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"
|
|
@@ -114,19 +114,10 @@ MEDIA_DEFAULTS: Dict[str, str] = {
|
|
|
114
114
|
"video": DEFAULT_WHISPER_MODEL,
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}
|
|
117
|
+
# TASK DEFAULTS ARE NOT CONSTANTS ANY MORE (operator ruling 2026-10-04: "each
|
|
118
|
+
# task is BOUND to a designated model in the DB. No guessing, no constants").
|
|
119
|
+
# The task -> model_key map now lives in the DB (settings_policies
|
|
120
|
+
# "task_bindings") and is read live from hugpy_core's GET /prompt/tasks
|
|
121
|
+
# ``defaults`` via config.models.models_config.task_bindings(). This name is
|
|
122
|
+
# kept EMPTY only so a mid-rollout importer does not break; nothing populates it.
|
|
123
|
+
TASK_DEFAULTS: Dict[str, str] = {}
|
|
@@ -188,3 +188,46 @@ def get_model_registry(dict_return: bool = False, return_dict: bool = False):
|
|
|
188
188
|
if dict_return or return_dict:
|
|
189
189
|
return MODEL_REGISTRY_DICT
|
|
190
190
|
return MODEL_REGISTRY
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# Task bindings — the task -> model_key map, from the DB via hugpy_core.
|
|
195
|
+
#
|
|
196
|
+
# Operator ruling 2026-10-04: "each task is BOUND to a designated model in the
|
|
197
|
+
# DB. No guessing, no constants." The bindings live in settings_policies
|
|
198
|
+
# ("task_bindings") and hugpy_core exposes them as the ``defaults`` of
|
|
199
|
+
# ``GET {core}/prompt/tasks``. The engine reads THAT (never a hardcoded
|
|
200
|
+
# categories table): TTL-cached, fail-open to the last-known map (empty at
|
|
201
|
+
# worst, which yields the honest "no model bound to task X" refusal).
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
_bindings: dict[str, str] = {}
|
|
204
|
+
_bindings_at = 0.0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def task_bindings(force: bool = False) -> dict:
|
|
208
|
+
"""The live {task: model_key} bindings from hugpy_core's /prompt/tasks
|
|
209
|
+
``defaults``. TTL-cached; a failed/empty read keeps the last-known map."""
|
|
210
|
+
global _bindings, _bindings_at
|
|
211
|
+
now = time.time()
|
|
212
|
+
if not force and _bindings and (now - _bindings_at) < _TTL_S:
|
|
213
|
+
return dict(_bindings)
|
|
214
|
+
url = _CORE_URL + "/prompt/tasks"
|
|
215
|
+
try:
|
|
216
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
217
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT_S) as r:
|
|
218
|
+
data = json.loads(r.read().decode("utf-8") or "null")
|
|
219
|
+
except Exception as exc: # noqa: BLE001 — never raise on the resolve path
|
|
220
|
+
logger.warning("registry: could not read task bindings from %s (%s); "
|
|
221
|
+
"keeping the last-known map (%d)", url, exc, len(_bindings))
|
|
222
|
+
_bindings_at = now
|
|
223
|
+
return dict(_bindings)
|
|
224
|
+
defaults = (data or {}).get("defaults") if isinstance(data, dict) else None
|
|
225
|
+
if isinstance(defaults, dict):
|
|
226
|
+
_bindings = {str(k): str(v) for k, v in defaults.items() if v}
|
|
227
|
+
_bindings_at = now
|
|
228
|
+
return dict(_bindings)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def task_binding(task: str) -> "Optional[str]":
|
|
232
|
+
"""The model_key bound to ``task``, or None (no binding)."""
|
|
233
|
+
return task_bindings().get(task)
|
|
@@ -26,13 +26,16 @@ from abstract_essentials import derive_media_type, safe_load_from_json
|
|
|
26
26
|
from pydantic import BaseModel
|
|
27
27
|
import logging
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
|
-
from hugpy_engine.categories import MEDIA_DEFAULTS
|
|
30
|
-
from hugpy_engine.config.models.models_config import
|
|
29
|
+
from hugpy_engine.categories import MEDIA_DEFAULTS
|
|
30
|
+
from hugpy_engine.config.models.models_config import (
|
|
31
|
+
MODELS, MODEL_REGISTRY, refresh_registry, task_bindings)
|
|
31
32
|
from hugpy_engine.config.models.models_default import DEFAULT_CHAT_MODEL
|
|
32
33
|
from hugpy_engine.schemas.task_schemas import Resolution
|
|
33
34
|
from hugpy_engine.resolvers.categories.builders import MODEL_REQUEST_BUILDERS
|
|
34
35
|
from hugpy_engine.resolvers.categories.frameworks import FRAMEWORK_RUNNERS, KNOWN_TASKS_REGISTRY
|
|
35
36
|
from hugpy_engine.resolvers.assure_model_key import assure_model_key
|
|
37
|
+
from hugpy_engine.task_endpoints import (
|
|
38
|
+
TASK_ROUTES, TaskEndpointRunner, build_endpoint_request)
|
|
36
39
|
|
|
37
40
|
# ---------------------------------------------------------------------------
|
|
38
41
|
# Unclassified / adapter rows — refuse by NAME, never by assumption (k61).
|
|
@@ -137,9 +140,9 @@ def resolve_model_key(
|
|
|
137
140
|
explicit representation override — authoritative when choosing between the
|
|
138
141
|
GGUF and Transformers rows of the same model (operator 2026-09-29).
|
|
139
142
|
"""
|
|
140
|
-
if task is not None and task not in KNOWN_TASKS_REGISTRY:
|
|
143
|
+
if task is not None and task not in KNOWN_TASKS_REGISTRY and task not in TASK_ROUTES:
|
|
141
144
|
raise KeyError(
|
|
142
|
-
f"Unknown task={task!r}; known: {sorted(KNOWN_TASKS_REGISTRY)}"
|
|
145
|
+
f"Unknown task={task!r}; known: {sorted(set(KNOWN_TASKS_REGISTRY) | set(TASK_ROUTES))}"
|
|
143
146
|
)
|
|
144
147
|
fmt = (model_format or "auto").strip().lower() if model_format else "auto"
|
|
145
148
|
if fmt not in _MODEL_FORMATS:
|
|
@@ -177,22 +180,25 @@ def resolve_model_key(
|
|
|
177
180
|
return model_key
|
|
178
181
|
|
|
179
182
|
if task is not None:
|
|
180
|
-
|
|
183
|
+
# Task -> model is a DB BINDING (settings_policies "task_bindings"), read
|
|
184
|
+
# live from hugpy_core's /prompt/tasks defaults. No guessing, no constants.
|
|
185
|
+
inferred = task_bindings().get(task)
|
|
181
186
|
if inferred is None:
|
|
182
187
|
raise KeyError(
|
|
183
|
-
f"
|
|
184
|
-
f"
|
|
188
|
+
f"no model bound to task {task!r}; bind one in Settings "
|
|
189
|
+
f"(task_bindings)"
|
|
185
190
|
)
|
|
186
191
|
if inferred not in MODEL_REGISTRY:
|
|
187
192
|
raise KeyError(
|
|
188
|
-
f"
|
|
193
|
+
f"model {inferred!r} bound to task {task!r} is not in the registry "
|
|
194
|
+
f"— no online worker has it (see /models)"
|
|
189
195
|
)
|
|
190
196
|
if task not in MODEL_REGISTRY[inferred].tasks:
|
|
191
197
|
raise ValueError(
|
|
192
|
-
f"
|
|
193
|
-
f"
|
|
198
|
+
f"model {inferred!r} bound to task {task!r} does not list it in its "
|
|
199
|
+
f"hugpy.json tasks={sorted(MODEL_REGISTRY[inferred].tasks)!r}"
|
|
194
200
|
)
|
|
195
|
-
logger.debug("resolve_model_key: task=%s -> key=%s", task, inferred)
|
|
201
|
+
logger.debug("resolve_model_key: task=%s -> bound key=%s", task, inferred)
|
|
196
202
|
return inferred
|
|
197
203
|
|
|
198
204
|
if media_type is None and file is not None:
|
|
@@ -218,20 +224,21 @@ def resolve_model_key(
|
|
|
218
224
|
logger.debug("resolve_model_key: media=%s -> key=%s", media_type, inferred)
|
|
219
225
|
return inferred
|
|
220
226
|
|
|
221
|
-
# Bare chat default — no model_key, task, media or file given.
|
|
222
|
-
# is
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
# Bare chat default — no model_key, task, media or file given. The default
|
|
228
|
+
# is the DB BINDING for text-generation (hugpy_core /prompt/tasks), never a
|
|
229
|
+
# constant. No binding -> an honest refusal (fail-as-data), never a surprise.
|
|
230
|
+
chat_default = task_bindings().get("text-generation")
|
|
231
|
+
if not chat_default:
|
|
232
|
+
raise KeyError(
|
|
233
|
+
"no model bound to task 'text-generation'; bind one in Settings "
|
|
234
|
+
"(task_bindings), or pass an explicit model_key"
|
|
235
|
+
)
|
|
227
236
|
if chat_default not in MODEL_REGISTRY:
|
|
228
237
|
raise KeyError(
|
|
229
|
-
f"
|
|
230
|
-
f"
|
|
231
|
-
f"{chat_default!r}). Pass an explicit model_key, or make sure a "
|
|
232
|
-
f"chat-capable model is online on a worker."
|
|
238
|
+
f"model {chat_default!r} bound to task 'text-generation' is not in the "
|
|
239
|
+
f"registry — no online worker has it (see /models)"
|
|
233
240
|
)
|
|
234
|
-
logger.debug("resolve_model_key: fallback to chat default=%s", chat_default)
|
|
241
|
+
logger.debug("resolve_model_key: fallback to bound chat default=%s", chat_default)
|
|
235
242
|
return chat_default
|
|
236
243
|
|
|
237
244
|
|
|
@@ -334,6 +341,27 @@ def resolve(prompt_kwargs: Dict[str, Any]) -> Resolution:
|
|
|
334
341
|
|
|
335
342
|
key = (framework, task)
|
|
336
343
|
|
|
344
|
+
# NON-CHAT TASKS -> the dedicated front-door route (hugpy_next cut 3). These
|
|
345
|
+
# bypass the plugin runner table ON PURPOSE: the media/video engine-plugins
|
|
346
|
+
# register IN-PROCESS runners for these tasks, and nothing may load a model
|
|
347
|
+
# in core (one GPU owner per box = the wrapper). resolve() routes them
|
|
348
|
+
# straight to TaskEndpointRunner, which relays to hugpy_core's /v1/<route>;
|
|
349
|
+
# core picks the worker holding this model (the same pick() as chat).
|
|
350
|
+
_endpoint = TASK_ROUTES.get(task)
|
|
351
|
+
if _endpoint is not None:
|
|
352
|
+
_route, _output = _endpoint
|
|
353
|
+
|
|
354
|
+
def _builder(_kw, _mk, *, _t=task, _r=_route, _o=_output):
|
|
355
|
+
return build_endpoint_request(_kw, _mk, task=_t, route=_r, output=_o)
|
|
356
|
+
|
|
357
|
+
logger.debug("resolve: model=%s task=%s -> front-door %s",
|
|
358
|
+
model_key, task, _route)
|
|
359
|
+
return Resolution(
|
|
360
|
+
model_key=model_key, framework=framework, task=task, cfg=cfg,
|
|
361
|
+
builder=_builder, runner_cls=TaskEndpointRunner,
|
|
362
|
+
cache_key=(model_key, task),
|
|
363
|
+
)
|
|
364
|
+
|
|
337
365
|
builder = MODEL_REQUEST_BUILDERS.get(key)
|
|
338
366
|
if builder is None:
|
|
339
367
|
raise KeyError(
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Non-chat task dispatch — one HTTP relay per task family through the front door.
|
|
2
|
+
|
|
3
|
+
Chat (text-generation / image-text-to-text) rides FitevictChatRunner ->
|
|
4
|
+
hugpy_core ``/v1/chat/completions``. Every OTHER served task (embeddings,
|
|
5
|
+
summarize, keywords, ASR, TTS, image/video generation) has a dedicated
|
|
6
|
+
front-door route; this module is the engine side of that:
|
|
7
|
+
|
|
8
|
+
* :data:`TASK_ROUTES` — the task -> (route, output) PROTOCOL table. It MIRRORS
|
|
9
|
+
the wrapper's RunnerSpec ``tasks``/``endpoint`` (fitevict/lifted/runners).
|
|
10
|
+
Routes are protocol, so they live in code; MODEL NAMES never do (bindings
|
|
11
|
+
come from the DB via hugpy_core, see config.models.models_config.task_bindings).
|
|
12
|
+
* :func:`build_endpoint_request` — maps execute_prompt kwargs onto each route's
|
|
13
|
+
JSON body using the hugpy-media / abstract-hugpy field names (text/texts/
|
|
14
|
+
prompt -> input, file -> file_path, summary_mode/top_n/... pass through).
|
|
15
|
+
* :class:`TaskEndpointRunner` — POSTs ``{HUGPY_CORE_URL}/v1/<route>`` with that
|
|
16
|
+
body (+ ``model``), and returns the response as a TaskResult. hugpy_core
|
|
17
|
+
picks the worker holding ``body.model`` (same pick() as chat); the box's
|
|
18
|
+
wrapper owns the load, serving and call_log. Nothing loads a model in core.
|
|
19
|
+
|
|
20
|
+
``resolve()`` selects this runner for any task in :data:`TASK_ROUTES`, ahead of
|
|
21
|
+
the plugin runner table — the media/video engine-plugins register in-process
|
|
22
|
+
runners that must NOT run in core (one GPU owner per box = the wrapper).
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import base64
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import urllib.error
|
|
33
|
+
import urllib.request
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
from pydantic import ConfigDict
|
|
37
|
+
|
|
38
|
+
from hugpy_engine.schemas.event_schemas import DoneEvent, ErrorEvent, TokenEvent
|
|
39
|
+
from hugpy_engine.schemas.task_schemas import TaskRequest, TaskResult
|
|
40
|
+
from hugpy_platform.utils import make_request_id
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
CORE_URL = (os.environ.get("HUGPY_CORE_URL")
|
|
45
|
+
or os.environ.get("HUGPY_FITEVICT_FRONT_DOOR")
|
|
46
|
+
or "http://127.0.0.1:7020").rstrip("/")
|
|
47
|
+
_TIMEOUT_S = float(os.environ.get("HUGPY_TASK_ENDPOINT_TIMEOUT_S", "900"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# task -> (front-door route, output kind). Mirrors the wrapper RunnerSpecs:
|
|
51
|
+
# embed /v1/embeddings | summarize /v1/summarize | keywords /v1/keywords |
|
|
52
|
+
# asr /v1/audio/transcriptions | tts /v1/audio/speech |
|
|
53
|
+
# imagegen /v1/images/generations | videogen /v1/videos/generations
|
|
54
|
+
# The HF pipeline-tag spellings a hugpy.json may carry are all mapped (the
|
|
55
|
+
# wrapper's spec.tasks list the same aliases).
|
|
56
|
+
TASK_ROUTES: dict[str, tuple[str, str]] = {
|
|
57
|
+
"feature-extraction": ("/v1/embeddings", "json"),
|
|
58
|
+
"sentence-similarity": ("/v1/embeddings", "json"),
|
|
59
|
+
"text-summarization": ("/v1/summarize", "json"),
|
|
60
|
+
"summarization": ("/v1/summarize", "json"),
|
|
61
|
+
"text2text-generation": ("/v1/summarize", "json"),
|
|
62
|
+
"keyword-extraction": ("/v1/keywords", "json"),
|
|
63
|
+
"automatic-speech-recognition": ("/v1/audio/transcriptions", "json"),
|
|
64
|
+
"text-to-speech": ("/v1/audio/speech", "audio"),
|
|
65
|
+
"text-to-image": ("/v1/images/generations", "image"),
|
|
66
|
+
"image-to-image": ("/v1/images/generations", "image"),
|
|
67
|
+
"inpainting": ("/v1/images/generations", "image"),
|
|
68
|
+
"text-to-video": ("/v1/videos/generations", "video"),
|
|
69
|
+
"image-to-video": ("/v1/videos/generations", "video"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def route_for_task(task: str) -> "Optional[tuple[str, str]]":
|
|
74
|
+
return TASK_ROUTES.get(task)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── request-body mapping, per ROUTE (shared across the tasks on that route) ──
|
|
78
|
+
# ``input_key``: where the primary payload lands; ``sources``: kwargs checked in
|
|
79
|
+
# order for it; ``file_sources``: folded to ``file_path``; ``passthrough``:
|
|
80
|
+
# body params copied verbatim when present. The hugpy-media / abstract-hugpy
|
|
81
|
+
# field names, so the wrapper runners (copied from hugpy_media) read them 1:1.
|
|
82
|
+
_BODY_SPECS: dict[str, dict] = {
|
|
83
|
+
"/v1/embeddings": {
|
|
84
|
+
"input_key": "input", "sources": ("input", "texts", "text", "prompt"),
|
|
85
|
+
"passthrough": ("other_texts", "normalize"),
|
|
86
|
+
},
|
|
87
|
+
"/v1/summarize": {
|
|
88
|
+
"input_key": "input", "sources": ("input", "text", "prompt"),
|
|
89
|
+
"passthrough": ("max_length", "min_length", "max_chunk_tokens",
|
|
90
|
+
"summary_mode"),
|
|
91
|
+
},
|
|
92
|
+
"/v1/keywords": {
|
|
93
|
+
"input_key": "input", "sources": ("input", "text", "prompt"),
|
|
94
|
+
"passthrough": ("top_n", "diversity", "use_mmr", "stop_words",
|
|
95
|
+
"keyphrase_ngram_range"),
|
|
96
|
+
},
|
|
97
|
+
"/v1/audio/transcriptions": {
|
|
98
|
+
"input_key": None,
|
|
99
|
+
"file_sources": ("file_path", "file", "path", "image_path"),
|
|
100
|
+
"passthrough": ("file_b64", "language", "translate", "prompt",
|
|
101
|
+
"temperature", "response_format"),
|
|
102
|
+
},
|
|
103
|
+
"/v1/audio/speech": {
|
|
104
|
+
"input_key": "input", "sources": ("input", "text", "prompt"),
|
|
105
|
+
"passthrough": ("voice", "response_format", "format", "speed"),
|
|
106
|
+
},
|
|
107
|
+
"/v1/images/generations": {
|
|
108
|
+
"input_key": "prompt", "sources": ("prompt", "input", "text"),
|
|
109
|
+
"passthrough": ("n", "size", "steps", "num_inference_steps",
|
|
110
|
+
"guidance_scale", "negative_prompt", "seed", "width",
|
|
111
|
+
"height", "image", "mask", "strength", "_route"),
|
|
112
|
+
},
|
|
113
|
+
"/v1/videos/generations": {
|
|
114
|
+
"input_key": "prompt", "sources": ("prompt", "input", "text"),
|
|
115
|
+
"passthrough": ("num_frames", "fps", "seed", "width", "height", "steps",
|
|
116
|
+
"num_inference_steps", "image", "negative_prompt"),
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class EndpointRequest(TaskRequest):
|
|
122
|
+
"""A built non-chat request: the resolved front-door ``route`` + JSON ``body``
|
|
123
|
+
(model-less; the runner adds ``model``). ``output`` is the artifact kind."""
|
|
124
|
+
model_config = ConfigDict(extra="forbid")
|
|
125
|
+
route: str
|
|
126
|
+
body: dict = {}
|
|
127
|
+
output: str = "json"
|
|
128
|
+
timeout_s: float = _TIMEOUT_S
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_endpoint_request(kwargs: dict, model_key: str, *, task: str,
|
|
132
|
+
route: str, output: str = "json") -> EndpointRequest:
|
|
133
|
+
"""execute_prompt kwargs -> the front-door JSON body for ``route``."""
|
|
134
|
+
spec = _BODY_SPECS.get(route, {"input_key": "input",
|
|
135
|
+
"sources": ("input", "text", "prompt"),
|
|
136
|
+
"passthrough": ()})
|
|
137
|
+
body: dict = {}
|
|
138
|
+
file_sources = spec.get("file_sources") or ()
|
|
139
|
+
for k in file_sources:
|
|
140
|
+
v = kwargs.get(k)
|
|
141
|
+
if v:
|
|
142
|
+
# The serving box is whichever core picks — it does not share this
|
|
143
|
+
# host's disk. A file readable HERE travels as bytes (file_b64, which
|
|
144
|
+
# every runner's input_file() accepts); anything else (a path only the
|
|
145
|
+
# box knows) is passed as-is.
|
|
146
|
+
if isinstance(v, str) and os.path.isfile(v):
|
|
147
|
+
with open(v, "rb") as fh:
|
|
148
|
+
body["file_b64"] = base64.b64encode(fh.read()).decode("ascii")
|
|
149
|
+
body["filename"] = os.path.basename(v)
|
|
150
|
+
else:
|
|
151
|
+
body["file_path"] = v
|
|
152
|
+
break
|
|
153
|
+
input_key = spec.get("input_key")
|
|
154
|
+
if input_key:
|
|
155
|
+
for k in spec.get("sources", ()):
|
|
156
|
+
if kwargs.get(k) is not None:
|
|
157
|
+
body[input_key] = kwargs[k]
|
|
158
|
+
break
|
|
159
|
+
for k in spec.get("passthrough", ()):
|
|
160
|
+
if kwargs.get(k) is not None:
|
|
161
|
+
body[k] = kwargs[k]
|
|
162
|
+
rid = kwargs.get("request_id") or make_request_id()
|
|
163
|
+
return EndpointRequest(request_id=rid, model_key=model_key, route=route,
|
|
164
|
+
body=body, output=output,
|
|
165
|
+
pool=kwargs.get("pool"))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TaskEndpointRunner:
|
|
169
|
+
"""Relays one non-chat request to hugpy_core's front-door route and returns
|
|
170
|
+
its response as a TaskResult. One class for every route — the route rides the
|
|
171
|
+
request (EndpointRequest.route), so dispatch's ``runner_cls(cfg)`` build is
|
|
172
|
+
uniform and the per-(model_key, task) instance cache still applies."""
|
|
173
|
+
|
|
174
|
+
request_type = EndpointRequest
|
|
175
|
+
result_type = TaskResult
|
|
176
|
+
|
|
177
|
+
def __init__(self, cfg, **runtime_kwargs):
|
|
178
|
+
self.cfg = cfg
|
|
179
|
+
self.model_key = getattr(cfg, "model_key", None)
|
|
180
|
+
|
|
181
|
+
def _post(self, url: str, body: dict, timeout: float) -> "tuple[int, dict]":
|
|
182
|
+
req = urllib.request.Request(url, data=json.dumps(body).encode("utf-8"),
|
|
183
|
+
headers={"Content-Type": "application/json"},
|
|
184
|
+
method="POST")
|
|
185
|
+
try:
|
|
186
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
187
|
+
return r.status, json.loads(r.read().decode("utf-8") or "{}")
|
|
188
|
+
except urllib.error.HTTPError as exc:
|
|
189
|
+
try:
|
|
190
|
+
return exc.code, json.loads(exc.read().decode("utf-8") or "{}")
|
|
191
|
+
except Exception: # noqa: BLE001
|
|
192
|
+
return exc.code, {"error": {"message": str(exc)}}
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _error(status: int, payload) -> str:
|
|
196
|
+
err = payload.get("error") if isinstance(payload, dict) else None
|
|
197
|
+
msg = err.get("message") if isinstance(err, dict) else err
|
|
198
|
+
return f"front door {status}: {msg or payload}"
|
|
199
|
+
|
|
200
|
+
def _result(self, req: EndpointRequest, status: int, payload) -> TaskResult:
|
|
201
|
+
ok = status == 200
|
|
202
|
+
fields = dict(payload) if isinstance(payload, dict) else {"data": payload}
|
|
203
|
+
for k in ("request_id", "model_key", "ok", "error"):
|
|
204
|
+
fields.pop(k, None)
|
|
205
|
+
return TaskResult(request_id=req.request_id, model_key=req.model_key,
|
|
206
|
+
ok=ok, error=(None if ok else self._error(status, payload)),
|
|
207
|
+
**fields)
|
|
208
|
+
|
|
209
|
+
async def run(self, req) -> TaskResult:
|
|
210
|
+
if not isinstance(req, EndpointRequest):
|
|
211
|
+
raise TypeError("TaskEndpointRunner requires an EndpointRequest")
|
|
212
|
+
body = {**req.body, "model": req.model_key}
|
|
213
|
+
status, payload = await asyncio.to_thread(
|
|
214
|
+
self._post, CORE_URL + req.route, body, req.timeout_s)
|
|
215
|
+
return self._result(req, status, payload)
|
|
216
|
+
|
|
217
|
+
async def stream(self, req: EndpointRequest, cancel_event=None):
|
|
218
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
219
|
+
yield DoneEvent(request_id=req.request_id, input_tokens=0,
|
|
220
|
+
output_chunks=0, finish_reason="cancelled")
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
res = await self.run(req)
|
|
224
|
+
except Exception as exc: # noqa: BLE001 — a transport fault is an event
|
|
225
|
+
yield ErrorEvent(request_id=req.request_id,
|
|
226
|
+
message=f"front door unreachable: {exc}")
|
|
227
|
+
return
|
|
228
|
+
if not res.ok:
|
|
229
|
+
yield ErrorEvent(request_id=req.request_id, message=res.error or "run failed")
|
|
230
|
+
return
|
|
231
|
+
text = getattr(res, "text", None)
|
|
232
|
+
if text:
|
|
233
|
+
yield TokenEvent(request_id=req.request_id, text=str(text))
|
|
234
|
+
yield DoneEvent(request_id=req.request_id, input_tokens=0, output_chunks=1,
|
|
235
|
+
finish_reason="stop")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = ["TASK_ROUTES", "route_for_task", "build_endpoint_request",
|
|
239
|
+
"EndpointRequest", "TaskEndpointRunner", "CORE_URL"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hugpy-engine
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hugpy_engine-0.3.2 → hugpy_engine-0.3.4}/src/hugpy_engine/resolvers/categories/frameworks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|