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:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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})}
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="
|
||||
|
||||
|
||||
Reference in New Issue
Block a user