feat(s41): PORTFOLIO_MODEL_MAP override + placeholder fail-fast in resolve_model
This commit is contained in:
parent
0670b2a6d8
commit
eb552f854e
3 changed files with 97 additions and 8 deletions
|
|
@ -22,17 +22,40 @@ import json
|
|||
import os
|
||||
from enum import Enum
|
||||
from importlib.resources import files
|
||||
from typing import Protocol, runtime_checkable
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
from agent_framework import BaseChatClient
|
||||
from agent_framework_foundry import FoundryChatClient
|
||||
from agent_framework_openai import OpenAIChatCompletionClient
|
||||
|
||||
_MODEL_MAP_RESOURCE = "data/model_map.json"
|
||||
# S4.1 — out-of-tree model-map override so tenant-specific deployment names are never committed.
|
||||
_MODEL_MAP_ENV = "PORTFOLIO_MODEL_MAP"
|
||||
# S4.1 — placeholder sentinel (mirrors costsim.PLACEHOLDER_PREFIX); an azure deployment left as
|
||||
# ``REPLACE-WITH-*`` must never reach a client build.
|
||||
_PLACEHOLDER_PREFIX = "REPLACE-WITH-"
|
||||
# Loopback only — never a remote host (D6 / research 03 no-egress). Override via env.
|
||||
_DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:11434/v1"
|
||||
|
||||
|
||||
def _load_effective_map() -> dict[str, Any]:
|
||||
"""Load the role->model map (B12). ``PORTFOLIO_MODEL_MAP`` (an out-of-tree path) wins so
|
||||
tenant-specific deployment names are never committed; otherwise the bundled resource. This is
|
||||
the SINGLE source of truth shared by ``resolve_model`` and the S4.1 preflight — so the checker
|
||||
and the run path can never validate different maps. Fail-fast (``FileNotFoundError``) when the
|
||||
override path does not exist (mirror ``contracts.load_goal_config``)."""
|
||||
override = os.environ.get(_MODEL_MAP_ENV)
|
||||
if override:
|
||||
path = Path(override)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"model map not found: {override!r}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return json.loads(
|
||||
files("portfolio_optimiser").joinpath(_MODEL_MAP_RESOURCE).read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
|
||||
class Profile(str, Enum):
|
||||
"""Model-serving backend profile (D2)."""
|
||||
|
||||
|
|
@ -52,16 +75,22 @@ class ChatBackend(Protocol):
|
|||
|
||||
|
||||
def resolve_model(profile: Profile | str, role: str) -> str:
|
||||
"""Resolve a role -> model/deployment id from ``data/model_map.json`` (B12). Falls back
|
||||
to the profile's ``default``; fail-fast (``ValueError``) when nothing maps."""
|
||||
"""Resolve a role -> model/deployment id from the effective model map (B12), honoring
|
||||
``PORTFOLIO_MODEL_MAP``. Falls back to the profile's ``default``; fail-fast (``ValueError``)
|
||||
when nothing maps, OR when the resolved id is still an unreplaced ``REPLACE-WITH-*`` placeholder
|
||||
(S4.1 — never build a client from a placeholder; the guard reaches the run path at
|
||||
``run.py`` too, not just the preflight)."""
|
||||
prof = Profile(profile)
|
||||
table = json.loads(
|
||||
files("portfolio_optimiser").joinpath(_MODEL_MAP_RESOURCE).read_text(encoding="utf-8")
|
||||
)
|
||||
table = _load_effective_map()
|
||||
profile_map = table.get(prof.value, {})
|
||||
model = profile_map.get(role) or profile_map.get("default")
|
||||
if not model:
|
||||
raise ValueError(f"no model mapped for profile={prof.value} role={role!r}")
|
||||
if model.startswith(_PLACEHOLDER_PREFIX):
|
||||
raise ValueError(
|
||||
f"model map has unresolved placeholder for profile={prof.value} role={role!r}: "
|
||||
f"{model!r} — replace the placeholder or set PORTFOLIO_MODEL_MAP"
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue