File size: 5,299 Bytes
dfb775d 730c5bb dfb775d 730c5bb dfb775d 730c5bb dfb775d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | """ModelRegistry — backend protocol + factory dispatch.
Canonical home per mindxtrain2.md §Part 4 `models.registry`. Merges the previous
`automindx.models.base` (Backend ABC + Pydantic chat schemas) and
`automindx.models.factory` (decorator-style register/build).
`Backend` is the plug interface; concrete backends live under
`mindxtrain.operator.backends.*` (vllm, openai_compat, ...).
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import AsyncIterator, Callable
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
Role = Literal["system", "user", "assistant", "tool"]
class ChatMessage(BaseModel):
model_config = ConfigDict(extra="forbid")
role: Role
content: str
class ChatRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
model: str
messages: list[ChatMessage]
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
max_tokens: int = Field(default=512, ge=1, le=131072)
stream: bool = False
class ChatResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
model: str
content: str
finish_reason: Literal["stop", "length", "error"] = "stop"
prompt_tokens: int = 0
completion_tokens: int = 0
# Engine-issued provenance for this answer, when the engine gives one (bankml's
# `bankml_receipt`: model_sha256, request_sha256, response_sha256, engine, tokens,
# wall_ms, ...). None for backends that issue no receipt.
receipt: dict[str, Any] | None = None
class Backend(ABC):
"""Pluggable inference backend; one of vLLM, OpenAI-compatible, HF Transformers, etc."""
name: str
@abstractmethod
async def chat(self, request: ChatRequest) -> ChatResponse: ...
@abstractmethod
async def stream_chat(self, request: ChatRequest) -> AsyncIterator[str]: ...
# Back-compat alias — old code imported `ModelBackend`.
ModelBackend = Backend
# ---- registry -------------------------------------------------------------
_REGISTRY: dict[str, Callable[..., Backend]] = {}
def register_backend(name: str) -> Callable[[Callable[..., Backend]], Callable[..., Backend]]:
"""Decorator: register a Backend constructor under `name`."""
def _wrap(ctor: Callable[..., Backend]) -> Callable[..., Backend]:
if name in _REGISTRY:
msg = f"backend {name!r} already registered"
raise ValueError(msg)
_REGISTRY[name] = ctor
return ctor
return _wrap
def build_backend(name: str, **kwargs: object) -> Backend:
"""Instantiate the backend registered under `name`."""
if name not in _REGISTRY:
available = ", ".join(sorted(_REGISTRY)) or "(none registered)"
msg = f"unknown backend {name!r}. registered: {available}"
raise KeyError(msg)
return _REGISTRY[name](**kwargs)
def list_backends() -> list[str]:
return sorted(_REGISTRY)
# ---- per-base-model preset registry --------------------------------------
_PRESETS: dict[str, BaseModel] = {}
def register_preset(name: str, preset: BaseModel) -> None:
"""Register a per-base-model preset (Pydantic model)."""
_PRESETS[name] = preset
def get_preset(name: str) -> BaseModel:
if name not in _PRESETS:
available = ", ".join(sorted(_PRESETS)) or "(none registered)"
msg = f"unknown base model {name!r}. registered: {available}"
raise KeyError(msg)
return _PRESETS[name]
def list_presets() -> list[str]:
return sorted(_PRESETS)
def chat_template_for(name: str) -> str:
"""Return the canonical chat-template name for a registered base model."""
preset = get_preset(name)
return str(getattr(preset, "chat_template", "hermes"))
class ModelRegistry:
"""Class facade over the module-level registry — preferred API per mindxtrain2.md."""
@staticmethod
def register(name: str) -> Callable[[Callable[..., Backend]], Callable[..., Backend]]:
return register_backend(name)
@staticmethod
def build(name: str, **kwargs: object) -> Backend:
return build_backend(name, **kwargs)
@staticmethod
def names() -> list[str]:
return list_backends()
@staticmethod
def register_preset(name: str, preset: BaseModel) -> None:
register_preset(name, preset)
@staticmethod
def preset(name: str) -> BaseModel:
return get_preset(name)
@staticmethod
def presets() -> list[str]:
return list_presets()
# Side-effect imports — register the built-in backends.
# Side-effect imports — register the built-in base-model presets.
from mindxtrain.models import deepseek_v32 as _deepseek_v32 # noqa: E402, F401
from mindxtrain.models import glm51 as _glm51 # noqa: E402, F401
from mindxtrain.models import mistral3 as _mistral3 # noqa: E402, F401
from mindxtrain.models import phi4_mini as _phi4_mini # noqa: E402, F401
from mindxtrain.models import qwen35 as _qwen35 # noqa: E402, F401
from mindxtrain.operator.backends import bankml as _bankml # noqa: E402, F401
from mindxtrain.operator.backends import ollama as _ollama # noqa: E402, F401
from mindxtrain.operator.backends import openai_compat as _openai_compat # noqa: E402, F401
from mindxtrain.operator.backends import vllm as _vllm # noqa: E402, F401
|