🐾 fix: resolve registry capabilities per agent, not once globally
_build_registry built one capabilities dict from the global default_model and
attached it to every entry, so any agent with an `agents.<name>.model` or
`model_capabilities` override was advertised under the wrong model — even
though server.py applies those overrides at startup. Resolve capabilities per
agent, mirroring _register_unknown_models.
Also stop emitting null context_window / max_output_tokens when
model_capabilities is absent: _register_one_model registers the model with
131072 / 16384, so the registry now advertises those effective values. The
defaults are hoisted to module constants in server.py so the two cannot drift.
Verified against mentor's config: capabilities go from
{"context_window": null, "max_output_tokens": null} to {131072, 16384}, model
name unchanged. No checked-in deployment uses per-agent overrides today, so no
currently-published model changes.
Adds tests/test_registry.py (9 tests, first coverage for registry.py) and
documents agents.<name>.model / model_capabilities, which the agents.yaml
field table omitted entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,11 +22,13 @@ import yaml
|
||||
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from pallas.metrics import REGISTRY as _metrics_registry, set_agent_info
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, PlainTextResponse, Response
|
||||
from starlette.routing import Route
|
||||
|
||||
from pallas.metrics import REGISTRY as _metrics_registry, set_agent_info
|
||||
from pallas.server import DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_OUTPUT_TOKENS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -45,37 +47,53 @@ def _load_deployment_config() -> dict:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
|
||||
def _load_model_capabilities() -> dict:
|
||||
"""Read model info and capabilities from the active fastagent.config.yaml."""
|
||||
def _load_fastagent_defaults() -> tuple[str, dict]:
|
||||
"""Read ``(default_model, model_capabilities)`` from fastagent.config.yaml."""
|
||||
config_path = _config_root() / "fastagent.config.yaml"
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
return "", {}
|
||||
|
||||
with open(config_path) as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
|
||||
default_model = config.get("default_model", "")
|
||||
capabilities = config.get("model_capabilities", {})
|
||||
|
||||
if not default_model and not capabilities:
|
||||
return {}
|
||||
|
||||
model_name = (
|
||||
default_model.split(".", 1)[-1] if "." in default_model else default_model
|
||||
return (
|
||||
config.get("default_model", "") or "",
|
||||
config.get("model_capabilities", {}) or {},
|
||||
)
|
||||
|
||||
|
||||
def _resolve_capabilities(
|
||||
agent: dict, default_model: str, default_capabilities: dict
|
||||
) -> dict | None:
|
||||
"""Resolve the capabilities Pallas actually registered for one agent.
|
||||
|
||||
Mirrors ``server._register_unknown_models``: an agent's own ``model``
|
||||
in agents.yaml wins over the global ``default_model``, its own
|
||||
``model_capabilities`` block wins over the global one, and the same
|
||||
defaults apply — so the registry advertises the effective values rather
|
||||
than nulls. Returns ``None`` when no model is known for this agent.
|
||||
"""
|
||||
model_spec = agent.get("model") or default_model
|
||||
if not model_spec:
|
||||
return None
|
||||
|
||||
capabilities = agent.get("model_capabilities") or default_capabilities
|
||||
model_name = model_spec.split(".", 1)[-1] if "." in model_spec else model_spec
|
||||
|
||||
return {
|
||||
"model": model_name or None,
|
||||
"model": model_name,
|
||||
"vision": capabilities.get("vision", False),
|
||||
"context_window": capabilities.get("context_window", None),
|
||||
"max_output_tokens": capabilities.get("max_output_tokens", None),
|
||||
"context_window": capabilities.get("context_window", DEFAULT_CONTEXT_WINDOW),
|
||||
"max_output_tokens": capabilities.get(
|
||||
"max_output_tokens", DEFAULT_MAX_OUTPUT_TOKENS
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _build_registry(config: dict) -> dict:
|
||||
"""Build the registry JSON from agents.yaml + fastagent.config.yaml."""
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
model_caps = _load_model_capabilities()
|
||||
default_model, default_capabilities = _load_fastagent_defaults()
|
||||
|
||||
host = config.get("host", "localhost")
|
||||
namespace = config.get("namespace", "")
|
||||
@@ -101,8 +119,9 @@ def _build_registry(config: dict) -> dict:
|
||||
}
|
||||
],
|
||||
}
|
||||
if model_caps:
|
||||
server_entry["capabilities"] = model_caps
|
||||
capabilities = _resolve_capabilities(agent, default_model, default_capabilities)
|
||||
if capabilities:
|
||||
server_entry["capabilities"] = capabilities
|
||||
|
||||
entries.append(
|
||||
{
|
||||
|
||||
@@ -24,6 +24,12 @@ from pallas.multimodal_server import MultimodalAgentMCPServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Effective model capability defaults, applied when `model_capabilities` omits
|
||||
# them. registry.py mirrors these so the published registry advertises what
|
||||
# was actually registered with fast-agent's ModelDatabase.
|
||||
DEFAULT_CONTEXT_WINDOW = 131072
|
||||
DEFAULT_MAX_OUTPUT_TOKENS = 16384
|
||||
|
||||
|
||||
def _config_root() -> Path:
|
||||
"""Return the working directory where agents.yaml and fastagent configs live."""
|
||||
@@ -143,8 +149,8 @@ def _register_one_model(model_spec: str, capabilities: dict) -> None:
|
||||
return
|
||||
|
||||
is_vision = capabilities.get("vision", False)
|
||||
context_window = capabilities.get("context_window", 131072)
|
||||
max_output_tokens = capabilities.get("max_output_tokens", 16384)
|
||||
context_window = capabilities.get("context_window", DEFAULT_CONTEXT_WINDOW)
|
||||
max_output_tokens = capabilities.get("max_output_tokens", DEFAULT_MAX_OUTPUT_TOKENS)
|
||||
|
||||
if is_vision:
|
||||
tokenizes = list(ModelDatabase.QWEN_MULTIMODAL)
|
||||
|
||||
Reference in New Issue
Block a user