OpenCode SDK for Elixir

Copy Markdown View Source

An unofficial Elixir SDK for OpenCode that mirrors the JS SDK (@opencode-ai/sdk). The client and types are generated from the OpenCode OpenAPI spec.

hex.pm link: https://hex.pm/packages/opencode_sdk/

Installation

Add opencode_sdk to your dependencies in mix.exs:

def deps do
  [
    {:opencode_sdk, "~> 0.1.91"}
  ]
end

Quickstart

Start an OpenCode server and get a connected client:

{:ok, %{client: client, server: server}} = OpenCode.create()

{:ok, health} = OpenCode.Generated.Operations.global_health(client)
IO.inspect(health, label: "health")
# => %{"healthy" => true, "version" => "1.1.53"}

OpenCode.close(%{server: server})

Connect to an existing server:

client = OpenCode.create_client(base_url: "http://127.0.0.1:4096")
{:ok, projects} = OpenCode.Generated.Operations.project_list(client)

Create API

OpenCode.create/1 options

OpenCode.create/1 forwards to OpenCode.create_server/1 and returns %{client, server}.

OptionTypeDescriptionDefault
:hostnameString.t()Server hostname"127.0.0.1"
:portinteger()Server port4096
:timeoutinteger()Startup timeout in ms5000
:configmap()Config passed via OPENCODE_CONFIG_CONTENT%{}

OpenCode.create_client/1 options

OptionTypeDescriptionDefault
:base_urlString.t()OpenCode server URL"http://127.0.0.1:4096"
:directoryString.t()Project directory sent via x-opencode-directorynil
:headersmap() | keyword()Extra HTTP headers[]
:timeoutinteger() | :infinityRequest timeout:infinity

Configuration

Pass a :config map to override settings. The server still reads your opencode.json, but inline config takes precedence:

{:ok, %{client: client, server: server}} =
  OpenCode.create(config: %{model: "opencode/big-pickle"})

API Reference

All operations live in OpenCode.Generated.Operations. The client keyword list is always the last argument.

Global

FunctionDescriptionResponse
global_health(client)Check server health and version%{"healthy" => true, "version" => "..."}
global_dispose(client)Shut down the serverboolean
global_config_get(client)Get global configConfig
global_config_update(body, client)Update global configConfig
{:ok, health} = Operations.global_health(client)
IO.puts(health["version"])

Sessions

FunctionDescriptionResponse
session_create(body, client)Create a new sessionSession
session_list(client)List all sessions[Session]
Session.session_get(id, client)Get a session by IDSession
Session.session_children(id, client)List child sessions[Session]
session_delete(id, client)Delete a sessionboolean
session_update(id, body, client)Update session propertiesSession
session_abort(id, client)Abort a running sessionboolean
session_share(id, client)Share a sessionSession
session_unshare(id, client)Unshare a sessionSession
session_summarize(id, body, client)Summarize a sessionboolean
# Create a session
{:ok, session} = Operations.session_create(%{title: "My session"}, client)

# List recent sessions (with optional filters)
{:ok, sessions} = Operations.session_list(Keyword.merge(client, limit: 10, search: "my"))

# Get a specific session
{:ok, session} = OpenCode.Generated.Session.session_get("session-id", client)

# Delete a session
{:ok, true} = Operations.session_delete("session-id", client)

Messages and prompts

FunctionDescriptionResponse
session_prompt(id, body, client)Send a prompt, get AI response%{"info" => AssistantMessage, "parts" => [Part]}
session_prompt_async(id, body, client)Send a prompt asynchronously:ok
session_messages(id, client)List messages in a session[%{"info" => Message, "parts" => [Part]}]
session_message(id, msg_id, client)Get a specific message%{"info" => Message, "parts" => [Part]}
session_command(id, body, client)Send a command to a session%{"info" => AssistantMessage, "parts" => [Part]}
session_shell(id, body, client)Run a shell commandAssistantMessage
session_diff(id, client)Get file diffs from a session[FileDiff]
session_revert(id, body, client)Revert a messageSession
session_unrevert(id, client)Restore reverted messagesSession
# Send a prompt
{:ok, result} =
  Operations.session_prompt(
    session["id"],
    %{parts: [%{type: "text", text: "Summarize this project in 3 bullets."}]},
    client
  )

# Extract text from the response
for %{"type" => "text", "text" => text} <- result["parts"] do
  IO.puts(text)
end

# Access token usage from the response info
info = result["info"]
IO.inspect(info["tokens"])
# => %{"input" => 1234, "output" => 567, ...}

# Specify a model in the prompt
{:ok, result} =
  Operations.session_prompt(
    session["id"],
    %{
      model: %{providerID: "opencode", modelID: "big-pickle"},
      parts: [%{type: "text", text: "Hello!"}]
    },
    client
  )

# Inject context without triggering AI response
{:ok, _} =
  Operations.session_prompt(
    session["id"],
    %{
      noReply: true,
      parts: [%{type: "text", text: "You are a helpful assistant."}]
    },
    client
  )

Response structure

session_prompt/3 returns {:ok, %{"info" => info, "parts" => parts}}:

  • info — assistant message metadata: "id", "role", "model_id", "provider_id", "cost", "tokens", "time".
  • parts — list of part maps, each with a "type" field:
Part typeKey fieldsDescription
"text""text"The assistant's text response
"tool-invocation""name", "args", "state", "result"A tool call and its result
"reasoning""text"Model reasoning/thinking
"step-start"—Start of a multi-step sequence
"step-finish"—End of a multi-step sequence
"file""filename", "url", "mime", "source"File attachment
"patch""files", "hash"File diff/patch

App

FunctionDescriptionResponse
app_agents(client)List available agents[Agent]
app_log(body, client)Write a log entryboolean
app_skills(client)List available skills[Skill]
{:ok, agents} = Operations.app_agents(client)

Operations.app_log(
  %{service: "my-app", level: "info", message: "Operation completed"},
  client
)
FunctionDescriptionResponse
file_list(client)List files in a path[FileNode]
file_read(client)Read file contentFileContent
file_status(client)Get git status of files[File]
find_files(client)Search files by name/pattern[String]
find_text(client)Search text with ripgrep[Match]
find_symbols(client)Search workspace symbols (LSP)[Symbol]
path_get(client)Get current path infoPath
# Search for text across the project
{:ok, results} = Operations.find_text(Keyword.merge(client, pattern: "defmodule"))

# Find files by pattern
{:ok, files} = Operations.find_files(Keyword.merge(client, query: "*.ex", type: "file"))

# Read a specific file
{:ok, content} = Operations.file_read(Keyword.merge(client, path: "lib/my_app.ex"))

# Get git status
{:ok, status} = Operations.file_status(client)

Config and providers

FunctionDescriptionResponse
config_get(client)Get configConfig
config_update(body, client)Update configConfig
config_providers(client)List providers and default models%{"providers" => [...], "default" => %{...}}
provider_list(client)List providers[Provider]
provider_auth(client)Get provider auth status—
{:ok, config} = Operations.config_get(client)
{:ok, %{"providers" => providers, "default" => defaults}} = Operations.config_providers(client)

Auth

FunctionDescriptionResponse
auth_set(providerID, body, client)Set auth credentialsboolean
auth_remove(providerID, client)Remove auth credentialsboolean
Operations.auth_set("anthropic", %{type: "api", key: "sk-..."}, client)

Events (SSE)

FunctionDescriptionResponse
event_subscribe(client)Subscribe to real-time events%{stream: Stream}
global_event(client)Subscribe to global events%{stream: Stream}
{:ok, %{stream: stream}} = Operations.event_subscribe(client)

Enum.each(stream, fn event ->
  IO.inspect(event, label: "event")
end)

Permissions and questions

FunctionDescriptionResponse
permission_list(client)List pending permissions[Permission]
permission_reply(id, body, client)Reply to a permission requestboolean
question_list(client)List pending questions[Question]
question_reply(id, body, client)Reply to a questionboolean
question_reject(id, client)Reject a questionboolean

MCP

FunctionDescriptionResponse
mcp_status(client)Get MCP server statusMcpStatus
mcp_add(body, client)Add an MCP server—
mcp_connect(name, client)Connect to an MCP server—
mcp_disconnect(name, client)Disconnect from an MCP server—
{:ok, mcp} = Operations.mcp_status(client)

PTY

FunctionDescriptionResponse
pty_list(client)List PTY sessions[Pty]
pty_create(body, client)Create a PTY sessionPty
pty_get(id, client)Get a PTY sessionPty
pty_remove(id, client)Remove a PTY sessionboolean

TUI

FunctionDescriptionResponse
tui_append_prompt(body, client)Append text to the promptboolean
tui_submit_prompt(client)Submit the current promptboolean
tui_clear_prompt(client)Clear the promptboolean
tui_execute_command(body, client)Execute a commandboolean
tui_show_toast(body, client)Show a toast notificationboolean
tui_open_help(client)Open help dialogboolean
tui_open_sessions(client)Open session selectorboolean
tui_open_models(client)Open model selectorboolean
tui_open_themes(client)Open theme selectorboolean
Operations.tui_append_prompt(%{text: "Add this to prompt"}, client)
Operations.tui_show_toast(%{message: "Done!", variant: "success"}, client)

TUI process lifecycle

{:ok, tui} = OpenCode.create_tui(project: "/path/to/project")
OpenCode.Tui.close(tui)

Error handling

All operations return {:ok, result} on success. Failures return {:error, {status, body}} for HTTP errors or {:error, reason} for connection issues:

case Operations.session_prompt(session_id, body, client) do
  {:ok, result} ->
    result

  {:error, {404, _body}} ->
    IO.puts("Session not found")

  {:error, {400, body}} ->
    IO.puts("Bad request: #{inspect(body)}")

  {:error, %Req.TransportError{reason: :econnrefused}} ->
    IO.puts("Cannot connect to server")
end

Examples

See the examples/ directory:

  • hello.exs — minimal example: start server, create session, send one prompt, print response.
  • chat.exs — interactive CLI chat REPL with session management, slash commands, and token usage display.

Run an example:

mix run examples/hello.exs
mix run examples/chat.exs

Types and Docs

All OpenAPI types are generated under OpenCode.Generated.* (for example, OpenCode.Generated.Session).

API functions and types are documented in generated module docs, primarily:

Regenerating

The OpenAPI spec is at priv/opencode_openapi.json. This SDK targets OpenCode's v1 API. The sync workflow follows v1/packages/sdk/openapi.json. Upstream dev now uses a different protocol API; its packages/protocol/openapi.json is not a compatible replacement. Generation requires Elixir 1.18 or later. Regenerate the client with:

mix opencode.gen.client --spec priv/opencode_openapi.json

Run mix test to check the SDK. Server integration tests require the opencode executable on PATH; CI uses the latest published OpenCode 1.x release and checks compilation and tests before publishing an upstream sync.

To publish SDK fixes without a spec change, manually run the Sync Upstream OpenAPI workflow with force_release enabled. It validates the SDK, bumps the patch version, and publishes to Hex. Scheduled runs only release when the spec changes.

Note

This project is unofficial and is not affiliated with the OpenCode team.