From 81b1ed84162e8d6ea5bb63bc71c19ff1892f7c8a Mon Sep 17 00:00:00 2001 From: Nat Welch Date: Sat, 17 Oct 2026 12:33:23 +0000 Subject: [PATCH] feat: allow anthropic and openai api key providers Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++++ compose.yml | 2 ++ opencode.jsonc | 10 ++++++++++ scripts/test-router-integration.mjs | 6 +++++- tests/routing.test.mjs | 2 ++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b72696d..d416f65 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,14 @@ Connect providers in OpenCode or run: ```sh docker compose exec opencode opencode auth login github-copilot --server http://127.0.0.1:4096 docker compose exec opencode opencode auth login google --server http://127.0.0.1:4096 +docker compose exec opencode opencode auth login anthropic --method key --server http://127.0.0.1:4096 +docker compose exec opencode opencode auth login openai --server http://127.0.0.1:4096 ``` - **GitHub Copilot:** complete device login with a subscribed account. - **Gemini:** choose the API-key method or set `GOOGLE_GENERATIVE_AI_API_KEY`. Organization-backed Code Assist OAuth also needs `OPENCODE_GEMINI_PROJECT_ID`; see the [Gemini plugin's account requirements](https://github.com/jenslys/opencode-gemini-auth). +- **Anthropic:** enter a [Console API key](https://console.anthropic.com/settings/keys) or set `ANTHROPIC_API_KEY`. Claude Pro and Max subscriptions do not work outside Claude Code. +- **OpenAI:** choose the API-key method with a [platform key](https://platform.openai.com/api-keys) or set `OPENAI_API_KEY`. - **Git access:** set `GH_TOKEN` or run `docker compose exec opencode gh auth login --hostname github.com --git-protocol https --web`. This is separate from Copilot login; stored credentials persist in `/data/.config/gh`. - **WakaTime:** put your API key in `/data/.wakatime.cfg`, owned by `1000:1000` with mode `600`, using the [plugin's configuration format](https://github.com/angristan/opencode-wakatime#prerequisites). diff --git a/compose.yml b/compose.yml index 4cf4d4a..944057a 100644 --- a/compose.yml +++ b/compose.yml @@ -20,6 +20,8 @@ services: KARAKEEP_API_ADDR: ${KARAKEEP_API_ADDR:-} KARAKEEP_API_KEY: ${KARAKEEP_API_KEY:-} OPENCODE_PASSWORD: ${OPENCODE_PASSWORD:?Set OPENCODE_PASSWORD} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} volumes: - data:/data - type: bind diff --git a/opencode.jsonc b/opencode.jsonc index 93dbc80..8efb645 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -143,6 +143,16 @@ "action": "provider.use", "resource": "github-copilot", "effect": "allow" + }, + { + "action": "provider.use", + "resource": "anthropic", + "effect": "allow" + }, + { + "action": "provider.use", + "resource": "openai", + "effect": "allow" } ] } diff --git a/scripts/test-router-integration.mjs b/scripts/test-router-integration.mjs index 58a7355..5c6eac3 100644 --- a/scripts/test-router-integration.mjs +++ b/scripts/test-router-integration.mjs @@ -86,6 +86,10 @@ try { assert.ok(google.methods.some(m => m.type === "oauth" && m.id === "gemini-cli")); const copilot = (await request("/api/integration/github-copilot")).data; assert.ok(copilot.methods.some(m => m.type === "oauth")); + for (const provider of ["anthropic", "openai"]) { + const { methods } = (await request(`/api/integration/${provider}`)).data; + assert.ok(methods.some(m => m.type === "key"), `${provider} accepts an API key: ${JSON.stringify(methods)}`); + } const agents = (await request("/api/agent")).data; const lead = agents.find(a => a.id === "build"); assert.ok(lead, "native build agent is available"); @@ -134,7 +138,7 @@ try { }); // Without credentials, a dropped provider policy shows up as OpenCode's free models. const models = (await request("/api/model")).data; - assert.ok(models.every(m => ["google", "github-copilot"].includes(m.providerID)), `policies hide other providers' models: ${models.map(m => m.providerID)}`); + assert.ok(models.every(m => ["google", "github-copilot", "anthropic", "openai"].includes(m.providerID)), `policies hide other providers' models: ${models.map(m => m.providerID)}`); session = (await request("/api/session", { title: "V2 integration test", agent: "build", location: { directory } })).data; await request(`/api/session/${session.id}/command`, { name: "quota_announcements", text: "" }); diff --git a/tests/routing.test.mjs b/tests/routing.test.mjs index d13746b..c3c1e83 100644 --- a/tests/routing.test.mjs +++ b/tests/routing.test.mjs @@ -41,6 +41,8 @@ test("image-owned skills are discoverable", async () => { test("existing providers, MCP integrations and quota are preserved", () => { assert.deepEqual(Object.keys(config.mcp.servers).sort(), ["context7", "grafana", "karakeep", "lunchmoney", "notion", "robinhood"]); assert.ok(config.plugins.includes("@slkiser/opencode-quota@5.0.2")); + const allowed = config.policies.filter(p => p.action === "provider.use" && p.effect === "allow").map(p => p.resource); + assert.deepEqual(allowed.sort(), ["anthropic", "github-copilot", "google", "openai"]); assert.ok(!config.permissions?.some(rule => rule.action === "*" && rule.effect === "allow")); assert.deepEqual(config.agents.build.permissions[0], { action: "*", resource: "*", effect: "allow" }); for (const name of ["general", "explore"])