refactor(ai): hoist Provider core to shared cmd_chat/ai/ (P1)

Move providers.py + profiles.py from cmd_chat/agent/ to a shared
cmd_chat/ai/ package so the operator bridge and the /ai chat agent can
consume one model-agnostic Provider core (groundwork for harness-mode
operators). Pure refactor — no behaviour change.

- cmd_chat/ai/{providers,profiles}.py: the canonical modules (moved verbatim)
- cmd_chat/ai/__init__.py: re-exports the public API
- cmd_chat/agent/{providers,profiles}.py: thin back-compat shims re-exporting
  from cmd_chat.ai (keeps `from cmd_chat.agent.providers import …` working,
  e.g. hh/scripts/bench-native-harness.py)
- internal agent consumers (memory/bridge/__main__/__init__) point at cmd_chat.ai

125 tests pass; shim identity verified (re-exports are the same objects).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-28 22:57:29 -07:00
parent 7fbae3ca84
commit 300e33980a
9 changed files with 673 additions and 601 deletions
+102
View File
@@ -0,0 +1,102 @@
"""Named model profiles for the hack-house AI agent.
A *profile* maps a friendly name (``groq-llama``, ``local``, ``claude``) to a
provider + model + endpoint, so operators type ``--profile groq-llama`` instead
of remembering ``--provider openai --base-url … --model …``. This mirrors the
``models:`` list in Continue.dev and the ``model_list`` in a LiteLLM proxy:
each entry is ``{provider, model, base_url, api_key_env}``.
Secrets are **never** stored here — ``api_key_env`` names an environment
variable to read the key from, keeping the file safe to commit and share.
Lookup order (first hit wins):
1. ``$HH_MODELS_FILE``
2. ``./models.toml`` (cwd)
3. ``~/.config/hh/models.toml``
"""
from __future__ import annotations
import os
from pathlib import Path
try: # stdlib on 3.11+, falls back to the `tomli` backport on 3.10
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib # type: ignore[no-redef]
from .providers import Provider, make_provider
_RECOGNIZED = {"provider", "model", "base_url", "host", "api_key_env",
"system", "context_window"}
def _candidate_paths(explicit: str | None) -> list[Path]:
if explicit:
return [Path(explicit).expanduser()]
paths = []
env = os.environ.get("HH_MODELS_FILE")
if env:
paths.append(Path(env).expanduser())
paths.append(Path.cwd() / "models.toml")
paths.append(Path.home() / ".config" / "hh" / "models.toml")
return paths
def find_profiles_file(explicit: str | None = None) -> Path | None:
for p in _candidate_paths(explicit):
if p.is_file():
return p
return None
def load_profiles(explicit: str | None = None) -> dict[str, dict]:
"""Return ``{name: profile_dict}`` from the first models.toml found."""
path = find_profiles_file(explicit)
if path is None:
return {}
with path.open("rb") as fh:
data = tomllib.load(fh)
profiles: dict[str, dict] = {}
for name, body in data.items():
if not isinstance(body, dict) or "provider" not in body:
continue # skip non-profile tables / malformed entries
unknown = set(body) - _RECOGNIZED
if unknown:
raise ValueError(
f"profile '{name}': unknown key(s) {', '.join(sorted(unknown))}"
)
profiles[name] = body
return profiles
def provider_from_profile(prof: dict, *, name: str = "?",
model: str | None = None,
base_url: str | None = None) -> Provider:
"""Build a :class:`Provider` from a profile dict.
``model`` / ``base_url`` (CLI flags) override the profile when given. The
api key is read from ``$<api_key_env>`` and passed only to providers that
accept one, so an Ollama profile never sees a stray ``api_key`` kwarg.
"""
spec = prof["provider"]
custom = ":" in spec
opts: dict = {}
mdl = model or prof.get("model")
bu = base_url or prof.get("base_url")
if bu and (spec == "openai" or custom):
opts["base_url"] = bu
if spec == "ollama" and prof.get("host"):
opts["host"] = prof["host"]
key_env = prof.get("api_key_env")
if key_env and (spec in ("openai", "anthropic") or custom):
key = os.environ.get(key_env)
if not key:
raise SystemExit(
f"profile '{name}': ${key_env} is not set — export it first"
)
opts["api_key"] = key
return make_provider(spec, model=mdl, **opts)