feat: choose pooled credential priority from the CLI
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user