simplify(compat): anthropic_adapter — drop 30 re-exports + 1 alias, repoint 22 caller files (32 sites), 38 test files (~125 sites)

This commit is contained in:
Teknium
2026-09-03 13:16:47 -07:00
parent 2031c819fe
commit d179f28307
60 changed files with 176 additions and 231 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional
import httpx
from agent.anthropic_adapter import _is_oauth_token, resolve_anthropic_token
from agent.anthropic_credentials import _is_oauth_token, resolve_anthropic_token
from hermes_cli.auth import AuthError, _read_codex_tokens, resolve_codex_runtime_credentials
from hermes_cli.runtime_provider import resolve_runtime_provider
+3 -2
View File
@@ -723,7 +723,8 @@ def _print_key_banner(key, label: str, warn_missing: bool = False) -> None:
def _init_anthropic_client(agent, api_key, base_url, _provider_timeout):
"""anthropic_messages: native Anthropic SDK (or AnthropicBedrock for Bedrock+Claude)."""
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
from agent.anthropic_adapter import build_anthropic_client
from agent.anthropic_credentials import resolve_anthropic_token
agent.client = None
agent._client_kwargs = {}
agent._anthropic_base_url = base_url
@@ -766,7 +767,7 @@ def _init_anthropic_client(agent, api_key, base_url, _provider_timeout):
# Third-party providers (MiniMax, Kimi, GLM, LiteLLM proxies) that accept the Anthropic protocol must
# never trip OAuth code paths — doing so injects Claude-Code identity headers and system prompts that
# cause 401/403 on their endpoints. See #1739.
from agent.anthropic_adapter import _is_oauth_token as _is_oat
from agent.anthropic_credentials import _is_oauth_token as _is_oat
agent._is_anthropic_oauth = _is_oat(effective_key) if (_is_native_anthropic and isinstance(effective_key, str)) else False
agent._anthropic_client = build_anthropic_client(effective_key, base_url, timeout=_provider_timeout)
if not agent.quiet_mode:
+3 -2
View File
@@ -1499,7 +1499,7 @@ def anthropic_prompt_cache_policy(
# 64K-token prompts and re-billing the full prompt on every turn. Observed within-turn progression with
# cache enabled: 1% → 67% → 84% → 97% (#25970). Reuses the canonical family matcher (covers bare
# k1./k2./k25 release slugs the substring check missed).
from agent.anthropic_adapter import _model_name_is_kimi_family
from agent.anthropic_endpoints import _model_name_is_kimi_family
is_kimi = _model_name_is_kimi_family(eff_model) or "moonshot" in model_lower
is_openrouter = base_url_host_matches(eff_base_url, "openrouter.ai")
# Nous Portal proxies to OpenRouter; treat as OpenRouter-equivalent for cache layout.
@@ -1859,7 +1859,8 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
agent.client = build_moa_facade(agent, agent.model)
return
if api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
from agent.anthropic_adapter import build_anthropic_client
from agent.anthropic_credentials import resolve_anthropic_token, _is_oauth_token
# Only fall back to ANTHROPIC_TOKEN for native Anthropic; other anthropic_messages providers
# must never receive Anthropic credentials.
is_native_anthropic = new_provider == "anthropic"
+5 -17
View File
@@ -2,38 +2,26 @@
OpenAI-style internals. Auth: API keys (``sk-ant-api*``) -> x-api-key; OAuth setup-tokens
(``sk-ant-oat*``) and Claude Code credentials -> Bearer + beta header. Endpoint predicates,
payload conversion and credentials live in ``agent/anthropic_{endpoints,message_convert,
credentials}.py`` and are re-exported here for long-standing imports."""
credentials}.py``; import them from there."""
import logging
import math
import re
import subprocess
from contextlib import suppress
from pathlib import Path # noqa: F401 (tests patch ``anthropic_adapter.Path.home``)
from typing import Any, Dict, List, Optional
from utils import normalize_proxy_env_vars
from agent.anthropic_endpoints import ( # noqa: F401
from agent.anthropic_credentials import _is_oauth_token
from agent.anthropic_endpoints import (
_base_url_needs_context_1m_beta, _is_azure_anthropic_endpoint, _is_kimi_coding_endpoint,
_is_minimax_anthropic_endpoint, _is_nous_portal_endpoint, _is_opencode_endpoint,
_is_third_party_anthropic_endpoint, _model_name_is_kimi_family, _normalize_base_url_text,
_requires_bearer_auth,
)
from agent.anthropic_message_convert import ( # noqa: F401
_EMPTY_TEXT_PLACEHOLDER, _convert_assistant_message, _convert_content_part_to_anthropic,
_convert_user_message, _ensure_leading_user_turn, _is_bedrock_model_id, _safe_text,
_sanitize_replay_block, _scrub_blank_text_blocks, _to_plain_data, convert_messages_to_anthropic,
convert_tools_to_anthropic, normalize_model_name,
)
from agent.anthropic_credentials import ( # noqa: F401
_OAUTH_TOKEN_USER_AGENT, CredentialPersistError, _get_hermes_oauth_file, _getenv, _is_oauth_token,
_read_claude_code_credentials_from_keychain, _refresh_oauth_token, _resolve_anthropic_pool_token,
_resolve_claude_code_token_from_credentials, _write_claude_code_credentials,
_write_hermes_oauth_credentials, claude_code_credentials_path, is_claude_code_token_valid,
is_rotation_consumed_uncommitted, mark_rotation_consumed_uncommitted, read_claude_code_credentials,
read_hermes_oauth_credentials, refresh_anthropic_oauth_pure, resolve_anthropic_token,
run_hermes_oauth_login_pure, run_oauth_setup_token,
from agent.anthropic_message_convert import (
convert_messages_to_anthropic, convert_tools_to_anthropic, normalize_model_name,
)
from hermes_cli import __version__ as _HERMES_VERSION
+1 -4
View File
@@ -5,8 +5,7 @@
``auth.json`` credential pool. ``~/.hermes/.anthropic_oauth.json`` (Hermes PKCE) and
the Claude Code file are *singletons*: ``credential_pool._seed_from_singletons()``
re-reads them on every ``load_pool()``, so a failed write here is a failed refresh
(``CredentialPersistError``), not a cache miss. ``agent.anthropic_adapter`` re-exports
every public name below.
(``CredentialPersistError``), not a cache miss.
"""
import base64
@@ -33,11 +32,9 @@ logger = logging.getLogger(__name__)
_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
# platform.claude.com is the live token host; console.anthropic.com 404s but is kept as a fallback.
# _OAUTH_TOKEN_URL stays for backward-compatible imports.
_OAUTH_TOKEN_URLS = [
"https://platform.claude.com/v1/oauth/token", "https://console.anthropic.com/v1/oauth/token"
]
_OAUTH_TOKEN_URL = _OAUTH_TOKEN_URLS[0]
# Anthropic 429s token-endpoint requests whose UA starts with ``claude-code/`` (or Mozilla); the real CLI uses
# bare axios there. Inference (build_anthropic_kwargs) still needs claude-code/.
_OAUTH_TOKEN_USER_AGENT = "axios/1.7.9"
+1 -1
View File
@@ -5,7 +5,7 @@ headers, and request quirks (MiniMax, Kimi/Moonshot, DeepSeek, OpenCode, Azure A
Portal, Bedrock). Every such difference is decided from the configured base URL, so the
predicates live together here as pure functions (no I/O, SDK or credentials) that both
``agent/anthropic_adapter.py`` and ``agent/anthropic_message_convert.py`` can import without a
cycle. ``agent.anthropic_adapter`` re-exports every name below.
cycle.
"""
from urllib.parse import urlparse
+1 -1
View File
@@ -2,7 +2,7 @@
schemas, and the message list (content blocks, thinking blocks and their signatures,
tool_use/tool_result pairing, cache_control placement, screenshot eviction, blank-block
scrubbing). Endpoint predicates come from ``agent/anthropic_endpoints.py`` so this module never
imports the adapter (no cycle); ``agent.anthropic_adapter`` re-exports the public names."""
imports the adapter (no cycle)."""
import copy
import json
+4 -3
View File
@@ -1600,7 +1600,7 @@ class _AnthropicCompletionsAdapter:
candidate = str(getattr(real_client, "base_url", "") or "") or None
if candidate:
with contextlib.suppress(Exception):
from agent.anthropic_adapter import _is_nous_portal_endpoint
from agent.anthropic_endpoints import _is_nous_portal_endpoint
if _is_nous_portal_endpoint(candidate):
self._base_url = candidate
@@ -2810,7 +2810,8 @@ def _try_azure_foundry(
def _try_anthropic(explicit_api_key: str = None) -> Tuple[Optional[Any], Optional[str]]:
try:
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
from agent.anthropic_adapter import build_anthropic_client
from agent.anthropic_credentials import resolve_anthropic_token
except ImportError:
return None, None
pool_present, entry = _select_pool_entry("anthropic")
@@ -2835,7 +2836,7 @@ def _try_anthropic(explicit_api_key: str = None) -> Tuple[Optional[Any], Optiona
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
if cfg_base_url and _is_anthropic_compatible_host(cfg_base_url):
base_url = cfg_base_url
from agent.anthropic_adapter import _is_oauth_token
from agent.anthropic_credentials import _is_oauth_token
is_oauth = _is_oauth_token(token)
model = _get_aux_model_for_provider("anthropic") or "claude-haiku-4-5-20251001"
if _aux_probe_active():
+2 -1
View File
@@ -1922,7 +1922,8 @@ def _swap_fallback_clients(agent, fb_client, fb_provider: str, fb_model: str, fb
"""Install the fallback client(s) in place, honoring request_timeout_seconds (None = SDK default)."""
timeout = get_provider_request_timeout(fb_provider, fb_model)
if fb_api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
from agent.anthropic_adapter import build_anthropic_client
from agent.anthropic_credentials import resolve_anthropic_token, _is_oauth_token
is_anthropic = fb_provider == "anthropic"
effective_key = fb_client.api_key or (resolve_anthropic_token() if is_anthropic else None) or ""
agent.api_key = agent._anthropic_api_key = effective_key
+2 -2
View File
@@ -420,7 +420,7 @@ class ClientLifecycleMixin:
def _anthropic_oauth_flag(self, token: str) -> bool:
"""OAuth flag only on native Anthropic; third-party Anthropic-protocol endpoints must not trip OAuth paths."""
from agent.anthropic_adapter import _is_oauth_token
from agent.anthropic_credentials import _is_oauth_token
return _is_oauth_token(token) if self.provider == "anthropic" else False
def _build_anthropic_client_for_key(self, key: tuple) -> Any:
@@ -752,7 +752,7 @@ class ClientLifecycleMixin:
):
return False
try:
from agent.anthropic_adapter import resolve_anthropic_token
from agent.anthropic_credentials import resolve_anthropic_token
new_token = resolve_anthropic_token()
except Exception as exc:
logger.debug("Anthropic credential refresh failed: %s", exc)
+3 -3
View File
@@ -44,12 +44,12 @@ class AnthropicTransport(ProviderTransport):
def convert_messages(self, messages: List[Dict[str, Any]], **kwargs) -> Any:
"""Convert OpenAI messages to an Anthropic (system, messages) tuple; ``base_url`` affects thinking-signature handling."""
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
return convert_messages_to_anthropic(messages, base_url=kwargs.get("base_url"))
def convert_tools(self, tools: List[Dict[str, Any]]) -> Any:
"""Convert OpenAI tool schemas to Anthropic input_schema format."""
from agent.anthropic_adapter import convert_tools_to_anthropic
from agent.anthropic_message_convert import convert_tools_to_anthropic
return convert_tools_to_anthropic(tools)
def build_kwargs(
@@ -65,7 +65,7 @@ class AnthropicTransport(ProviderTransport):
def normalize_response(self, response: Any, **kwargs) -> NormalizedResponse:
"""Parse content blocks (text/thinking/tool_use), map stop_reason, collect reasoning_details."""
import json
from agent.anthropic_adapter import _sanitize_replay_block, _to_plain_data
from agent.anthropic_message_convert import _sanitize_replay_block, _to_plain_data
strip_tool_prefix = kwargs.get("strip_tool_prefix", False)
text_parts, reasoning_parts, reasoning_details, tool_calls = [], [], [], []
# Anthropic signs each thinking block against the blocks PRECEDING it; when thinking
+1 -1
View File
@@ -179,7 +179,7 @@ def _format_exhausted_status(entry) -> str:
def _anthropic_oauth_login(args) -> dict:
from agent import anthropic_adapter as anthropic_mod
from agent import anthropic_credentials as anthropic_mod
creds = anthropic_mod.run_hermes_oauth_login_pure()
if not creds:
raise SystemExit("Anthropic OAuth login did not return credentials.")
+2 -1
View File
@@ -154,7 +154,8 @@ def _probe_anthropic() -> ProbeResult:
return _skip(name)
try:
import httpx
from agent.anthropic_adapter import _is_oauth_token, _COMMON_BETAS, _OAUTH_ONLY_BETAS, _CONTEXT_1M_BETA
from agent.anthropic_adapter import _COMMON_BETAS, _OAUTH_ONLY_BETAS, _CONTEXT_1M_BETA
from agent.anthropic_credentials import _is_oauth_token
is_oauth = _is_oauth_token(key)
headers = {"anthropic-version": "2023-06-01", **({"Authorization": f"Bearer {key}", "anthropic-beta": ",".join(_COMMON_BETAS + _OAUTH_ONLY_BETAS)}
if is_oauth else {"x-api-key": key})}
+1 -1
View File
@@ -439,7 +439,7 @@ def _anthropic_oauth_credentials_present() -> bool:
"""True when the user explicitly authenticated Anthropic via OAuth (Hermes device flow or Claude Code
login) — those leave no trace in active_provider / model.provider / API-key env vars."""
try:
from agent.anthropic_adapter import read_claude_code_credentials, read_hermes_oauth_credentials
from agent.anthropic_credentials import read_claude_code_credentials, read_hermes_oauth_credentials
readers = (read_hermes_oauth_credentials, read_claude_code_credentials)
if any((read() or {}).get("accessToken") for read in readers):
+1 -4
View File
@@ -1164,10 +1164,7 @@ def _has_any_provider_configured(*, strict_profile_scope: bool = False) -> bool:
# configured — having Claude Code installed isn't consent to use its tokens.
if _has_hermes_config and not strict_profile_scope:
try:
from agent.anthropic_adapter import (
read_claude_code_credentials,
is_claude_code_token_valid,
)
from agent.anthropic_credentials import read_claude_code_credentials, is_claude_code_token_valid
creds = read_claude_code_credentials()
if creds and (
+1 -1
View File
@@ -644,7 +644,7 @@ def _stepfun_base_url_for_region(region: str) -> str:
def _run_anthropic_oauth_flow(save_env_value):
"""Run the Claude OAuth setup-token flow. Returns True if credentials were saved."""
from agent.anthropic_adapter import run_oauth_setup_token, read_claude_code_credentials, is_claude_code_token_valid
from agent.anthropic_credentials import run_oauth_setup_token, read_claude_code_credentials, is_claude_code_token_valid
from hermes_cli.config import save_anthropic_oauth_token, use_anthropic_claude_code_credentials
def _activate_claude_code_credentials_if_available() -> bool:
+1 -1
View File
@@ -999,7 +999,7 @@ def _model_flow_anthropic(config, current_model=""):
existing_key = get_anthropic_key()
cc_available = False
with contextlib.suppress(Exception):
from agent.anthropic_adapter import read_claude_code_credentials, is_claude_code_token_valid, _is_oauth_token
from agent.anthropic_credentials import read_claude_code_credentials, is_claude_code_token_valid, _is_oauth_token
cc_creds = read_claude_code_credentials()
if cc_creds and is_claude_code_token_valid(cc_creds):
cc_available = True
+1 -1
View File
@@ -747,7 +747,7 @@ def _overlay_has_creds(b: _PickerBuild, pid: str, hermes_slug: str, overlay) ->
# The pool gates anthropic behind is_provider_explicitly_configured() (aux tasks must not
# consume Claude Code tokens); the picker is discovery-oriented, so read the files directly.
try:
from agent.anthropic_adapter import read_claude_code_credentials, read_hermes_oauth_credentials
from agent.anthropic_credentials import read_claude_code_credentials, read_hermes_oauth_credentials
hermes_creds = read_hermes_oauth_credentials()
cc_creds = read_claude_code_credentials()
if (hermes_creds and hermes_creds.get("accessToken")) or (cc_creds and cc_creds.get("accessToken")):
+1 -1
View File
@@ -1668,7 +1668,7 @@ def _fetch_anthropic_models(
``api_key``, else ``resolve_anthropic_token()`` (env / OAuth / Claude Code), else a read-only
API-key credential_pool entry."""
try:
from agent.anthropic_adapter import resolve_anthropic_token, _is_oauth_token
from agent.anthropic_credentials import resolve_anthropic_token, _is_oauth_token
except ImportError:
return None
+1 -1
View File
@@ -273,7 +273,7 @@ def _anthropic_cfg_base_url(model_cfg: Dict[str, Any]) -> str:
def _anthropic_token_or_raise() -> str:
from agent.anthropic_adapter import resolve_anthropic_token
from agent.anthropic_credentials import resolve_anthropic_token
token = resolve_anthropic_token()
if not token:
raise AuthError(_NO_ANTHROPIC_CREDENTIALS_MSG)
+1 -1
View File
@@ -528,7 +528,7 @@ def _clear_anthropic_auth() -> bool:
"""Clear only the Hermes-managed PKCE file and auth-store entry (never ~/.claude/*)."""
cleared = False
try:
from agent.anthropic_adapter import _get_hermes_oauth_file
from agent.anthropic_credentials import _get_hermes_oauth_file
oauth_file = _get_hermes_oauth_file()
if oauth_file.exists():
oauth_file.unlink()
+2 -2
View File
@@ -51,7 +51,7 @@ def _anthropic_oauth_status() -> Dict[str, Any]:
``claude-code`` entry, and counting it here would shadow a real ANTHROPIC_API_KEY.
"""
try:
from agent.anthropic_adapter import read_hermes_oauth_credentials, _get_hermes_oauth_file
from agent.anthropic_credentials import read_hermes_oauth_credentials, _get_hermes_oauth_file
hermes_creds = read_hermes_oauth_credentials()
except Exception:
hermes_creds = None
@@ -79,7 +79,7 @@ def _anthropic_oauth_status() -> Dict[str, Any]:
def _claude_code_only_status() -> Dict[str, Any]:
"""Claude Code CLI credentials as their own entry, independent of the Anthropic card."""
try:
from agent.anthropic_adapter import read_claude_code_credentials
from agent.anthropic_credentials import read_claude_code_credentials
creds = read_claude_code_credentials()
except Exception:
creds = None
+33 -48
View File
@@ -9,23 +9,10 @@ from unittest.mock import patch, MagicMock
import pytest
from agent.prompt_caching import apply_anthropic_cache_control
from agent.anthropic_adapter import (
_is_azure_anthropic_endpoint,
_is_oauth_token,
_refresh_oauth_token,
_to_plain_data,
_write_claude_code_credentials,
build_anthropic_client,
build_anthropic_bedrock_client,
build_anthropic_kwargs,
convert_messages_to_anthropic,
convert_tools_to_anthropic,
is_claude_code_token_valid,
normalize_model_name,
read_claude_code_credentials,
resolve_anthropic_token,
run_oauth_setup_token,
)
from agent.anthropic_adapter import build_anthropic_client, build_anthropic_bedrock_client, build_anthropic_kwargs
from agent.anthropic_credentials import _is_oauth_token, _refresh_oauth_token, _write_claude_code_credentials, is_claude_code_token_valid, read_claude_code_credentials, resolve_anthropic_token, run_oauth_setup_token
from agent.anthropic_endpoints import _is_azure_anthropic_endpoint
from agent.anthropic_message_convert import _to_plain_data, convert_messages_to_anthropic, convert_tools_to_anthropic, normalize_model_name
from agent.transports import get_transport
@@ -169,7 +156,7 @@ class TestReadClaudeCodeCredentials:
"expiresAt": int(time.time() * 1000) + 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
creds = read_claude_code_credentials()
assert creds is not None
assert creds["accessToken"] == "sk-ant-oat01-token"
@@ -179,7 +166,7 @@ class TestReadClaudeCodeCredentials:
def test_ignores_primary_api_key_for_native_anthropic_resolution(self, tmp_path, monkeypatch):
claude_json = tmp_path / ".claude.json"
claude_json.write_text(json.dumps({"primaryApiKey": "sk-ant-api03-primary"}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
creds = read_claude_code_credentials()
assert creds is None
@@ -210,7 +197,7 @@ class TestResolveAnthropicToken:
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-mykey")
monkeypatch.setenv("ANTHROPIC_TOKEN", "sk-ant-oat01-mytoken")
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
assert resolve_anthropic_token() == "sk-ant-oat01-mytoken"
def test_does_not_resolve_primary_api_key_as_native_anthropic_token(self, monkeypatch, tmp_path):
@@ -218,7 +205,7 @@ class TestResolveAnthropicToken:
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
(tmp_path / ".claude.json").write_text(json.dumps({"primaryApiKey": "sk-ant-api03-primary"}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
assert resolve_anthropic_token() is None
@@ -226,7 +213,7 @@ class TestResolveAnthropicToken:
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant...ykey")
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
assert resolve_anthropic_token() == "sk-ant...ykey"
def test_api_key_wins_over_auto_discovered_claude_code_credentials(
@@ -244,7 +231,7 @@ class TestResolveAnthropicToken:
"expiresAt": int(time.time() * 1000) + 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
assert resolve_anthropic_token() == "sk-ant...ykey"
@@ -272,14 +259,14 @@ class TestResolveAnthropicToken:
"expiresAt": int(time.time() * 1000) + 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
assert resolve_anthropic_token() == "cc-auto-token"
def test_falls_back_to_anthropic_credential_pool_oauth(self, monkeypatch, tmp_path):
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
# Isolate source #5 (credential_pool): ensure source #4 (Claude Code
# creds, incl. the macOS keychain read which Path.home does not cover)
# returns nothing, mirroring a Hermes-PKCE-only setup.
@@ -300,7 +287,7 @@ class TestResolveAnthropicToken:
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant...ykey")
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
monkeypatch.setattr(
"agent.anthropic_credentials.read_claude_code_credentials",
self._assert_not_called,
@@ -320,7 +307,7 @@ class TestResolveAnthropicToken:
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant...ykey")
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.read_claude_code_credentials", lambda: None)
broken_entry = SimpleNamespace(auth_type="oauth", access_token=None)
@@ -340,7 +327,7 @@ class TestResolveAnthropicToken:
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.read_claude_code_credentials", lambda: None)
api_key_entry = SimpleNamespace(auth_type="api_key", access_token="sk-pool-apikey")
@@ -360,7 +347,7 @@ class TestResolveAnthropicToken:
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.read_claude_code_credentials", lambda: None)
captured = {}
@@ -389,7 +376,7 @@ class TestResolveAnthropicToken:
"expiresAt": int(time.time() * 1000) + 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
assert resolve_anthropic_token() == "cc-auto-token"
@@ -397,7 +384,7 @@ class TestResolveAnthropicToken:
class TestRefreshOauthToken:
def test_returns_none_without_refresh_token(self, tmp_path, monkeypatch):
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
# Neutralize live Claude Code sources (macOS Keychain + ~/.claude file)
# so the adopt-already-refreshed branch can't short-circuit with a real
# credential on a dev/CI machine that happens to have Claude Code creds.
@@ -408,7 +395,7 @@ class TestRefreshOauthToken:
assert _refresh_oauth_token(creds) is None
def test_successful_refresh(self, tmp_path, monkeypatch):
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
monkeypatch.setattr(
"agent.anthropic_credentials.read_claude_code_credentials", lambda: None
)
@@ -444,7 +431,7 @@ class TestRefreshOauthToken:
assert written["claudeAiOauth"]["refreshToken"] == "new-refresh-456"
def test_failed_refresh_returns_none(self, tmp_path, monkeypatch):
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
monkeypatch.setattr(
"agent.anthropic_credentials.read_claude_code_credentials", lambda: None
)
@@ -460,7 +447,7 @@ class TestRefreshOauthToken:
class TestWriteClaudeCodeCredentials:
def test_writes_new_file(self, tmp_path, monkeypatch):
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
_write_claude_code_credentials("tok", "ref", 12345)
cred_file = tmp_path / ".claude" / ".credentials.json"
assert cred_file.exists()
@@ -470,7 +457,7 @@ class TestWriteClaudeCodeCredentials:
assert data["claudeAiOauth"]["expiresAt"] == 12345
def test_preserves_existing_fields(self, tmp_path, monkeypatch):
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
cred_dir = tmp_path / ".claude"
cred_dir.mkdir()
cred_file = cred_dir / ".credentials.json"
@@ -490,7 +477,7 @@ class TestWriteClaudeCodeCredentials:
the fix shipped in #19673 (google_oauth) and #21148 (mcp_oauth).
"""
import stat as _stat
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
_write_claude_code_credentials("tok", "ref", 12345)
cred_file = tmp_path / ".claude" / ".credentials.json"
@@ -516,7 +503,7 @@ class TestResolveWithRefresh:
"expiresAt": int(time.time() * 1000) - 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
# Mock refresh to succeed
with patch("agent.anthropic_credentials._refresh_oauth_token", return_value="refreshed-token"):
@@ -538,7 +525,7 @@ class TestResolveWithRefresh:
"expiresAt": int(time.time() * 1000) - 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
with patch("agent.anthropic_credentials._refresh_oauth_token", return_value="refreshed-token"):
result = resolve_anthropic_token()
@@ -564,7 +551,7 @@ class TestRunOauthSetupToken:
"expiresAt": int(time.time() * 1000) + 3600_000,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
@@ -583,7 +570,7 @@ class TestRunOauthSetupToken:
monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/claude")
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
@@ -1053,10 +1040,8 @@ class TestBuildAnthropicKwargs:
classified as Kimi family (adaptive thinking) even on proxied
endpoints where only the model name is available. Lookalike
non-Kimi names must NOT match the exact-slug rule."""
from agent.anthropic_adapter import (
_model_name_is_kimi_family,
_supports_adaptive_thinking,
)
from agent.anthropic_adapter import _supports_adaptive_thinking
from agent.anthropic_endpoints import _model_name_is_kimi_family
for m in ("k3", "K3", "moonshotai/k3", "k3.1-preview", "k3-turbo"):
assert _model_name_is_kimi_family(m) is True, m
assert _supports_adaptive_thinking("k3") is True
@@ -1502,7 +1487,7 @@ class TestBlankTextBlockFiltering:
"""
def _convert(self, message):
from agent.anthropic_adapter import _convert_assistant_message
from agent.anthropic_message_convert import _convert_assistant_message
return _convert_assistant_message(message)
@@ -1563,7 +1548,7 @@ class TestBlankTextBlockFiltering:
a blank text block carrying cache_control (e.g. a stored, previously
cache-marked turn where prompt_caching later becomes blank on replay)
must not silently lose the breakpoint when dropped."""
from agent.anthropic_adapter import _convert_assistant_message
from agent.anthropic_message_convert import _convert_assistant_message
msg = {
"role": "assistant",
"content": "",
@@ -1604,7 +1589,7 @@ class TestAllBlankFallbackAndNonStringText:
"""
def _convert(self, message):
from agent.anthropic_adapter import _convert_assistant_message
from agent.anthropic_message_convert import _convert_assistant_message
return _convert_assistant_message(message)
@@ -1674,7 +1659,7 @@ class TestReplayAllBlankFallback:
"""
def _convert(self, message):
from agent.anthropic_adapter import _convert_assistant_message
from agent.anthropic_message_convert import _convert_assistant_message
return _convert_assistant_message(message)
def test_sole_blank_marked_replay_block_keeps_marker_on_placeholder(self):
+5 -9
View File
@@ -7,11 +7,7 @@ from unittest.mock import patch, MagicMock
import pytest
from agent.anthropic_adapter import (
_read_claude_code_credentials_from_keychain,
read_claude_code_credentials,
_refresh_oauth_token,
)
from agent.anthropic_credentials import _read_claude_code_credentials_from_keychain, read_claude_code_credentials, _refresh_oauth_token
# This module exercises the reader itself with explicit platform and subprocess
@@ -67,7 +63,7 @@ class TestReadClaudeCodeCredentialsPriority:
"expiresAt": 9999999999999,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
# Mock Keychain to return a "newer" token
with patch("agent.anthropic_adapter.subprocess.run") as mock_run:
@@ -100,7 +96,7 @@ class TestReadClaudeCodeCredentialsPriority:
"expiresAt": 9999999999999,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
with patch("agent.anthropic_adapter.subprocess.run") as mock_run:
# Simulate Keychain entry not found
@@ -113,7 +109,7 @@ class TestReadClaudeCodeCredentialsPriority:
def test_returns_none_when_neither_keychain_nor_json_has_creds(self, tmp_path, monkeypatch):
"""No credentials anywhere — must return None cleanly."""
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
with patch("agent.anthropic_adapter.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="")
@@ -147,7 +143,7 @@ class TestReadClaudeCodeCredentialsDesync:
"expiresAt": file_expires_at,
}
}))
monkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path)
monkeypatch.setattr("agent.anthropic_credentials.Path.home", lambda: tmp_path)
def _keychain_payload(self, *, access_token, expires_at, refresh_token="kc-refresh"):
return MagicMock(
@@ -3,7 +3,7 @@
from types import SimpleNamespace
from agent.transports import get_transport
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
SIG = "sig-k3"
+3 -3
View File
@@ -120,7 +120,7 @@ def test_authorization_url_state_is_not_pkce_verifier(monkeypatch, tmp_path):
monkeypatch.setattr(builtins, "input", fake_input)
from agent.anthropic_adapter import run_hermes_oauth_login_pure
from agent.anthropic_credentials import run_hermes_oauth_login_pure
result = run_hermes_oauth_login_pure()
assert result is not None, "OAuth flow should succeed with matching state"
@@ -176,7 +176,7 @@ def test_login_token_exchange_uses_platform_claude_host(monkeypatch, tmp_path):
monkeypatch.setattr(builtins, "input", fake_input)
from agent.anthropic_adapter import run_hermes_oauth_login_pure
from agent.anthropic_credentials import run_hermes_oauth_login_pure
result = run_hermes_oauth_login_pure()
@@ -207,7 +207,7 @@ def test_callback_state_mismatch_aborts(monkeypatch, tmp_path, caplog):
capture_token_request=captured_token,
)
from agent.anthropic_adapter import run_hermes_oauth_login_pure
from agent.anthropic_credentials import run_hermes_oauth_login_pure
result = run_hermes_oauth_login_pure()
@@ -45,7 +45,7 @@ class TestOAuthUserAgentPrefix:
def test_token_refresh_ua_not_throttled(self):
"""refresh_anthropic_oauth_pure must NOT send a throttled token-endpoint UA."""
import inspect
import agent.anthropic_adapter as mod
import agent.anthropic_credentials as mod
func = getattr(mod, "refresh_anthropic_oauth_pure", None)
if func is None or not callable(func):
@@ -11,11 +11,7 @@ normalize_response capture, _sanitize_replay_block (ordered-blocks replay), and
_convert_content_part_to_anthropic (content-list replay).
"""
import pytest
from agent.anthropic_adapter import (
_sanitize_replay_block,
_convert_content_part_to_anthropic,
_convert_assistant_message,
)
from agent.anthropic_message_convert import _sanitize_replay_block, _convert_content_part_to_anthropic, _convert_assistant_message
FORBIDDEN = {"parsed_output", "caller"}
@@ -13,10 +13,7 @@ list, and the system-block path coerces blanks at extraction time (a blank block
carrying a cache_control breakpoint cannot be dropped).
Ref #69512 / #70909 (follow-up: request-level guard, not just per-message).
"""
from agent.anthropic_adapter import (
_EMPTY_TEXT_PLACEHOLDER,
convert_messages_to_anthropic,
)
from agent.anthropic_message_convert import _EMPTY_TEXT_PLACEHOLDER, convert_messages_to_anthropic
def _all_text_blocks(messages):
@@ -36,7 +36,7 @@ from types import SimpleNamespace
import pytest
from agent.transports import get_transport
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
def _thinking_block(text: str, signature: str) -> SimpleNamespace:
@@ -32,7 +32,7 @@ import pytest
from unittest.mock import patch
from agent import secret_scope as ss
from agent.anthropic_adapter import resolve_anthropic_token
from agent.anthropic_credentials import resolve_anthropic_token
@pytest.fixture(autouse=True)
@@ -12,12 +12,7 @@ text to a non-whitespace placeholder at two points on the Anthropic request path
in ``_convert_assistant_message`` (main path). Ref #69512.
"""
import pytest
from agent.anthropic_adapter import (
_EMPTY_TEXT_PLACEHOLDER,
_safe_text,
_sanitize_replay_block,
_convert_assistant_message,
)
from agent.anthropic_message_convert import _EMPTY_TEXT_PLACEHOLDER, _safe_text, _sanitize_replay_block, _convert_assistant_message
def _text_blocks(msg):
@@ -53,7 +53,7 @@ class TestAnthropicPoolExhaustedFallsBackToEnv:
with patch(
"agent.auxiliary_client._select_pool_entry", return_value=(True, None)
), patch(
"agent.anthropic_adapter.resolve_anthropic_token", return_value=None
"agent.anthropic_credentials.resolve_anthropic_token", return_value=None
):
from agent.auxiliary_client import _try_anthropic
+7 -7
View File
@@ -670,7 +670,7 @@ class TestAnthropicOAuthFlag:
def test_api_key_no_oauth_flag(self, monkeypatch):
"""Regular API keys (sk-ant-api-*) should create client with is_oauth=False."""
with patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-api03-testkey1234"), \
with patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="sk-ant-api03-testkey1234"), \
patch("agent.anthropic_adapter.build_anthropic_client") as mock_build, \
patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)):
mock_build.return_value = MagicMock()
@@ -695,7 +695,7 @@ class TestAnthropicOAuthFlag:
with (
patch("agent.auxiliary_client.load_pool", return_value=_Pool()),
patch("agent.anthropic_adapter.resolve_anthropic_token", side_effect=AssertionError("legacy path should not run")),
patch("agent.anthropic_credentials.resolve_anthropic_token", side_effect=AssertionError("legacy path should not run")),
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()) as mock_build,
):
from agent.auxiliary_client import _try_anthropic
@@ -961,7 +961,7 @@ class TestExplicitProviderRouting:
def test_explicit_anthropic_api_key(self, monkeypatch):
"""provider='anthropic' + regular API key should work with is_oauth=False."""
with patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-api-regular-key"), \
with patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="sk-ant-api-regular-key"), \
patch("agent.anthropic_adapter.build_anthropic_client") as mock_build, \
patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)):
mock_build.return_value = MagicMock()
@@ -1217,7 +1217,7 @@ class TestVisionClientFallback:
patch("agent.auxiliary_client._read_main_provider", return_value="anthropic"),
patch("agent.auxiliary_client._read_main_model", return_value="claude-sonnet-4"),
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()),
patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="***"),
patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="***"),
):
backends = get_available_vision_backends()
@@ -4027,7 +4027,7 @@ class TestAnthropicExplicitApiKey:
def test_try_anthropic_uses_explicit_api_key_over_env(self):
"""_try_anthropic(explicit_api_key) must use the supplied key, not the env fallback."""
with patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="env-fallback-key"), \
with patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="env-fallback-key"), \
patch("agent.anthropic_adapter.build_anthropic_client") as mock_build, \
patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)):
mock_build.return_value = MagicMock()
@@ -4041,7 +4041,7 @@ class TestAnthropicExplicitApiKey:
def test_try_anthropic_without_explicit_key_falls_back_to_resolve(self):
"""Without explicit_api_key, _try_anthropic falls back to resolve_anthropic_token."""
with patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="env-fallback-key"), \
with patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="env-fallback-key"), \
patch("agent.anthropic_adapter.build_anthropic_client") as mock_build, \
patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)):
mock_build.return_value = MagicMock()
@@ -4052,7 +4052,7 @@ class TestAnthropicExplicitApiKey:
def test_resolve_provider_client_passes_explicit_api_key_to_anthropic(self):
"""resolve_provider_client(provider='anthropic', explicit_api_key=...) must propagate the key."""
with patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="env-key"), \
with patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="env-key"), \
patch("agent.anthropic_adapter.build_anthropic_client") as mock_build, \
patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)):
mock_build.return_value = MagicMock()
@@ -39,7 +39,7 @@ class TestTryAnthropicBaseUrlHostValidation:
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="***",
),
patch(
@@ -74,7 +74,7 @@ class TestTryAnthropicBaseUrlHostValidation:
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="***",
),
patch(
@@ -106,7 +106,7 @@ class TestTryAnthropicBaseUrlHostValidation:
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="***",
),
patch(
@@ -141,7 +141,7 @@ class TestTryAnthropicBaseUrlHostValidation:
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="***",
),
patch(
@@ -175,7 +175,7 @@ class TestTryAnthropicBaseUrlHostValidation:
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="***",
),
patch(
@@ -220,7 +220,7 @@ class TestTryAnthropicBaseUrlHostValidation:
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="***",
),
patch(
+5 -5
View File
@@ -290,7 +290,7 @@ class TestPackaging:
# us.anthropic.claude-sonnet-4-5-20250929-v1:0
# apac.anthropic.claude-haiku-4-5
#
# ``agent.anthropic_adapter.normalize_model_name`` converts dots to hyphens
# ``agent.anthropic_message_convert.normalize_model_name`` converts dots to hyphens
# unless the caller opts in via ``preserve_dots=True``. Before this fix,
# ``AIAgent._anthropic_preserve_dots`` returned False for the ``bedrock``
# provider, so Claude-on-Bedrock requests went out with
@@ -339,7 +339,7 @@ class TestBedrockModelNameNormalization:
def test_global_anthropic_inference_profile_preserved(self):
"""The reporter's exact model ID."""
from agent.anthropic_adapter import normalize_model_name
from agent.anthropic_message_convert import normalize_model_name
assert normalize_model_name(
"global.anthropic.claude-opus-4-7", preserve_dots=True
) == "global.anthropic.claude-opus-4-7"
@@ -351,7 +351,7 @@ class TestBedrockModelNameNormalization:
always returned unmangled -- ``preserve_dots`` is irrelevant for
these IDs because the dots are namespace separators, not version
separators. Regression for #12295."""
from agent.anthropic_adapter import normalize_model_name
from agent.anthropic_message_convert import normalize_model_name
assert normalize_model_name(
"global.anthropic.claude-opus-4-7", preserve_dots=False
) == "global.anthropic.claude-opus-4-7"
@@ -403,7 +403,7 @@ class TestBedrockModelIdDetection:
regardless of ``preserve_dots``. Regression for #12295."""
def test_bare_bedrock_id_detected(self):
from agent.anthropic_adapter import _is_bedrock_model_id
from agent.anthropic_message_convert import _is_bedrock_model_id
assert _is_bedrock_model_id("anthropic.claude-opus-4-7") is True
@@ -415,7 +415,7 @@ class TestBedrockModelIdDetection:
"""The primary bug from #12295: ``anthropic.claude-opus-4-7``
sent to bedrock-mantle via auxiliary clients that don't pass
``preserve_dots=True``."""
from agent.anthropic_adapter import normalize_model_name
from agent.anthropic_message_convert import normalize_model_name
assert normalize_model_name(
"anthropic.claude-opus-4-7", preserve_dots=False
) == "anthropic.claude-opus-4-7"
@@ -53,7 +53,7 @@ def test_load_heals_legacy_row_and_exposes_it_to_resolver(tmp_path, monkeypatch)
},
}))
from agent.anthropic_adapter import resolve_anthropic_token
from agent.anthropic_credentials import resolve_anthropic_token
from agent.credential_pool import load_pool
entry = load_pool("anthropic").entries()[0]
@@ -35,7 +35,7 @@ class TestDeepSeekAnthropicPreservesThinking:
DeepSeek issues its own signatures and cannot validate Anthropic's —
the strip-signed / keep-unsigned split matches the Kimi policy.
"""
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
messages = [
{"role": "user", "content": "hi"},
@@ -73,7 +73,7 @@ class TestDeepSeekAnthropicPreservesThinking:
as ignored — cache markers interfere with signature validation on
upstreams that do check them, so Hermes strips them everywhere.
"""
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
messages = [
{"role": "user", "content": "hi"},
@@ -118,7 +118,7 @@ class TestKimiFamilyGetsAdaptiveThinking:
blocks must survive the third-party signature-stripping pass so
the upstream's message-history validation passes.
"""
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
messages = [
{"role": "user", "content": "hi"},
+2 -2
View File
@@ -97,7 +97,7 @@ def test_agent_init_anthropic_url_implies_provider_and_api_mode():
from run_agent import AIAgent
with patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), patch(
"agent.anthropic_adapter._is_oauth_token", return_value=False
"agent.anthropic_credentials._is_oauth_token", return_value=False
):
agent = AIAgent(
provider=None,
@@ -127,7 +127,7 @@ def test_agent_init_anthropic_url_preserves_credential_pool():
pool = SimpleNamespace(provider="anthropic")
with patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), patch(
"agent.anthropic_adapter._is_oauth_token", return_value=False
"agent.anthropic_credentials._is_oauth_token", return_value=False
):
agent = AIAgent(
provider=None,
+3 -3
View File
@@ -275,7 +275,7 @@ class TestMinimaxPreserveDots:
def test_normalize_preserves_m25_free_dot(self):
from agent.anthropic_adapter import normalize_model_name
from agent.anthropic_message_convert import normalize_model_name
assert normalize_model_name("minimax-m2.5-free", preserve_dots=True) == "minimax-m2.5-free"
@@ -312,8 +312,8 @@ class TestMinimaxSwitchModelCredentialGuard:
agent._fallback_chain = []
with patch("agent.anthropic_adapter.build_anthropic_client") as mock_build, \
patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-leaked") as mock_resolve, \
patch("agent.anthropic_adapter._is_oauth_token", return_value=False):
patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="sk-ant-leaked") as mock_resolve, \
patch("agent.anthropic_credentials._is_oauth_token", return_value=False):
agent.switch_model(
new_model="MiniMax-M2.7",
+1 -1
View File
@@ -6,7 +6,7 @@ a present-but-null value sails through as None and crashes any chained
method call.
"""
from agent.anthropic_adapter import _convert_user_message
from agent.anthropic_message_convert import _convert_user_message
from agent.moa_loop import _slot_label
@@ -172,10 +172,7 @@ class TestClientShape:
def test_lookalike_host_does_not_get_portal_treatment(self):
"""Substring matching would hand a spoofed host the Portal JWT as a
Bearer token. Hostname matching must reject it."""
from agent.anthropic_adapter import (
_is_nous_portal_endpoint,
_requires_bearer_auth,
)
from agent.anthropic_endpoints import _is_nous_portal_endpoint, _requires_bearer_auth
spoofed = "https://inference-api.nousresearch.com.attacker.test/v1"
assert not _is_nous_portal_endpoint(spoofed)
@@ -369,7 +366,7 @@ class TestPortalThinkingReplay:
]
def _assert_thinking_kept(self, base_url):
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
_system, converted = convert_messages_to_anthropic(
self._messages(),
@@ -402,7 +399,7 @@ class TestPortalThinkingReplay:
def test_other_third_party_gateways_still_strip_thinking(self):
"""The Portal carve-out must not leak into MiniMax-style proxies."""
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
_system, converted = convert_messages_to_anthropic(
self._messages(),
@@ -101,7 +101,7 @@ def test_relay_tools_jsonable_no_warning_leak():
def test_anthropic_to_plain_data_no_warning_leak():
from agent.anthropic_adapter import _to_plain_data
from agent.anthropic_message_convert import _to_plain_data
_, snapshot = _accumulated_stop_event()
with warnings.catch_warnings(record=True) as recorded:
@@ -126,7 +126,7 @@ def test_duck_typed_model_dump_fallback():
"""Non-pydantic objects with a bare model_dump() must still serialize."""
from agent.relay_llm import _jsonable as rl_jsonable
from agent.relay_tools import _jsonable as rt_jsonable
from agent.anthropic_adapter import _to_plain_data
from agent.anthropic_message_convert import _to_plain_data
class Duck:
def model_dump(self): # no mode/warnings kwargs
+10 -15
View File
@@ -631,21 +631,16 @@ def _neutralize_macos_keychain_creds(request, monkeypatch):
if request.node.get_closest_marker(_ALLOW_MACOS_KEYCHAIN_MARK):
return None
# Patch the implementation owner (agent.anthropic_credentials) AND the
# adapter re-export: after the adapter godfile split, the real call
# executes inside agent.anthropic_credentials, so patching only the
# adapter alias silently stopped intercepting Keychain reads.
for _module_name in ("agent.anthropic_credentials", "agent.anthropic_adapter"):
try:
_mod = importlib.import_module(_module_name)
except Exception:
continue
monkeypatch.setattr(
_mod,
"_read_claude_code_credentials_from_keychain",
lambda *_args, **_kwargs: None,
raising=False,
)
try:
_mod = importlib.import_module("agent.anthropic_credentials")
except Exception:
return None
monkeypatch.setattr(
_mod,
"_read_claude_code_credentials_from_keychain",
lambda *_args, **_kwargs: None,
raising=False,
)
return None
@@ -27,7 +27,7 @@ class TestStaleOAuthTokenDetection:
# No valid Claude Code credentials available (expired, no refresh token)
monkeypatch.setattr(
"agent.anthropic_adapter.read_claude_code_credentials",
"agent.anthropic_credentials.read_claude_code_credentials",
lambda: {
"accessToken": "expired-cc-token",
"refreshToken": "", # No refresh — can't recover
@@ -36,16 +36,16 @@ class TestStaleOAuthTokenDetection:
},
)
monkeypatch.setattr(
"agent.anthropic_adapter.is_claude_code_token_valid",
"agent.anthropic_credentials.is_claude_code_token_valid",
lambda creds: False, # Explicitly expired
)
monkeypatch.setattr(
"agent.anthropic_adapter._is_oauth_token",
"agent.anthropic_credentials._is_oauth_token",
lambda key: key.startswith("sk-ant-"),
)
# _resolve_claude_code_token_from_credentials has no valid path
monkeypatch.setattr(
"agent.anthropic_adapter._resolve_claude_code_token_from_credentials",
"agent.anthropic_credentials._resolve_claude_code_token_from_credentials",
lambda creds=None: None,
)
@@ -77,15 +77,15 @@ class TestStaleOAuthTokenDetection:
save_env_value("ANTHROPIC_TOKEN", "")
monkeypatch.setattr(
"agent.anthropic_adapter.read_claude_code_credentials",
"agent.anthropic_credentials.read_claude_code_credentials",
lambda: None, # No CC creds
)
monkeypatch.setattr(
"agent.anthropic_adapter.is_claude_code_token_valid",
"agent.anthropic_credentials.is_claude_code_token_valid",
lambda creds: False,
)
monkeypatch.setattr(
"agent.anthropic_adapter._is_oauth_token",
"agent.anthropic_credentials._is_oauth_token",
lambda key: key.startswith("sk-ant-") and "oat" in key,
)
@@ -6,11 +6,11 @@ from hermes_cli.config import load_env, save_env_value
def test_run_anthropic_oauth_flow_prefers_claude_code_credentials(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setattr(
"agent.anthropic_adapter.run_oauth_setup_token",
"agent.anthropic_credentials.run_oauth_setup_token",
lambda: "sk-ant-oat01-from-claude-setup",
)
monkeypatch.setattr(
"agent.anthropic_adapter.read_claude_code_credentials",
"agent.anthropic_credentials.read_claude_code_credentials",
lambda: {
"accessToken": "cc-access-token",
"refreshToken": "cc-refresh-token",
@@ -18,7 +18,7 @@ def test_run_anthropic_oauth_flow_prefers_claude_code_credentials(tmp_path, monk
},
)
monkeypatch.setattr(
"agent.anthropic_adapter.is_claude_code_token_valid",
"agent.anthropic_credentials.is_claude_code_token_valid",
lambda creds: True,
)
@@ -36,9 +36,9 @@ def test_run_anthropic_oauth_flow_prefers_claude_code_credentials(tmp_path, monk
def test_run_anthropic_oauth_flow_manual_token_still_persists(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setattr("agent.anthropic_adapter.run_oauth_setup_token", lambda: None)
monkeypatch.setattr("agent.anthropic_adapter.read_claude_code_credentials", lambda: None)
monkeypatch.setattr("agent.anthropic_adapter.is_claude_code_token_valid", lambda creds: False)
monkeypatch.setattr("agent.anthropic_credentials.run_oauth_setup_token", lambda: None)
monkeypatch.setattr("agent.anthropic_credentials.read_claude_code_credentials", lambda: None)
monkeypatch.setattr("agent.anthropic_credentials.is_claude_code_token_valid", lambda creds: False)
monkeypatch.setattr("builtins.input", lambda _prompt="": "sk-ant-oat01-manual-token")
monkeypatch.setattr(
"hermes_cli.secret_prompt.masked_secret_prompt",
@@ -23,7 +23,7 @@ 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_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
lambda: None,
)
monkeypatch.setattr(
@@ -71,7 +71,7 @@ def test_anthropic_pool_api_key_overrides_conflicting_active_endpoint(monkeypatc
lambda: {"provider": "anthropic", "base_url": active_endpoint},
)
monkeypatch.setattr(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
lambda: None,
)
monkeypatch.setattr(
+2 -2
View File
@@ -535,11 +535,11 @@ class TestHasAnyProviderConfigured:
monkeypatch.setattr("hermes_cli.auth.get_auth_status", lambda _pid: {})
# Simulate valid Claude Code credentials
monkeypatch.setattr(
"agent.anthropic_adapter.read_claude_code_credentials",
"agent.anthropic_credentials.read_claude_code_credentials",
lambda: {"accessToken": "sk-ant-test", "refreshToken": "ref-tok"},
)
monkeypatch.setattr(
"agent.anthropic_adapter.is_claude_code_token_valid",
"agent.anthropic_credentials.is_claude_code_token_valid",
lambda creds: True,
)
from hermes_cli.main import _has_any_provider_configured
+1 -1
View File
@@ -960,7 +960,7 @@ def test_seed_from_singletons_respects_hermes_pkce_suppression(tmp_path, monkeyp
}))
# Stub the readers so only hermes_pkce is "available"; claude_code returns None
import agent.anthropic_adapter as aa
import agent.anthropic_credentials as aa
monkeypatch.setattr(aa, "read_hermes_oauth_credentials", lambda: {
"accessToken": "tok", "refreshToken": "r", "expiresAt": 9999999999000,
})
+4 -4
View File
@@ -311,11 +311,11 @@ def test_anthropic_oauth_presence_accepts_pool_only_oauth_entry():
with (
patch(
"agent.anthropic_adapter.read_hermes_oauth_credentials",
"agent.anthropic_credentials.read_hermes_oauth_credentials",
return_value=None,
),
patch(
"agent.anthropic_adapter.read_claude_code_credentials",
"agent.anthropic_credentials.read_claude_code_credentials",
return_value=None,
),
patch(
@@ -331,11 +331,11 @@ def test_anthropic_oauth_presence_accepts_pool_only_oauth_entry():
# (they are handled by the explicit-config gate / env var paths).
with (
patch(
"agent.anthropic_adapter.read_hermes_oauth_credentials",
"agent.anthropic_credentials.read_hermes_oauth_credentials",
return_value=None,
),
patch(
"agent.anthropic_adapter.read_claude_code_credentials",
"agent.anthropic_credentials.read_claude_code_credentials",
return_value=None,
),
patch(
@@ -154,8 +154,8 @@ class TestAgentSwitchModelDefenseInDepth:
with patch(
"agent.anthropic_adapter.build_anthropic_client",
side_effect=_raise_after_capture,
), patch("agent.anthropic_adapter.resolve_anthropic_token", return_value=""), patch(
"agent.anthropic_adapter._is_oauth_token", return_value=False
), patch("agent.anthropic_credentials.resolve_anthropic_token", return_value=""), patch(
"agent.anthropic_credentials._is_oauth_token", return_value=False
):
with pytest.raises(_Sentinel):
agent.switch_model(
@@ -1192,7 +1192,7 @@ class TestAzureAnthropicEnvVarHint:
called["resolve_anthropic_token"] = True
return "token-from-resolver"
monkeypatch.setattr(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
_fake_resolve,
)
@@ -156,14 +156,14 @@ class TestCommandCodeAnthropicBearerAuth:
"""
def test_requires_bearer_auth_recognizes_commandcode(self):
from agent.anthropic_adapter import _requires_bearer_auth
from agent.anthropic_endpoints import _requires_bearer_auth
assert _requires_bearer_auth("https://api.commandcode.ai/provider/v1") is True
assert _requires_bearer_auth("https://api.commandcode.ai/provider/v1/models") is True
assert _requires_bearer_auth("https://api.commandcode.ai/anthropic") is True
def test_bearer_auth_does_not_affect_unrelated(self):
from agent.anthropic_adapter import _requires_bearer_auth
from agent.anthropic_endpoints import _requires_bearer_auth
# Native Anthropic still uses x-api-key
assert _requires_bearer_auth("https://api.anthropic.com") is False
@@ -171,7 +171,7 @@ class TestCommandCodeAnthropicBearerAuth:
assert _requires_bearer_auth("https://openrouter.ai/api/v1") is False
def test_bearer_auth_case_insensitive(self):
from agent.anthropic_adapter import _requires_bearer_auth
from agent.anthropic_endpoints import _requires_bearer_auth
assert _requires_bearer_auth("https://API.COMMANDCODE.AI/provider/v1") is True
@@ -38,8 +38,8 @@ class TestCredentialPoolPreservedOnAutoDetect:
with patch("agent.auxiliary_client.resolve_provider_client", return_value=(None, None)), \
patch("run_agent.get_tool_definitions", return_value=[]), \
patch('agent.anthropic_adapter.build_anthropic_client', return_value=MagicMock()), \
patch('agent.anthropic_adapter.resolve_anthropic_token', return_value=''), \
patch('agent.anthropic_adapter._is_oauth_token', return_value=False), \
patch('agent.anthropic_credentials.resolve_anthropic_token', return_value=''), \
patch('agent.anthropic_credentials._is_oauth_token', return_value=False), \
patch('agent.azure_identity_adapter.is_token_provider', return_value=False), \
patch('hermes_cli.model_normalize.normalize_model_for_provider', return_value='test-model'), \
patch('agent.credential_pool.load_pool', return_value=MagicMock()), \
@@ -64,7 +64,7 @@ class TestOAuthFlagOnRefresh:
agent._is_anthropic_oauth = False
with (
patch("agent.anthropic_adapter.resolve_anthropic_token",
patch("agent.anthropic_credentials.resolve_anthropic_token",
return_value=_OAUTH_LIKE_TOKEN),
patch("agent.anthropic_adapter.build_anthropic_client",
return_value=MagicMock()),
@@ -111,7 +111,7 @@ class TestOAuthFlagOnConstruction:
return_value=MagicMock()),
# Simulate a stale ANTHROPIC_TOKEN in the env — the init code
# MUST NOT fall back to it when provider != anthropic.
patch("agent.anthropic_adapter.resolve_anthropic_token",
patch("agent.anthropic_credentials.resolve_anthropic_token",
return_value=_OAUTH_LIKE_TOKEN),
):
agent = AIAgent(
@@ -136,7 +136,7 @@ class TestOAuthFlagOnFallbackActivation:
def test_fallback_to_third_party_does_not_flip_oauth(self, agent):
"""Directly mimic the post-fallback assignment at line ~6537."""
from agent.anthropic_adapter import _is_oauth_token
from agent.anthropic_credentials import _is_oauth_token
# Emulate the relevant lines of _try_activate_fallback without
# running the entire recovery stack (which pulls in streaming,
@@ -153,6 +153,6 @@ class TestApiKeyTokensAlwaysSafe:
"""Regression: plain API-key shapes must always resolve to non-OAuth, any provider."""
def test_native_anthropic_with_api_key_token(self):
from agent.anthropic_adapter import _is_oauth_token
from agent.anthropic_credentials import _is_oauth_token
assert _is_oauth_token(_API_KEY_TOKEN) is False
@@ -40,8 +40,8 @@ def _make_agent(chain):
def _switch_to_anthropic(agent):
with (
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()),
patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-xyz"),
patch("agent.anthropic_adapter._is_oauth_token", return_value=False),
patch("agent.anthropic_credentials.resolve_anthropic_token", return_value="sk-ant-xyz"),
patch("agent.anthropic_credentials._is_oauth_token", return_value=False),
patch("hermes_cli.timeouts.get_provider_request_timeout", return_value=None),
):
agent.switch_model(
@@ -127,10 +127,10 @@ def test_anthropic_client_rebuild_failure_rolls_back_to_original_state():
side_effect=RuntimeError("simulated anthropic build failure"),
),
patch(
"agent.anthropic_adapter.resolve_anthropic_token",
"agent.anthropic_credentials.resolve_anthropic_token",
return_value="sk-ant-resolved",
),
patch("agent.anthropic_adapter._is_oauth_token", return_value=False),
patch("agent.anthropic_credentials._is_oauth_token", return_value=False),
patch("hermes_cli.timeouts.get_provider_request_timeout", return_value=None),
):
with pytest.raises(RuntimeError, match="simulated anthropic build failure"):
+5 -8
View File
@@ -33,14 +33,11 @@ def test_webbrowser_get_controller_is_neutralized(_neutralize_webbrowser):
def _isolate_anthropic_credentials(monkeypatch, tmp_path):
from agent import anthropic_adapter as aa
from agent import anthropic_credentials as ac
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
# Patch the implementation owner (anthropic_credentials); the adapter
# merely re-exports these functions after the godfile split.
monkeypatch.setattr(ac.Path, "home", lambda: tmp_path)
monkeypatch.setattr(ac.platform, "system", lambda: "Darwin")
@@ -48,22 +45,22 @@ def _isolate_anthropic_credentials(monkeypatch, tmp_path):
raise AssertionError("test reached the real macOS Keychain command")
monkeypatch.setattr(ac.subprocess, "run", _real_keychain_reached)
return aa
return ac
def test_claude_code_credential_read_does_not_touch_macos_keychain(
monkeypatch, tmp_path
):
"""The real credential reader should be safe under the suite guard."""
aa = _isolate_anthropic_credentials(monkeypatch, tmp_path)
ac = _isolate_anthropic_credentials(monkeypatch, tmp_path)
assert aa.read_claude_code_credentials() is None
assert ac.read_claude_code_credentials() is None
def test_anthropic_token_resolution_does_not_touch_macos_keychain(
monkeypatch, tmp_path
):
"""Token resolution should be safe under the same suite guard."""
aa = _isolate_anthropic_credentials(monkeypatch, tmp_path)
ac = _isolate_anthropic_credentials(monkeypatch, tmp_path)
assert aa.resolve_anthropic_token() is None
assert ac.resolve_anthropic_token() is None
+2 -2
View File
@@ -406,7 +406,7 @@ class TestCuaCaptureImageDimensions:
class TestAnthropicAdapterMultimodal:
def test_multimodal_envelope_becomes_tool_result_with_image_block(self):
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
fake_png = "iVBORw0KGgo="
messages = [
@@ -446,7 +446,7 @@ class TestAnthropicAdapterMultimodal:
def test_old_screenshots_are_evicted_beyond_max_keep(self):
"""Image blocks in old tool_results get replaced with placeholders."""
from agent.anthropic_adapter import convert_messages_to_anthropic
from agent.anthropic_message_convert import convert_messages_to_anthropic
fake_png = "iVBORw0KGgo="