Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions hermes_cli/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,50 @@ def _filter_explicit_provider_rows(rows: list[dict], ctx: ConfigContext) -> list
continue
if is_provider_explicitly_configured(slug):
kept.append(row)
continue
if slug == "anthropic" and _has_valid_external_anthropic_credentials():
# is_provider_explicitly_configured() deliberately excludes
# CLAUDE_CODE_OAUTH_TOKEN / external Claude Code credential files
# so aux tasks never silently burn the user's Claude Code
# subscription tokens without an explicit Hermes-side choice
# (PR #4210). That gate must stay untouched.
#
# But list_authenticated_providers() (used by BOTH the CLI
# `/model` picker and the desktop picker, upstream of this
# explicit-only filter) already surfaces anthropic whenever
# valid external Claude Code / Hermes-PKCE credentials exist —
# that's why `hermes model` already lists it. The desktop's
# explicit-only filter was silently dropping that same row,
# so the two surfaces disagreed. Keep the row visible here too
# (display only — is_provider_explicitly_configured is untouched,
# so aux-task gating and credential-pool behavior don't change).
kept.append(row)
return kept


def _has_valid_external_anthropic_credentials() -> bool:
"""True when Claude Code CLI or Hermes-managed OAuth creds are usable.

Mirrors the has_creds fallback in ``list_authenticated_providers()`` so
the desktop's explicit-only picker filter agrees with what the CLI
`/model` picker already shows for Anthropic. Display-only check — does
NOT affect ``is_provider_explicitly_configured()`` or aux-task gating.
"""
try:
from agent.anthropic_adapter import (
read_claude_code_credentials,
read_hermes_oauth_credentials,
)
hermes_creds = read_hermes_oauth_credentials()
cc_creds = read_claude_code_credentials()
return bool(
(hermes_creds and hermes_creds.get("accessToken"))
or (cc_creds and cc_creds.get("accessToken"))
)
except Exception:
return False


def _raw_config_has_enabled_moa_preset() -> bool:
"""Return True when the user's raw config explicitly enables MoA.

Expand Down
54 changes: 54 additions & 0 deletions tests/hermes_cli/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,60 @@ def test_explicit_only_filters_ambient_credentials_but_keeps_current_and_custom_
]


def test_explicit_only_keeps_anthropic_row_when_claude_code_credentials_valid():
"""Desktop's explicit-only picker must show Anthropic whenever valid
external Claude Code CLI / Hermes-PKCE credentials exist — matching what
`list_authenticated_providers()` (and thus the CLI `/model` picker)
already surfaces. is_provider_explicitly_configured() intentionally
excludes CLAUDE_CODE_OAUTH_TOKEN so aux-task gating (#4210) is untouched;
this only affects picker row visibility."""
rows = [
{"slug": "anthropic", "name": "Anthropic", "models": ["claude-sonnet-5"],
"total_models": 1, "is_current": False, "is_user_defined": False,
"source": "built-in"},
{"slug": "gemini", "name": "Gemini", "models": ["gemini-2.5-pro"],
"total_models": 1, "is_current": False, "is_user_defined": False,
"source": "built-in"},
]
ctx = _empty_ctx()
with (
_list_auth_returning(rows),
patch("hermes_cli.config.read_raw_config", return_value={}),
patch("hermes_cli.auth.is_provider_explicitly_configured", return_value=False),
patch(
"agent.anthropic_adapter.read_claude_code_credentials",
return_value={"accessToken": "tok", "refreshToken": "rtok"},
),
patch(
"agent.anthropic_adapter.read_hermes_oauth_credentials",
return_value=None,
),
):
payload = build_models_payload(ctx, explicit_only=True)

assert [row["slug"] for row in payload["providers"]] == ["anthropic"]


def test_explicit_only_drops_anthropic_row_without_external_credentials():
"""Without valid Claude Code / Hermes-PKCE credentials, the ambient
anthropic row stays hidden from the explicit-only picker as before."""
rows = [
{"slug": "anthropic", "name": "Anthropic", "models": ["claude-sonnet-5"],
"total_models": 1, "is_current": False, "is_user_defined": False,
"source": "built-in"},
]
ctx = _empty_ctx()
with (
_list_auth_returning(rows),
patch("hermes_cli.config.read_raw_config", return_value={}),
patch("hermes_cli.auth.is_provider_explicitly_configured", return_value=False),
patch("agent.anthropic_adapter.read_claude_code_credentials", return_value=None),
patch("agent.anthropic_adapter.read_hermes_oauth_credentials", return_value=None),
):
payload = build_models_payload(ctx, explicit_only=True)

assert payload["providers"] == []


# ─── picker_hints ──────────────────────────────────────────────────────

Expand Down
Loading