| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- """
- Extension plugin interface for GPUStack.
- Third-party or enterprise plugins can implement this interface
- and register via the ``gpustack.plugins`` entry-point group.
- A plugin is fully wired at construction time: the subclass's
- ``__init__(app, cfg)`` is expected to mount routers, install
- middleware, run migrations, and otherwise mutate ``app`` as needed.
- After construction the instance can optionally expose long-running
- background coroutines via ``async_tasks()`` and a distributed-mode
- coordinator via the ``coordinator`` attribute.
- """
- import logging
- from typing import Any, Coroutine, Generator, List, Optional, TYPE_CHECKING, Tuple
- from fastapi import FastAPI
- from gpustack.config.config import Config
- if TYPE_CHECKING:
- from gpustack.server.coordinator import Coordinator
- logger = logging.getLogger(__name__)
- def iter_plugin_classes() -> Generator[Tuple[str, type], None, None]:
- """Iterate over registered plugin classes via the ``gpustack.plugins``
- entry-point group.
- Used at CLI-parse time (before any FastAPI app exists) so plugins can
- contribute ``start`` command arguments via ``Plugin.setup_start_cmd``.
- At runtime the server instantiates plugins inside ``create_app`` and
- stores instances on ``app.state.extension_plugins``.
- """
- try:
- from importlib.metadata import entry_points
- for ep in entry_points(group="gpustack.plugins"):
- try:
- yield ep.name, ep.load()
- except Exception:
- logger.warning(f"Failed to load plugin class: {ep.name}", exc_info=True)
- except ImportError:
- pass
- class Plugin:
- """Base class that all extension plugins must implement.
- Subclasses override ``__init__(app, cfg)`` to perform the full
- registration — there is no separate ``register`` phase. To opt into
- distributed-mode coordination, an ``__init__`` may assign a
- ``Coordinator`` to ``self.coordinator``; the server picks it up from
- ``app.state.extension_plugins`` and owns its lifecycle.
- """
- # Optional distributed-mode coordinator; the server starts/stops it.
- coordinator: Optional["Coordinator"] = None
- def __init__(self, app: FastAPI, cfg: Config) -> None:
- pass
- def async_tasks(self) -> List[Coroutine[Any, Any, Any]]:
- """Long-running background coroutines to be scheduled alongside
- the API server. Each coroutine is awaited in the server's main
- gather(), so an uncaught exception aborts the server — plugins
- are expected to handle their own restart semantics. Default: no
- tasks."""
- return []
- @classmethod
- def setup_start_cmd(cls, parser) -> None:
- """Contribute arguments to the ``gpustack start`` argparse parser.
- Called at CLI-parse time, before ``Config`` is built and before
- any FastAPI app exists, so this must be a classmethod and must
- not depend on instance state.
- """
- pass
- @classmethod
- def contribute_config(cls, args, config_data: dict) -> None:
- """Forward plugin-contributed CLI args into the ``Config`` kwargs dict.
- Called after argparse parsing and core's ``set_*_options`` have
- populated ``config_data``, but before ``Config(**config_data)`` is
- constructed. Args registered via ``setup_start_cmd`` end up on the
- ``args`` namespace but are not automatically forwarded — override
- this method to copy the relevant fields. Because ``Config`` uses
- ``extra="allow"``, copied keys appear as attributes on ``cfg``.
- Plugins should not overwrite keys that core already set unless the
- intent is to override; this hook runs last and last-write-wins.
- """
- pass
|