diff --git a/CHANGELOG.md b/CHANGELOG.md index 346995be..41c20b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- OpenCode provider key lookup now honors the shared `OPENCODE_API_KEY` fallback for both Zen and Go runtime paths, matching model-picker detection when provider-specific keys are absent (#3145). + ## [v0.51.165] — 2026-05-30 — Release EK (stage-batch47 — stop EventSource reconnect storm on long-lived SSE streams) ### Fixed diff --git a/api/providers.py b/api/providers.py index 200000d5..77b9c718 100644 --- a/api/providers.py +++ b/api/providers.py @@ -682,6 +682,12 @@ _PROVIDER_ENV_VAR_ALIASES: dict[str, tuple[str, ...]] = { # #1500 — agent runtime reads LM_API_KEY (canonical), but WebUI builds # ≤ v0.50.272 wrote LMSTUDIO_API_KEY into .env. Keep reading both. "lmstudio": ("LMSTUDIO_API_KEY",), + # #3145 — provider detection treats OPENCODE_API_KEY as enabling both + # OpenCode Zen and OpenCode Go. The runtime-facing lookup must read the same + # shared bridge key after the provider-specific slot, otherwise Settings can + # show the groups as configured while chat fails the no-key path. + "opencode-zen": ("OPENCODE_API_KEY",), + "opencode-go": ("OPENCODE_API_KEY",), } # Providers that use OAuth or token flows — their credentials are managed diff --git a/tests/test_issue3145_opencode_shared_runtime_key.py b/tests/test_issue3145_opencode_shared_runtime_key.py new file mode 100644 index 00000000..34875cdb --- /dev/null +++ b/tests/test_issue3145_opencode_shared_runtime_key.py @@ -0,0 +1,56 @@ +"""Regression tests for shared OpenCode API key runtime lookup (#3145).""" + +import pytest + + +@pytest.mark.parametrize("provider_id", ["opencode-zen", "opencode-go"]) +def test_shared_opencode_api_key_resolves_for_runtime(monkeypatch, tmp_path, provider_id): + """Runtime-facing key lookup should honor OPENCODE_API_KEY for both groups.""" + import api.providers as providers + + hermes_home = tmp_path / "hermes-home" + hermes_home.mkdir() + (hermes_home / ".env").write_text("OPENCODE_API_KEY=shared-opencode-key\n", encoding="utf-8") + + monkeypatch.setattr(providers, "_get_hermes_home", lambda: hermes_home) + monkeypatch.delenv("OPENCODE_ZEN_API_KEY", raising=False) + monkeypatch.delenv("OPENCODE_GO_API_KEY", raising=False) + monkeypatch.delenv("OPENCODE_API_KEY", raising=False) + + assert providers._provider_has_key(provider_id) is True + assert providers._get_provider_api_key(provider_id) == "shared-opencode-key" + + +@pytest.mark.parametrize("provider_id", ["opencode-zen", "opencode-go"]) +def test_shared_opencode_api_key_resolves_from_process_env(monkeypatch, tmp_path, provider_id): + """The shared env var should work even when no .env file exists.""" + import api.providers as providers + + monkeypatch.setattr(providers, "_get_hermes_home", lambda: tmp_path / "missing-home") + monkeypatch.delenv("OPENCODE_ZEN_API_KEY", raising=False) + monkeypatch.delenv("OPENCODE_GO_API_KEY", raising=False) + monkeypatch.setenv("OPENCODE_API_KEY", "shared-env-opencode-key") + + assert providers._provider_has_key(provider_id) is True + assert providers._get_provider_api_key(provider_id) == "shared-env-opencode-key" + + +def test_provider_specific_opencode_key_still_wins_over_shared(monkeypatch, tmp_path): + """Provider-specific env vars keep precedence over the shared fallback.""" + import api.providers as providers + + hermes_home = tmp_path / "hermes-home" + hermes_home.mkdir() + (hermes_home / ".env").write_text( + "OPENCODE_API_KEY=shared-opencode-key\n" + "OPENCODE_ZEN_API_KEY=zen-specific-key\n", + encoding="utf-8", + ) + + monkeypatch.setattr(providers, "_get_hermes_home", lambda: hermes_home) + monkeypatch.delenv("OPENCODE_API_KEY", raising=False) + monkeypatch.delenv("OPENCODE_ZEN_API_KEY", raising=False) + monkeypatch.delenv("OPENCODE_GO_API_KEY", raising=False) + + assert providers._get_provider_api_key("opencode-zen") == "zen-specific-key" + assert providers._get_provider_api_key("opencode-go") == "shared-opencode-key"