From 423bc7e4e45ebc7d3b2101ec3e917afafb0f7723 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:33:26 -0700 Subject: [PATCH] fix(credential-pool): hydrate on-disk env rows on the openrouter branch too The openrouter branch of _seed_from_env returns before the generic loop, so a persisted `env:OPENROUTER_API_KEY_2` row stayed empty exactly like the registry-provider case #103067 fixed. Fold the "declared vars + on-disk env rows" union into one helper both branches use. --- agent/credential_pool.py | 43 ++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 074731c79d..fcea4c1510 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -2566,6 +2566,24 @@ _ENV_BASE_URL_RESOLVERS = { } +def _with_on_disk_env_sources(env_vars: List[str], entries: List[PooledCredential]) -> List[str]: + """*env_vars* plus the ``env:VAR`` names already persisted in the pool. + + Env-backed rows are written to auth.json without their secret and + re-hydrated on every load; a row whose VAR the registry does not + declare (a second key the user pointed at ``env:PROVIDER_API_KEY_2``) + would otherwise stay empty forever and be silently dropped from + rotation by ``_available_entries``. + """ + names = list(env_vars) + for entry in entries: + if entry.source.startswith("env:"): + env_name = entry.source.split(":", 1)[1].strip() + if env_name and env_name not in names: + names.append(env_name) + return names + + def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]: seed = _Seeder(provider, entries) # Copilot's singleton branch exchanges the raw ghu_ OAuth token for the @@ -2576,12 +2594,13 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool return seed.result if provider == "openrouter": - token = get_env_prefer_dotenv("OPENROUTER_API_KEY") - if token and seed.upsert( - "env:OPENROUTER_API_KEY", - _env_payload(env_var="OPENROUTER_API_KEY", token=token, base_url=OPENROUTER_BASE_URL), - ): - _warn_env_ingestion_once(provider, "OPENROUTER_API_KEY") + for env_var in _with_on_disk_env_sources(["OPENROUTER_API_KEY"], entries): + token = get_env_prefer_dotenv(env_var) + if token and seed.upsert( + f"env:{env_var}", + _env_payload(env_var=env_var, token=token, base_url=OPENROUTER_BASE_URL), + ): + _warn_env_ingestion_once(provider, env_var) return seed.result pconfig = PROVIDER_REGISTRY.get(provider) @@ -2595,17 +2614,7 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool env_vars = list(pconfig.api_key_env_vars) if provider == "anthropic": env_vars = ["ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"] - - # Also seed any env-source pool entries already on disk that aren't in - # the registry's tuple. A user can put `source: env:OPENCODE_GO_API_KEY2` - # in auth.json expecting it to be picked up from the environment; the - # registry only declares the primary env var, so this loop fills the - # gap and keeps multi-key rotation working without per-provider code. - for entry in entries: - if entry.source.startswith("env:"): - env_name = entry.source.split(":", 1)[1] - if env_name and env_name not in env_vars: - env_vars.append(env_name) + env_vars = _with_on_disk_env_sources(env_vars, entries) resolve_base_url = _ENV_BASE_URL_RESOLVERS.get(provider) for env_var in env_vars: