-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat: add OpenCode community provider #1372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
choufeng
wants to merge
1
commit into
coleam00:dev
Choose a base branch
from
choufeng:feat/opencode-provider
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
106 changes: 106 additions & 0 deletions
106
docs/superpowers/specs/2026-04-23-opencode-provider-design.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| # OpenCode Community Provider Design | ||
|
|
||
| > Date: 2026-04-23 | ||
| > Author: choufeng | ||
| > Reference: Issue #1151 | ||
|
|
||
| ## Goal | ||
|
|
||
| Add OpenCode as a community provider to Archon, enabling users to use OpenCode's AI coding agent as a backend for Archon workflows. | ||
|
|
||
| ## Architecture | ||
|
|
||
| OpenCode is a client/server AI coding agent (unlike Claude Code/Codex which are monolithic CLIs). The provider will: | ||
|
|
||
| 1. **Lazy-start** an OpenCode Server (`opencode serve`) on first use | ||
| 2. **Connect** via `@opencode-ai/sdk` using `createOpencodeClient` | ||
| 3. **Bridge** SSE events to Archon's `MessageChunk` async generator contract | ||
| 4. **Manage** session lifecycle (create/resume/abort) | ||
|
|
||
| ## Key Differences from Pi Provider | ||
|
|
||
| | Aspect | Pi | OpenCode | | ||
| |--------|-----|----------| | ||
| | SDK loading | Dynamic import with `PI_PACKAGE_DIR` shim | Clean dynamic import | | ||
| | Auth | OAuth + API key file (`~/.pi/agent/auth.json`) | HTTP Basic Auth (`OPENCODE_SERVER_PASSWORD`) | | ||
| | Model refs | `<pi-provider>/<model-id>` | `<provider-id>/<model-id>` | | ||
| | Session storage | `~/.pi/agent/sessions/` (filesystem) | OpenCode Server internal | | ||
| | Structured output | Prompt engineering (best-effort) | SDK native support | | ||
| | MCP | Not supported | Native support | | ||
| | Server management | None (library call) | Must manage `opencode serve` lifecycle | | ||
|
|
||
| ## File Structure | ||
|
|
||
| ``` | ||
| packages/providers/src/community/opencode/ | ||
| ├── provider.ts # OpenCodeProvider class | ||
| ├── capabilities.ts # OPENCODE_CAPABILITIES | ||
| ├── config.ts # parseOpencodeConfig | ||
| ├── server-manager.ts # OpenCode Server lifecycle | ||
| ├── event-bridge.ts # SSE Event → MessageChunk | ||
| ├── registration.ts # registerOpencodeProvider() | ||
| ├── index.ts # Public exports | ||
| ├── provider.test.ts # Tests | ||
| └── config.test.ts # Config tests | ||
| ``` | ||
|
|
||
| ## Capability Declaration (Honest) | ||
|
|
||
| ```typescript | ||
| export const OPENCODE_CAPABILITIES: ProviderCapabilities = { | ||
| sessionResume: true, // ✅ OpenCode sessions have IDs | ||
| mcp: true, // ✅ Native MCP support | ||
| hooks: false, // ❌ Archon hooks ≠ OpenCode plugins | ||
| skills: true, // ✅ Via systemPrompt injection | ||
| agents: false, // ❌ No inline sub-agent definitions | ||
| toolRestrictions: true, // ✅ Via tools whitelist/blacklist | ||
| structuredOutput: true, // ✅ SDK native JSON Schema support | ||
| envInjection: true, // ✅ Via request options | ||
| costControl: false, // ❌ No cost limit API | ||
| effortControl: true, // ✅ Via reasoning effort | ||
| thinkingControl: true, // ✅ Via reasoning toggle | ||
| fallbackModel: false, // ❌ No automatic fallback | ||
| sandbox: false, // ❌ No sandbox support | ||
| }; | ||
| ``` | ||
|
|
||
| ## Event Bridge Mapping | ||
|
|
||
| | OpenCode Event | Archon MessageChunk | | ||
| |----------------|---------------------| | ||
| | `message.part.updated` (text delta) | `assistant` | | ||
| | `message.part.updated` (reasoning) | `thinking` | | ||
| | `message.part.updated` (tool call) | `tool` | | ||
| | `message.updated` (assistant complete) | `result` (with tokens) | | ||
| | `session.error` | `result` (isError: true) | | ||
| | `message.part.updated` (step-finish) | `result` (with cost/tokens) | | ||
|
|
||
| ## Server Lifecycle | ||
|
|
||
| 1. On first `sendQuery()`, check if OpenCode Server is running (health check) | ||
| 2. If not running, spawn `opencode serve --port <port> --hostname <hostname>` | ||
| 3. Wait for health check to pass (timeout: 30s) | ||
| 4. Create SDK client connected to the server | ||
| 5. Server process follows Archon process lifecycle (not detached) | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```yaml | ||
| # .archon/config.yaml | ||
| assistants: | ||
| opencode: | ||
| model: anthropic/claude-sonnet-4 | ||
| hostname: 127.0.0.1 | ||
| port: 4096 | ||
| autoStartServer: true | ||
| ``` | ||
|
|
||
| ## Cross-Cutting Changes | ||
|
|
||
| 1. `packages/providers/package.json` - Add `@opencode-ai/sdk` dependency | ||
| 2. `packages/providers/src/registry.ts` - Add `registerOpencodeProvider()` call | ||
| 3. `packages/providers/package.json` scripts - Add test command | ||
|
|
||
| No changes to: | ||
| - `AssistantDefaultsConfig` or `AssistantDefaults` (community provider defaults live behind `[string]` index) | ||
| - CLI or server entrypoints (use aggregator pattern) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import type { ProviderCapabilities } from '../../types'; | ||
|
|
||
| /** | ||
| * OpenCode capabilities — intentionally conservative. Declared flags must | ||
| * reflect wired-up behavior, not potential support. The dag-executor uses | ||
| * these to warn users when a workflow node specifies a feature the provider | ||
| * ignores. | ||
| * | ||
| * OpenCode is a client/server AI coding agent with native MCP support, | ||
| * structured output, and session management. Unlike Pi, it does not require | ||
| * package.json shims or filesystem session stores. | ||
| */ | ||
| export const OPENCODE_CAPABILITIES: ProviderCapabilities = { | ||
| sessionResume: true, | ||
| mcp: true, | ||
| hooks: false, | ||
| skills: true, | ||
| agents: false, | ||
| toolRestrictions: true, | ||
| structuredOutput: true, | ||
| envInjection: true, | ||
| costControl: false, | ||
| effortControl: true, | ||
| thinkingControl: true, | ||
| fallbackModel: false, | ||
| sandbox: false, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { describe, test, expect } from 'bun:test'; | ||
| import { parseOpencodeConfig } from './config'; | ||
|
|
||
| describe('parseOpencodeConfig', () => { | ||
| test('returns empty object for empty input', () => { | ||
| const result = parseOpencodeConfig({}); | ||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| test('parses model string', () => { | ||
| const result = parseOpencodeConfig({ model: 'anthropic/claude-sonnet-4' }); | ||
| expect(result.model).toBe('anthropic/claude-sonnet-4'); | ||
| }); | ||
|
|
||
| test('parses hostname', () => { | ||
| const result = parseOpencodeConfig({ hostname: '0.0.0.0' }); | ||
| expect(result.hostname).toBe('0.0.0.0'); | ||
| }); | ||
|
|
||
| test('parses port', () => { | ||
| const result = parseOpencodeConfig({ port: 8080 }); | ||
| expect(result.port).toBe(8080); | ||
| }); | ||
|
|
||
| test('parses serverPassword', () => { | ||
| const result = parseOpencodeConfig({ serverPassword: 'secret123' }); | ||
| expect(result.serverPassword).toBe('secret123'); | ||
| }); | ||
|
|
||
| test('parses autoStartServer', () => { | ||
| const result = parseOpencodeConfig({ autoStartServer: false }); | ||
| expect(result.autoStartServer).toBe(false); | ||
| }); | ||
|
|
||
| test('ignores invalid fields', () => { | ||
| const result = parseOpencodeConfig({ | ||
| model: 'anthropic/claude-sonnet-4', | ||
| port: 'not-a-number', | ||
| autoStartServer: 'yes', | ||
| unknownField: 'ignored', | ||
| } as Record<string, unknown>); | ||
| expect(result.model).toBe('anthropic/claude-sonnet-4'); | ||
| expect(result.port).toBeUndefined(); | ||
| expect(result.autoStartServer).toBeUndefined(); | ||
| expect(result.unknownField).toBeUndefined(); | ||
| }); | ||
|
|
||
| test('parses full config', () => { | ||
| const result = parseOpencodeConfig({ | ||
| model: 'openai/gpt-5', | ||
| hostname: '127.0.0.1', | ||
| port: 4096, | ||
| serverPassword: 'my-password', | ||
| autoStartServer: true, | ||
| }); | ||
| expect(result).toEqual({ | ||
| model: 'openai/gpt-5', | ||
| hostname: '127.0.0.1', | ||
| port: 4096, | ||
| serverPassword: 'my-password', | ||
| autoStartServer: true, | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import type { ProviderDefaults } from '../../types'; | ||
|
|
||
| export interface OpencodeProviderDefaults extends ProviderDefaults { | ||
| /** Default model ref in '<provider-id>/<model-id>' format, e.g. 'anthropic/claude-sonnet-4' */ | ||
| model?: string; | ||
| /** OpenCode Server hostname. @default '127.0.0.1' */ | ||
| hostname?: string; | ||
| /** OpenCode Server port. @default 4096 */ | ||
| port?: number; | ||
| /** OpenCode Server password for HTTP Basic Auth. If unset, auto-generated. */ | ||
| serverPassword?: string; | ||
| /** Auto-start OpenCode Server on first use. @default true */ | ||
| autoStartServer?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Parse raw YAML-derived config into typed OpenCode defaults. | ||
| * Defensive: invalid fields are dropped silently. | ||
| */ | ||
| export function parseOpencodeConfig(raw: Record<string, unknown>): OpencodeProviderDefaults { | ||
| const result: OpencodeProviderDefaults = {}; | ||
|
|
||
| if (typeof raw.model === 'string') { | ||
| result.model = raw.model; | ||
| } | ||
|
|
||
| if (typeof raw.hostname === 'string') { | ||
| result.hostname = raw.hostname; | ||
| } | ||
|
|
||
| if (typeof raw.port === 'number') { | ||
| result.port = raw.port; | ||
| } | ||
|
|
||
| if (typeof raw.serverPassword === 'string') { | ||
| result.serverPassword = raw.serverPassword; | ||
| } | ||
|
|
||
| if (typeof raw.autoStartServer === 'boolean') { | ||
| result.autoStartServer = raw.autoStartServer; | ||
| } | ||
|
|
||
| return result; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Does opencode server spawned viaopencode serveenforce the OPENCODE_SERVER_PASSWORD env variable on HTTP requests, and how is it passed by@opencode-ai/sdkcreateOpencodeClient?💡 Result:
Yes, the opencode server spawned via opencode serve enforces the OPENCODE_SERVER_PASSWORD environment variable on HTTP requests using HTTP Basic Authentication. When set, the server requires clients to provide the correct username (default "opencode", override with OPENCODE_SERVER_USERNAME) and password via the Authorization header. Without valid credentials, requests return 401 Unauthorized. The
@opencode-ai/sdk's createOpencodeClient does not automatically read or pass the OPENCODE_SERVER_PASSWORD. It supports custom fetch implementations and headers (via options like fetch or headers in wrappers), allowing manual addition of Basic Auth. Internal OpenCode clients (TUI, plugins, run mode) add the header when the env var is set, but plain SDK clients require explicit configuration, such as: const credentials = btoa(opencode:${process.env.OPENCODE_SERVER_PASSWORD}); const client = createOpencodeClient({ baseUrl: "http://localhost:4096", headers: { Authorization: Basic ${credentials} } }); SDK docs list options like baseUrl, fetch, etc., but not auth directly; Basic Auth is handled via headers or custom fetch interceptors. Provider auth (e.g., client.auth.set for Anthropic API keys) is separate from server auth.Citations:
createOpenCodeClientoptions ben-vargas/ai-sdk-provider-opencode-sdk#5🏁 Script executed:
Repository: coleam00/Archon
Length of output: 243
🏁 Script executed:
Repository: coleam00/Archon
Length of output: 118
🏁 Script executed:
cat -n "packages/providers/src/community/opencode/provider.ts"Repository: coleam00/Archon
Length of output: 6643
🏁 Script executed:
cat -n "packages/providers/src/community/opencode/server-manager.ts"Repository: coleam00/Archon
Length of output: 4874
Wire HTTP Basic Auth into the OpenCode SDK client.
The password is correctly spawned into
OPENCODE_SERVER_PASSWORDinserver-manager.ts(line 70), and the web search confirms the server enforces HTTP Basic Auth. However,provider.ts(lines 58–60) creates the SDK client with onlybaseUrl, omitting anyAuthorizationheader. The password retrieved on lines 50–51 is never passed tocreateOpencodeClient(), so every request will fail with 401 Unauthorized.Update the client creation to include the auth header:
Example fix
🤖 Prompt for AI Agents