Verified on 2026-06-23 against:
- OpenCode docs: https://opencode.ai/docs/sdk/
- OpenCode server docs: https://opencode.ai/docs/server/
- OpenCode CLI docs: https://opencode.ai/docs/cli/
- OpenCode config docs: https://opencode.ai/docs/config/
- OpenCode tools docs: https://opencode.ai/docs/tools/
- npm packages:
@opencode-ai/sdk@1.17.9andopencode-ai@1.17.9
Add a direct @headless-coder-sdk/opencode-adapter package instead of replacing the removed protocol server with another server bridge.
The best fit is the OpenCode server SDK path:
- Use
createOpencode()when the adapter should own a local OpenCode server lifecycle. - Use
createOpencodeClient({ baseUrl })when callers provide an existingopencode serveinstance. - Use
opencode run --format jsononly as a fallback path for simple one-shot runs, not as the primary adapter.
The SDK path matches our current adapter model better because it exposes sessions, message prompts, async prompting, session aborts, message history, typed server-sent events, config, MCP status, tool metadata, and file/project APIs.
For implementation, prefer the exported @opencode-ai/sdk/v2 surface if structured output needs to work on day one. The default documented client is simpler, but the packed v1 generated types do not include the documented format field. The shipped v2 types do expose format?: OutputFormat on prompt input and richer message/event shapes.
Create a new workspace package:
packages/opencode-adapter/
├── package.json
├── tsup.config.ts
├── src/index.ts
└── test/stream-events.test.ts
Exports should mirror the other adapters:
OPENCODE_CODER_NAME = "opencode"createAdapter()createHeadlessOpenCode()DEFAULT_MODELonly if we want an SDK-level default; otherwise let OpenCode resolve the configured model.
Dependencies:
- runtime dependency or peer dependency:
@opencode-ai/sdk - optional peer/runtime dependency for CLI fallback:
opencode-ai - peer dependency:
@headless-coder-sdk/core
Use existing StartOpts first and add OpenCode-specific passthrough under providerOptions.opencode.
Suggested supported options:
model: accept OpenCode'sprovider/modelformat. Split into{ providerID, modelID }forclient.session.prompt().workingDirectory: pass as SDK requestquery.directory; for owned servers also pass the clientdirectoryoption.systemPrompt: map tobody.system.allowedTools: map to OpenCode promptbody.toolswhere possible.permissionMode: map to OpenCode configpermissionor CLI--dangerously-skip-permissionsfor CLI fallback.outputSchema: map to v2body.format = { type: "json_schema", schema, retryCount }. The docs show this field on the default client, but the packed v1 types do not expose it.signalandthread.interrupt(): callclient.session.abort(...)and emit/throw the same interrupted shape used by the other adapters.
Suggested providerOptions.opencode fields:
baseUrlhostnameporttimeoutconfigclientConfigagenttoolsmessageIdFactoryuseAsyncPromptcliPathcliArgs
startThread(opts):
- Ensure an SDK client exists.
- Call
client.session.create({ body: { title }, query: { directory } }). - Return a
ThreadHandlekeyed by the OpenCode session id.
Keep a small local helper for session paths because the documented default client uses path: { id }, while the packaged v2 generated types use path: { sessionID }.
resumeThread(id):
- Optionally validate with
client.session.get({ path: { id }, query: { directory } }). - Return a
ThreadHandleusing the supplied session id.
run(input, opts):
- Convert
PromptInputto OpenCodeparts. - Call
client.session.prompt({ path, query: { directory }, body }). - Combine returned text parts into
RunResult.text. - Map
AssistantMessage.tokensandcostintoRunResult.usageandRunResult.raw.
runStreamed(input, opts):
- Subscribe with
client.event.subscribe(). - Filter events to the active
sessionID. - Start the prompt with
client.session.promptAsync()or runclient.session.prompt()while consuming events. - Yield normalized
CoderStreamEventvalues untilsession.idle,message.updatedcompletion,session.error, abort, or request completion.
Initial event mapping should be fixture-driven from @opencode-ai/sdk@1.17.9 generated event types:
server.connected->initmessage.part.updatedwith text part ->messagemessage.part.updatedwith reasoning part ->progressmessage.part.updatedwith tool part ->tool_useortool_resultdepending on tool statepermission.updated->permissionfile.edited,session.diff, patch parts ->file_changetodo.updated->plan_updatecommand.executed->progressortool_usesession.errorand assistant message errors ->errorsession.idleor completed prompt result ->done
Always preserve the raw OpenCode event on originalItem.
OpenCode prompt input accepts:
TextPartInputFilePartInputAgentPartInputSubtaskPartInput
Map existing SDK prompt parts conservatively:
- string input -> one
textpart - text content part ->
text - local image/file content part ->
fileonly when it can be represented as a URL accepted by OpenCode; otherwise throw a clear unsupported-input error
Add three test layers:
- Unit tests for prompt conversion and model splitting.
- Unit tests for event normalization using saved OpenCode SSE fixtures.
- Live tests gated behind
OPENCODE_LIVE=1, with skips whenopencode-aior credentials are unavailable.
The normal workspace build, test, and smoke commands should pass without requiring OpenCode live credentials.
- Whether
@opencode-ai/sdk/v2should be treated as the primary import despite the docs still showing the default client path. - Whether the adapter should default to launching a managed server or require
baseUrlfor production use. - How much of OpenCode's dynamic MCP configuration should be exposed directly versus left to
opencode.json.