Files
beaver-gateway/src/beaver_gateway/frontends/base.py
T

90 lines
3.7 KiB
Python

"""Frontend ABC + the runtime context handed to ``configure``.
A frontend is anything that listens on a port and routes inbound traffic
into the gateway. ``GatewayRuntime`` carries everything a frontend may
need that isn't user-config: built registries, per-agent backends, and
the in-memory token store. The user's ``/config/config.py`` defines a
``Gateway`` (lists); ``cli.main`` turns that into a ``GatewayRuntime``
and hands it to each frontend's ``configure``.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable, Mapping, Sequence
from beaver_gateway.backends.base import Backend
from beaver_gateway.core.auth import TokenStore
from beaver_gateway.core.registry import AgentRegistry, McpRegistry
from beaver_gateway.core.turn_record import TurnRecord
from beaver_gateway.storage import Database
TurnLogHandler = Callable[[TurnRecord], Awaitable[None]]
@dataclass(frozen=True, slots=True)
class GatewayRuntime:
"""Post-build state of the gateway, shared with every frontend.
Backends are keyed by **agent name**, not type — one ``RaycastBackend``
instance can serve many ``RaycastAgent`` instances, but the lookup
site (an inbound request with ``model=<agent.name>``) already has
the name in hand, so the indirection lives one step earlier.
``mcp_internal_urls`` is filled in Phase 2.1: one loopback URL per
declared ``McpServer`` so ``ClaudeSdkBackend``
can pass them to ``BackendOptions.mcp_servers`` without re-running
discovery.
``db`` (Phase 4.1) is the shared :class:`Database` handle. Phase 4.2
will switch ``TokenStore`` to read from it; Phase 4.3 admin/audit
write through it. Phase 4.1 only attaches it — existing frontends
ignore it.
"""
agents: AgentRegistry
mcps: McpRegistry
backends: dict[str, Backend]
token_store: TokenStore
db: Database
mcp_internal_urls: Mapping[str, str] = field(default_factory=dict)
# Phase 4.3 — AdminFrontend reads creds + cookie-signing key from
# the runtime so the user's ``config.py`` doesn't have to know
# anything about env wiring. Defaulted to empty so existing tests /
# call sites that don't touch the admin path keep building; the
# admin frontend ``configure()`` itself rejects empty values.
admin_user: str = ""
admin_pass: str = ""
session_secret: str = ""
# The full sibling-frontends list, in declaration order. AdminFrontend
# uses it to advertise concrete bearer-endpoint URLs (host/port) on
# the dashboard so the operator can copy ready-to-use links / curl
# snippets. Other frontends ignore it.
frontends: Sequence[Frontend] = field(default_factory=tuple)
# Frontends that finish a turn (Anthropic Messages, Markdown) iterate
# this list and ``await`` each handler with a ``TurnRecord``. Handlers
# are appended during ``configure()`` by frontends that want a
# cross-frontend chat archive — currently the markdown frontend's
# ``log_all_chats`` mode. Handler exceptions are caught at the call
# site; they never block the user-visible response.
#
# The field is typed as ``list[Any]`` rather than the precise
# ``list[TurnLogHandler]`` because the alias lives under TYPE_CHECKING
# to keep ``anthropic.types`` out of the runtime import graph for
# this base module.
turn_log_handlers: list[TurnLogHandler] = field(default_factory=list)
class Frontend(ABC):
"""Listens on a port, dispatches into the gateway."""
@abstractmethod
def configure(self, runtime: GatewayRuntime) -> None: ...
@abstractmethod
async def serve(self) -> None: ...