feat: choose pooled credential priority from the CLI

This commit is contained in:
Brian Le
2026-09-07 02:09:54 -07:00
committed by Teknium
parent b86fb277b8
commit 1a4bb74a40
7 changed files with 102 additions and 4 deletions
+17
View File
@@ -67,6 +67,23 @@ class CredentialPoolAdminMixin:
self._current_id = None
return removed
def move_entry(self, credential_id: str, priority: int) -> Optional[PooledCredential]:
"""Place an entry at a clamped zero-based position and persist contiguous priorities."""
from agent.credential_pool import _normalize_pool_priorities
with self._lock:
entry = self._find(lambda e: e.id == credential_id)
if entry is None:
return None
others = [e for e in self._entries if e.id != credential_id]
others.insert(max(0, min(int(priority), len(others))), entry)
entries = [replace(e, priority=p) for p, e in enumerate(others)]
# Apply load-time ordering now so the reported position survives reload.
_normalize_pool_priorities(self.provider, entries)
self._entries = sorted(entries, key=lambda e: e.priority)
self._persist()
return self._find(lambda e: e.id == credential_id)
def resolve_target(self, target: Any) -> Tuple[Optional[int], Optional[PooledCredential], Optional[str]]:
raw = str(target or "").strip()
if not raw:
+1
View File
@@ -74,6 +74,7 @@ Examples:
hermes auth list List pooled credentials
hermes auth remove <p> <t> Remove pooled credential by index, id, or label
hermes auth reset <p> [t] Clear exhaustion status for a provider, or one credential
hermes auth priority <p> <t> <n> Move a pooled credential to priority n (0 = tried first)
hermes model Select default model
hermes fallback [list] Show fallback provider chain
hermes fallback add Add a fallback provider (same picker as `hermes model`)
+68 -1
View File
@@ -349,6 +349,14 @@ def auth_add_command(args) -> None:
if not is_custom:
_unsuppress_provider_sources(provider)
wanted_priority = getattr(args, "priority", None)
before = {entry.id for entry in pool.entries()}
_add_credential(args, provider, pool, requested_type)
if wanted_priority is not None:
_place_added_credential(provider, before, int(wanted_priority))
def _add_credential(args, provider: str, pool, requested_type: str) -> None:
if requested_type == AUTH_TYPE_API_KEY:
_add_api_key_credential(args, provider, pool)
return
@@ -379,6 +387,64 @@ def auth_add_command(args) -> None:
print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
def _place_added_credential(provider: str, before_ids: set, priority: int) -> None:
"""Move the credential `auth add` just created to *priority*.
Every add path (api key, OAuth spec, Nous) persists through the pool, so the
new row is the one id that was not there before the add. Reloading rather
than reusing the add's pool object keeps this correct for paths that write
their own store.
"""
pool = load_pool(provider)
entries = pool.entries()
added = [entry for entry in entries if entry.id not in before_ids]
if len(added) == 1:
target = added[0]
elif not added and len(entries) == 1:
# The add updated the sole existing row in place (a repeat Nous login).
target = entries[0]
else:
# The credential was saved; only the placement is unresolved, so do not fail.
print(f"note: could not identify the credential just added to {provider}; set its "
f"priority with `hermes auth priority {provider} <target> {priority}`.",
file=sys.stderr)
return
moved = pool.move_entry(target.id, priority)
_report_priority(provider, pool, moved, priority, "Placed", "at")
def _report_priority(provider: str, pool, moved, requested: int, verb: str, prep: str) -> None:
"""Print the effective priority and say why it differs from the request, if it does."""
print(f'{verb} {provider} credential "{moved.label}" {prep} priority {moved.priority} '
f"(#{moved.priority + 1} in `hermes auth list {provider}`)")
size = len(pool.entries())
if moved.priority != requested:
if requested < 0 or requested >= size:
reason = f"the pool has {size} credentials, so it was clamped"
else:
reason = "anthropic keeps manually added credentials ahead of seeded ones"
print(f"note: requested priority {requested}; effective priority is {moved.priority} "
f"because {reason}.", file=sys.stderr)
strategy = get_pool_strategy(provider)
if strategy != STRATEGY_FILL_FIRST:
print(f"note: {provider} uses the {strategy} strategy; priority only orders "
f"fill_first selection.", file=sys.stderr)
def auth_priority_command(args) -> None:
"""`hermes auth priority <provider> <target> <priority>`: reorder one pooled credential."""
provider = _normalize_provider(getattr(args, "provider", ""))
pool = load_pool(provider)
index, matched, error = pool.resolve_target(getattr(args, "target", None))
if matched is None or index is None:
raise SystemExit(f"{error} Provider: {provider}.")
requested = int(getattr(args, "priority"))
moved = pool.move_entry(matched.id, requested)
if moved is None:
raise SystemExit(f'No credential matching "{getattr(args, "target", None)}" for provider {provider}.')
_report_priority(provider, pool, moved, requested, "Set", "to")
def auth_list_command(args) -> None:
provider_filter = _normalize_provider(getattr(args, "provider", "") or "")
if provider_filter:
@@ -665,7 +731,8 @@ def _interactive_strategy() -> None:
_AUTH_ACTIONS = {
"add": auth_add_command, "list": auth_list_command, "remove": auth_remove_command,
"reset": auth_reset_command, "status": auth_status_command, "logout": auth_logout_command,
"reset": auth_reset_command, "priority": auth_priority_command, "status": auth_status_command,
"logout": auth_logout_command,
"spotify": auth_spotify_command}
+1 -1
View File
@@ -292,7 +292,7 @@ _CLI_FAMILIES: dict[str, tuple[_CliSurface, str]] = {
"memory": (_sub("memory", "build_memory_parser", "cmd_memory"), "status, *off, *reset"),
"auth": (
_sub("auth", "build_auth_parser", "cmd_auth"),
"list, status, *reset, *add, *remove, *logout, spotify status, *spotify login, "
"list, status, *reset, *priority, *add, *remove, *logout, spotify status, *spotify login, "
"*spotify logout"),
"pairing": (
_sub("pairing", "build_pairing_parser", "cmd_pairing"),
+9
View File
@@ -16,6 +16,10 @@ def build_auth_parser(subparsers, *, cmd_auth: Callable) -> None:
"--type", dest="auth_type", choices=["oauth", "api-key", "api_key"],
help="Credential type to add")
auth_add.add_argument("--label", help="Optional display label")
auth_add.add_argument(
"--priority", type=int,
help="Place the new credential at this priority (0 = tried first under fill_first); "
"appends last when omitted")
auth_add.add_argument("--api-key", help="API key value (otherwise prompted securely)")
auth_add.add_argument("--portal-url", help="Nous portal base URL")
auth_add.add_argument("--inference-url", help="Nous inference base URL")
@@ -39,6 +43,11 @@ def build_auth_parser(subparsers, *, cmd_auth: Callable) -> None:
auth_reset.add_argument(
"target", nargs="?",
help="Optional credential index, entry id, or exact label; clears every credential when omitted")
auth_priority = auth_subparsers.add_parser(
"priority", help="Move a pooled credential to a priority (0 = tried first under fill_first)")
auth_priority.add_argument("provider", help="Provider id")
auth_priority.add_argument("target", help="Credential index, entry id, or exact label")
auth_priority.add_argument("priority", type=int, help="New priority; others are renumbered")
auth_status = auth_subparsers.add_parser("status", help="Show auth status for a provider")
auth_status.add_argument("provider", help="Provider id")
auth_logout = auth_subparsers.add_parser(
+3 -1
View File
@@ -581,7 +581,9 @@ hermes auth list # Show all pools
hermes auth list openrouter # Show specific provider
hermes auth add openrouter --api-key sk-or-v1-xxx # Add API key
hermes auth add anthropic --type oauth # Add OAuth credential
hermes auth add openai-codex --type oauth --priority 0 # Add an account and try it first
hermes auth remove openrouter 2 # Remove by index
hermes auth priority openrouter backup-key 0 # Move a credential to the front of fill_first order
hermes auth reset openrouter # Clear cooldowns
hermes auth reset openrouter 2 # Clear the cooldown on one credential
hermes auth status anthropic # Show auth status for a provider
@@ -589,7 +591,7 @@ hermes auth logout anthropic # Log out and clear sto
hermes auth spotify # Authenticate Hermes with Spotify via PKCE
```
Subcommands: `add`, `list`, `remove`, `reset`, `status`, `logout`, `spotify`. When called with no subcommand, launches the interactive management wizard.
Subcommands: `add`, `list`, `remove`, `reset`, `priority`, `status`, `logout`, `spotify`. When called with no subcommand, launches the interactive management wizard.
## `hermes status`
@@ -116,6 +116,8 @@ Type [1/2]:
| `hermes auth add <provider>` | Add a credential (prompts for type and key) |
| `hermes auth add <provider> --type api-key --api-key <key>` | Add an API key non-interactively |
| `hermes auth add <provider> --type oauth` | Add an OAuth credential via browser login |
| `hermes auth add <provider> --priority 0` | Add a credential and place it first in the `fill_first` order |
| `hermes auth priority <provider> <target> <n>` | Move a credential to priority `n` (0 = tried first); the rest are renumbered |
| `hermes auth remove <provider> <index>` | Remove credential by 1-based index |
| `hermes auth reset <provider>` | Clear all cooldowns/exhaustion status |
| `hermes auth reset <provider> <target>` | Clear the cooldown on one credential by index, id, or label |
@@ -132,7 +134,7 @@ credential_pool_strategies:
| Strategy | Behavior |
|----------|----------|
| `fill_first` (default) | Use the first healthy key until it's exhausted, then move to the next |
| `fill_first` (default) | Use the first healthy key until it's exhausted, then move to the next; order is each credential's `priority` (`hermes auth priority` changes it) |
| `round_robin` | Cycle through keys evenly, rotating after each selection |
| `least_used` | Always pick the key with the lowest request count |
| `random` | Random selection among healthy keys |