Files
hermes-agent/tests/hermes_cli/test_anthropic_pool_model_discovery.py
T
Teknium d26dbb2f77 Port from OpenHands/OpenHands#16758: Anthropic model catalogs no longer stop at the first page
Anthropic's /v1/models is cursor-paginated with a default page size of 20.
Both hermes fetchers read a single unpaginated page, so any model past the
first page silently vanished from the /model picker and provider catalogs.

- hermes_cli/models.py _fetch_anthropic_models(): request limit=1000 and
  follow has_more/last_id (bounded, repeated-cursor guarded, de-duped)
- plugins/model-providers/anthropic fetch_models(): same pagination walk,
  and it now honors the base_url argument instead of hardcoding
  api.anthropic.com
- tests: live-HTTP paginated-server regression tests for both fetchers,
  incl. single-page and stuck-cursor termination; updated the two URL-pinning
  pool-discovery tests for the ?limit=1000 contract
2026-09-13 21:07:35 -07:00

114 lines
3.4 KiB
Python

from __future__ import annotations
import json
from hermes_cli import models
class _Response:
def __init__(self, payload: dict):
self._payload = payload
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self) -> bytes:
return json.dumps(self._payload).encode()
def test_anthropic_picker_discovers_models_with_pool_api_key(monkeypatch):
"""A direct API key stored only in auth.json must reach /v1/models."""
monkeypatch.setattr(models, "_get_model_config_dict", lambda: {"provider": "nous"})
monkeypatch.setattr(
"agent.anthropic_credentials.resolve_anthropic_token",
lambda: None,
)
monkeypatch.setattr(
"hermes_cli.auth.read_credential_pool",
lambda provider: (
[
{
"auth_type": "api_key",
"access_token": "sk-ant-api03-pool-key",
"base_url": "https://api.anthropic.com",
}
]
if provider == "anthropic"
else []
),
)
captured = {}
def _open(request, *, timeout):
captured["url"] = request.full_url
captured["headers"] = {
key.lower(): value for key, value in request.header_items()
}
captured["timeout"] = timeout
return _Response({"data": [{"id": "claude-opus-5"}]})
monkeypatch.setattr(models, "_urlopen_model_catalog_request", _open)
result = models.provider_model_ids("anthropic")
assert "claude-opus-5" in result
assert captured["url"] == "https://api.anthropic.com/v1/models?limit=1000"
assert captured["headers"]["x-api-key"] == "sk-ant-api03-pool-key"
assert "authorization" not in captured["headers"]
def test_anthropic_pool_api_key_overrides_conflicting_active_endpoint(monkeypatch):
"""A pool-scoped key must never reach the active model's other endpoint."""
active_endpoint = "https://active.example/anthropic/v1"
pool_endpoint = "https://pool.example/anthropic/v1"
monkeypatch.setattr(
models,
"_get_model_config_dict",
lambda: {"provider": "anthropic", "base_url": active_endpoint},
)
monkeypatch.setattr(
"agent.anthropic_credentials.resolve_anthropic_token",
lambda: None,
)
monkeypatch.setattr(
"hermes_cli.auth.read_credential_pool",
lambda provider: (
[
{
"auth_type": "api_key",
"access_token": "proxy-key",
"base_url": pool_endpoint,
}
]
if provider == "anthropic"
else []
),
)
requests = []
def _open(request, *, timeout):
requests.append((
request.full_url,
{key.lower(): value for key, value in request.header_items()},
))
return _Response({"data": [{"id": "claude-proxy-model"}]})
monkeypatch.setattr(models, "_urlopen_model_catalog_request", _open)
assert models.provider_model_ids("anthropic") == ["claude-proxy-model"]
assert requests == [
(
f"{pool_endpoint}/models?limit=1000",
{
"anthropic-version": "2023-06-01",
"x-api-key": "proxy-key",
},
)
]
assert all(not url.startswith(active_endpoint) for url, _headers in requests)