Skip to content

Commit 0d18dff

Browse files
authored
Merge pull request #2 from weizhoublue/pr/env
env OPENCODE_API_KEY prioritizes over the stored api key
2 parents 940c387 + c4e7dad commit 0d18dff

7 files changed

Lines changed: 380 additions & 10 deletions

File tree

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: welan release
1+
name: a welan release
22

33
# Fork patch author: only contiguous commits at the tip of origin/welan authored by this
44
# name are cherry-picked onto the upstream release tag.
@@ -195,6 +195,11 @@ jobs:
195195
env:
196196
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false"
197197

198+
- name: Run env key priority tests
199+
working-directory: packages/opencode
200+
timeout-minutes: 5
201+
run: bun test --timeout 60000 test/provider/provider-env-key-priority.test.ts
202+
198203
- name: Build CLI binaries
199204
run: OPENCODE_VERSION="$BUILD_VERSION" ./packages/opencode/script/build.ts
200205

@@ -224,16 +229,10 @@ jobs:
224229
tag="$RELEASE_TAG"
225230
target="$(git rev-parse HEAD)"
226231
227-
if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then
228-
gh release delete "$tag" --repo "${{ github.repository }}" --yes
229-
fi
230-
231-
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then
232-
git push origin ":refs/tags/$tag"
233-
fi
232+
gh release delete "$tag" --repo "${{ github.repository }}" --yes 2>/dev/null || true
234233
235234
git tag -f "$tag" "$target"
236-
git push origin "refs/tags/$tag"
235+
git push origin "refs/tags/$tag" --force
237236
238237
gh release create "$tag" \
239238
/tmp/welan-release-assets/opencode-darwin-arm64 \

.github/workflows/test-linux.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: welan-test-pr
2+
3+
on:
4+
push:
5+
branches: [welan]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
checks: write
16+
17+
jobs:
18+
test:
19+
name: unit (linux)
20+
runs-on: ubuntu-24.04
21+
steps:
22+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
23+
24+
- uses: ./.github/actions/setup-bun
25+
26+
- name: Configure git identity
27+
run: |
28+
git config --global user.email "bot@opencode.ai"
29+
git config --global user.name "opencode"
30+
31+
- name: Run opencode tests
32+
working-directory: packages/opencode
33+
timeout-minutes: 20
34+
run: |
35+
find test \( -name '*.test.ts' -o -name '*.test.tsx' \) ! -name 'run-process*.test.ts' -print0 \
36+
| xargs -0 bun test --timeout 60000
37+
bun test --timeout 60000 --max-concurrency=1 test/cli/run/run-process-limit.test.ts
38+
bun test --timeout 60000 --max-concurrency=1 test/cli/run/run-process.test.ts \
39+
--test-name-pattern='exits 0 and writes|prints each|prints reasoning|unknown stream|format json|rejects requested|attach mode|SIGINT'
40+
env:
41+
GITHUB_ACTIONS: "false"
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Env Var API Key Priority Over Stored Key Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make environment variable API keys (e.g. `OPENCODE_API_KEY`) take precedence over API keys stored via the interactive UI, so users can switch keys at launch time without touching stored configuration.
6+
7+
**Architecture:** In the provider loading sequence inside `provider.ts`, the `// load apikeys` section currently runs after `// load env` and overwrites whatever env var key was loaded. The fix adds a 2-line guard: before merging a stored API key into a provider, check whether that provider's env vars already provided a key. If so, skip the stored key.
8+
9+
**Tech Stack:** TypeScript, Effect, Bun test runner
10+
11+
## Global Constraints
12+
13+
- Run tests from `packages/opencode`, never from repo root
14+
- Use `bun test` (not `bun run test`) to run tests
15+
- Use `bun typecheck` (not `tsc`) for type checking
16+
- Do not move or restructure code blocks — only add the guard inside the existing loop
17+
18+
---
19+
20+
### Task 1: Write the failing test
21+
22+
**Files:**
23+
- Modify: `packages/opencode/test/provider/provider.test.ts` (append at end of file)
24+
25+
**Interfaces:**
26+
- Consumes: `Auth.Service` (already imported as `import { Auth } from "@/auth"`), `set` helper (already defined at line 36), `Global` from `@opencode-ai/core/global`, `Filesystem` from `@/util/filesystem`
27+
- Produces: test case `"env var takes precedence over stored API key"`
28+
29+
- [ ] **Step 1: Append the new test to the test file**
30+
31+
Open `packages/opencode/test/provider/provider.test.ts` and append at the very end:
32+
33+
```typescript
34+
it.instance("env var takes precedence over stored API key", () =>
35+
Effect.gen(function* () {
36+
// Set a stored API key for anthropic
37+
const auth = yield* Auth.Service
38+
yield* auth.set("anthropic", { type: "api", key: "stored-key" })
39+
40+
// Set an env var key — this should win
41+
yield* set("ANTHROPIC_API_KEY", "env-key")
42+
43+
const providers = yield* list
44+
const anthropic = providers[ProviderV2.ID.anthropic]
45+
expect(anthropic).toBeDefined()
46+
// The loaded key should be the env var, not the stored key
47+
expect(anthropic.key).toBe("env-key")
48+
}),
49+
)
50+
```
51+
52+
- [ ] **Step 2: Run the test to verify it fails**
53+
54+
```bash
55+
cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key"
56+
```
57+
58+
Expected: test **FAILS** — currently `anthropic.key` will be `"stored-key"` (stored key wins over env).
59+
60+
---
61+
62+
### Task 2: Implement the fix
63+
64+
**Files:**
65+
- Modify: `packages/opencode/src/provider/provider.ts:1501-1512`
66+
67+
**Interfaces:**
68+
- Consumes: `database` (already in scope — the provider registry with `env` arrays), `envs` (already in scope — loaded env vars map)
69+
- Produces: modified `// load apikeys` loop that skips stored keys when env var is already providing a key
70+
71+
- [ ] **Step 1: Locate the `// load apikeys` loop**
72+
73+
In `packages/opencode/src/provider/provider.ts`, find this block (around line 1501):
74+
75+
```typescript
76+
// load apikeys
77+
const auths = yield* auth.all().pipe(Effect.orDie)
78+
for (const [id, provider] of Object.entries(auths)) {
79+
const providerID = ProviderV2.ID.make(id)
80+
if (disabled.has(providerID)) continue
81+
if (provider.type === "api") {
82+
mergeProvider(providerID, {
83+
source: "api",
84+
key: provider.key,
85+
})
86+
}
87+
}
88+
```
89+
90+
- [ ] **Step 2: Add the env-priority guard**
91+
92+
Replace only the inner `if (provider.type === "api")` block:
93+
94+
```typescript
95+
// load apikeys
96+
const auths = yield* auth.all().pipe(Effect.orDie)
97+
for (const [id, provider] of Object.entries(auths)) {
98+
const providerID = ProviderV2.ID.make(id)
99+
if (disabled.has(providerID)) continue
100+
if (provider.type === "api") {
101+
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
102+
if (envKey) continue
103+
mergeProvider(providerID, {
104+
source: "api",
105+
key: provider.key,
106+
})
107+
}
108+
}
109+
```
110+
111+
The two new lines are:
112+
1. `const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)` — reuse the same env-var lookup already used in `// load env`
113+
2. `if (envKey) continue` — skip stored key when env var is present
114+
115+
- [ ] **Step 3: Run type check**
116+
117+
```bash
118+
cd packages/opencode && bun typecheck
119+
```
120+
121+
Expected: no errors.
122+
123+
- [ ] **Step 4: Run the new test to verify it passes**
124+
125+
```bash
126+
cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key"
127+
```
128+
129+
Expected: **PASS**
130+
131+
- [ ] **Step 5: Run the full provider test suite to check for regressions**
132+
133+
```bash
134+
cd packages/opencode && bun test test/provider/provider.test.ts
135+
```
136+
137+
Expected: all existing tests **PASS**.
138+
139+
- [ ] **Step 6: Commit**
140+
141+
```bash
142+
git add packages/opencode/src/provider/provider.ts packages/opencode/test/provider/provider.test.ts
143+
git commit -m "fix(core): env var api key takes precedence over stored key"
144+
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Design: Env Var API Key Takes Precedence Over UI-Stored Key
2+
3+
**Date**: 2026-07-07
4+
**Scope**: `packages/opencode/src/provider/provider.ts`
5+
6+
## Problem
7+
8+
When a user configures an API key through the interactive UI (stored in the auth database), that stored key currently **overrides** any `OPENCODE_API_KEY` environment variable set at launch time. This makes it impossible to switch keys via the command line without modifying stored configuration.
9+
10+
The root cause: in the provider loading sequence, `// load env` runs before `// load apikeys`. Because `mergeDeep` is applied sequentially, the later call (`// load apikeys`) overwrites the `key` field set by the earlier `// load env` call.
11+
12+
## Goal
13+
14+
When a provider's environment variable (e.g. `OPENCODE_API_KEY`) is set at launch time, it must take precedence over any stored API key configured through the UI. This lets users switch keys by setting the env var without touching stored configuration.
15+
16+
## Design
17+
18+
**File**: `packages/opencode/src/provider/provider.ts`
19+
**Location**: `// load apikeys` loop (around line 1502)
20+
21+
In the `// load apikeys` loop, before merging a stored API key into the provider, check whether any of that provider's declared environment variables are already set. If so, skip the stored key — the env var wins.
22+
23+
```typescript
24+
// load apikeys
25+
const auths = yield* auth.all().pipe(Effect.orDie)
26+
for (const [id, provider] of Object.entries(auths)) {
27+
const providerID = ProviderV2.ID.make(id)
28+
if (disabled.has(providerID)) continue
29+
if (provider.type === "api") {
30+
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
31+
if (envKey) continue // env var takes precedence over stored key
32+
mergeProvider(providerID, {
33+
source: "api",
34+
key: provider.key,
35+
})
36+
}
37+
}
38+
```
39+
40+
The `database[providerID]?.env` array is the same list of env var names already used in the `// load env` section (e.g. `["OPENCODE_API_KEY"]` for the opencode provider). Re-using this lookup ensures the two sections stay in sync.
41+
42+
## Scope
43+
44+
This change applies to **all providers**, not just opencode. The policy — "an explicitly set runtime env var overrides persisted configuration" — is a sound general principle and reduces special-case branching.
45+
46+
## Non-Goals
47+
48+
- Does not affect the `opencode.ts` plugin's `hasKey` check (line 166), which independently determines whether to operate in public mode. That logic already checks `process.env.OPENCODE_API_KEY` first and remains correct.
49+
- Does not change behavior when no env var is set — stored keys continue to work as before.
50+
- Does not affect OAuth credentials; only `provider.type === "api"` (stored API keys) is touched.
51+
52+
## Merge Conflict Risk
53+
54+
Low. The change is a 2-line addition inside an existing loop. It does not move or restructure any code blocks, minimizing diff size and conflict surface when merging upstream changes.

packages/opencode/src/provider/provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,6 +1508,8 @@ const layer = Layer.effect(
15081508
const providerID = ProviderV2.ID.make(id)
15091509
if (disabled.has(providerID)) continue
15101510
if (provider.type === "api") {
1511+
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
1512+
if (envKey) continue
15111513
mergeProvider(providerID, {
15121514
source: "api",
15131515
key: provider.key,

0 commit comments

Comments
 (0)