Files
aiturk-hermes-ide/tests/hermes_cli/test_keyed_provider_credential_pool.py
T

217 lines
7.4 KiB
Python

"""Keyed ``providers.<key>`` entries must use the durable pool slug.
``hermes auth add b-ai`` stores keys under ``credential_pool.b-ai``. Runtime
used to look up ``custom:<display-name>`` (e.g. ``custom:b.ai`` from
``name: B.AI``), miss the pool, and send the ``no-key-required`` placeholder
to an auth-required endpoint (HTTP 401 Invalid api_key format).
"""
from __future__ import annotations
import json
import yaml
POOL_KEY = "sk-real-b-ai-pool-key-12345"
LEGACY_KEY = "sk-legacy-custom-b-ai-pool-key"
ENDPOINT = "https://api.b.ai/v1"
def _write_keyed_provider_home(tmp_path, monkeypatch, *, pool_id="b-ai", extra_config=None):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
config = {
"model": {"default": "b-ai-model", "provider": "b-ai"},
"providers": {
"b-ai": {
"name": "B.AI",
"base_url": ENDPOINT,
}
},
}
if extra_config:
config["providers"]["b-ai"].update(extra_config)
(hermes_home / "config.yaml").write_text(yaml.safe_dump(config), encoding="utf-8")
(hermes_home / "auth.json").write_text(
json.dumps(
{
"version": 1,
"providers": {},
"credential_pool": {
pool_id: [
{
"id": "k1",
"label": "primary",
"auth_type": "api_key",
"priority": 0,
"source": "manual",
"access_token": POOL_KEY if pool_id == "b-ai" else LEGACY_KEY,
}
]
},
}
),
encoding="utf-8",
)
return hermes_home
def test_get_named_custom_provider_exposes_provider_key_and_key_env(
tmp_path, monkeypatch
):
_write_keyed_provider_home(
tmp_path, monkeypatch, extra_config={"key_env": "B_AI_API_KEY"}
)
monkeypatch.setenv("B_AI_API_KEY", "sk-from-env-not-the-pool")
from hermes_cli.runtime_provider import _get_named_custom_provider
entry = _get_named_custom_provider("b-ai")
assert entry is not None
assert entry.get("provider_key") == "b-ai"
assert entry.get("key_env") == "B_AI_API_KEY"
assert entry.get("name") == "B.AI"
assert entry.get("base_url") == ENDPOINT
def test_keyed_provider_runtime_uses_durable_pool_slug(tmp_path, monkeypatch):
"""Main turns must send the pooled key, not the no-key-required placeholder."""
_write_keyed_provider_home(tmp_path, monkeypatch)
from hermes_cli import runtime_provider as rp
resolved = rp.resolve_runtime_provider(requested="b-ai")
assert resolved["base_url"] == ENDPOINT
assert resolved["api_key"] == POOL_KEY
assert resolved["api_key"] != "no-key-required"
assert str(resolved.get("source") or "").startswith("pool:")
def test_keyed_provider_runtime_falls_back_to_legacy_custom_namespace(
tmp_path, monkeypatch
):
"""Older auth.json rows stored under custom:<display-name> must still work."""
_write_keyed_provider_home(tmp_path, monkeypatch, pool_id="custom:b.ai")
from hermes_cli import runtime_provider as rp
resolved = rp.resolve_runtime_provider(requested="b-ai")
assert resolved["api_key"] == LEGACY_KEY
assert resolved["api_key"] != "no-key-required"
def _model_config_entry(entry_id, token):
return {
"id": entry_id,
"label": "model_config",
"auth_type": "api_key",
"priority": 0,
"source": "model_config",
"access_token": token,
}
def test_prune_keeps_active_legacy_pool_for_keyed_provider(tmp_path, monkeypatch):
"""A keyed provider's own legacy-named pool must not be false-pruned.
Regression: with keys stored under ``custom:b.ai`` while the provider is
configured as ``providers.b-ai``, the active pool key resolves to the
durable slug ``b-ai``; comparing with ``==`` let the prune strip the
provider's own current credential from ``custom:b.ai``.
"""
config = {
"model": {"default": "b-ai-model", "provider": "b-ai"},
"providers": {
"b-ai": {
"name": "B.AI",
"base_url": "https://api.b.ai/v1",
}
},
}
pools = {
# legacy-named pool for the (now keyed) b.ai provider holding the
# credential seeded from model.api_key — this is the ACTIVE pool
"custom:b.ai": [_model_config_entry("mc1", "sk-current-b-ai-key")],
# an unrelated stale pool that SHOULD be pruned
"custom:old-endpoint": [_model_config_entry("mc2", "sk-stale-key")],
}
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir(exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
(hermes_home / "config.yaml").write_text(yaml.safe_dump(config), encoding="utf-8")
(hermes_home / "auth.json").write_text(
json.dumps(
{
"version": 1,
"providers": {},
"credential_pool": pools,
}
),
encoding="utf-8",
)
from hermes_cli.model_setup_flows import (
_prune_replaced_custom_model_config_credentials,
)
_prune_replaced_custom_model_config_credentials(
"https://api.b.ai/v1", provider_name="B.AI"
)
after = json.loads(
(tmp_path / ".hermes" / "auth.json").read_text(encoding="utf-8")
)
kept = (after.get("credential_pool") or {}).get("custom:b.ai")
pruned = (after.get("credential_pool") or {}).get("custom:old-endpoint")
assert kept, "active provider's own legacy-named pool must keep its credential"
assert kept[0]["access_token"] == "sk-current-b-ai-key"
assert pruned == [], "stale pool for a different endpoint must be pruned"
def test_seed_custom_pool_matches_legacy_named_pool(tmp_path, monkeypatch):
"""A legacy-named pool must still seed from model.api_key.
Regression: with model.provider 'custom' pointing at a keyed provider's
base_url, the pool key ``custom:b.ai`` no longer equals the preferred
candidate (the slug ``b-ai``), silently skipping the model_config seed.
"""
config = {
"model": {
"default": "b-ai-model",
"provider": "custom",
"base_url": "https://api.b.ai/v1",
"api_key": "sk-model-config-key",
},
"providers": {
"b-ai": {
"name": "B.AI",
"base_url": "https://api.b.ai/v1",
}
},
}
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir(exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
(hermes_home / "config.yaml").write_text(yaml.safe_dump(config), encoding="utf-8")
(hermes_home / "auth.json").write_text(
json.dumps(
{
"version": 1,
"providers": {},
"credential_pool": {"custom:b.ai": []},
}
),
encoding="utf-8",
)
from agent.credential_pool import load_pool
pool = load_pool("custom:b.ai")
entries = pool.entries() if hasattr(pool, "entries") else []
seeded = [e for e in entries if getattr(e, "source", "") == "model_config"]
assert seeded, "legacy-named pool must still seed model_config from model.api_key"
assert getattr(seeded[0], "access_token", "") == "sk-model-config-key"