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/
Add opencode_sdk to your dependencies in mix.exs:
def deps do
[
{:opencode_sdk, "~> 0.1.91"}
]
endStart 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)OpenCode.create/1 forwards to OpenCode.create_server/1 and returns %{client, server}.
| Option | Type | Description | Default |
|---|---|---|---|
:hostname |
String.t() |
Server hostname | "127.0.0.1" |
:port |
integer() |
Server port | 4096 |
:timeout |
integer() |
Startup timeout in ms | 5000 |
:config |
map() |
Config passed via OPENCODE_CONFIG_CONTENT |
%{} |
| Option | Type | Description | Default |
|---|---|---|---|
:base_url |
String.t() |
OpenCode server URL | "http://127.0.0.1:4096" |
:directory |
String.t() |
Project directory sent via x-opencode-directory |
nil |
:headers |
map() | keyword() |
Extra HTTP headers | [] |
:timeout |
integer() | :infinity |
Request timeout | :infinity |
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"})All operations live in OpenCode.Generated.Operations. The client keyword list is always the last argument.
| Function | Description | Response |
|---|---|---|
global_health(client) |
Check server health and version | %{"healthy" => true, "version" => "..."} |
global_dispose(client) |
Shut down the server | boolean |
global_config_get(client) |
Get global config | Config |
global_config_update(body, client) |
Update global config | Config |
{:ok, health} = Operations.global_health(client)
IO.puts(health["version"])| Function | Description | Response |
|---|---|---|
session_create(body, client) |
Create a new session | Session |
session_list(client) |
List all sessions | [Session] |
Session.session_get(id, client) |
Get a session by ID | Session |
Session.session_children(id, client) |
List child sessions | [Session] |
session_delete(id, client) |
Delete a session | boolean |
session_update(id, body, client) |
Update session properties | Session |
session_abort(id, client) |
Abort a running session | boolean |
session_share(id, client) |
Share a session | Session |
session_unshare(id, client) |
Unshare a session | Session |
session_summarize(id, body, client) |
Summarize a session | boolean |
# 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)| Function | Description | Response |
|---|---|---|
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 command | AssistantMessage |
session_diff(id, client) |
Get file diffs from a session | [FileDiff] |
session_revert(id, body, client) |
Revert a message | Session |
session_unrevert(id, client) |
Restore reverted messages | Session |
# 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
)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 type | Key fields | Description |
|---|---|---|
"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 |
| Function | Description | Response |
|---|---|---|
app_agents(client) |
List available agents | [Agent] |
app_log(body, client) |
Write a log entry | boolean |
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
)| Function | Description | Response |
|---|---|---|
file_list(client) |
List files in a path | [FileNode] |
file_read(client) |
Read file content | FileContent |
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 info | Path |
# 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)| Function | Description | Response |
|---|---|---|
config_get(client) |
Get config | Config |
config_update(body, client) |
Update config | Config |
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)| Function | Description | Response |
|---|---|---|
auth_set(providerID, body, client) |
Set auth credentials | boolean |
auth_remove(providerID, client) |
Remove auth credentials | boolean |
Operations.auth_set("anthropic", %{type: "api", key: "sk-..."}, client)| Function | Description | Response |
|---|---|---|
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)| Function | Description | Response |
|---|---|---|
permission_list(client) |
List pending permissions | [Permission] |
permission_reply(id, body, client) |
Reply to a permission request | boolean |
question_list(client) |
List pending questions | [Question] |
question_reply(id, body, client) |
Reply to a question | boolean |
question_reject(id, client) |
Reject a question | boolean |
| Function | Description | Response |
|---|---|---|
mcp_status(client) |
Get MCP server status | McpStatus |
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)| Function | Description | Response |
|---|---|---|
pty_list(client) |
List PTY sessions | [Pty] |
pty_create(body, client) |
Create a PTY session | Pty |
pty_get(id, client) |
Get a PTY session | Pty |
pty_remove(id, client) |
Remove a PTY session | boolean |
| Function | Description | Response |
|---|---|---|
tui_append_prompt(body, client) |
Append text to the prompt | boolean |
tui_submit_prompt(client) |
Submit the current prompt | boolean |
tui_clear_prompt(client) |
Clear the prompt | boolean |
tui_execute_command(body, client) |
Execute a command | boolean |
tui_show_toast(body, client) |
Show a toast notification | boolean |
tui_open_help(client) |
Open help dialog | boolean |
tui_open_sessions(client) |
Open session selector | boolean |
tui_open_models(client) |
Open model selector | boolean |
tui_open_themes(client) |
Open theme selector | boolean |
Operations.tui_append_prompt(%{text: "Add this to prompt"}, client)
Operations.tui_show_toast(%{message: "Done!", variant: "success"}, client){:ok, tui} = OpenCode.create_tui(project: "/path/to/project")
OpenCode.Tui.close(tui)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")
endSee 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.exsAll OpenAPI types are generated under OpenCode.Generated.* (for example, OpenCode.Generated.Session).
API functions and types are documented in generated module docs, primarily:
OpenCodeOpenCode.Generated.OperationsOpenCode.Generated.*type modules
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.jsonRun 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.
This project is unofficial and is not affiliated with the OpenCode team.