opencode-python-sdk
Typed Python client for the OpenCode 2.x server API.
Documentation: https://shrijayan.github.io/opencode-python-sdk/
Unofficial, community-maintained. This package is not affiliated with or endorsed by the OpenCode team. If the OpenCode maintainers want to take it over, we are happy to transfer this repository and the
opencode-python-sdkname on PyPI. Open an issue and we will hand it over.
- Complete. All 142 operations of the OpenCode 2.1 API, sync and async, with typed models for every request, response and event (96 event types).
- Generated from the source of truth. The contract is exported from the same Effect
HttpApithe official TypeScript client is built from, so method names and request shapes match it exactly. - Safe to run in production. Retries only when a request cannot have changed server state, bounded backoff that
honors
Retry-After, typed errors for every declared server error, and timeouts on everything. - Forward compatible. New fields, enum values, union variants and event types from newer servers are kept, not rejected, so an older client keeps working after a server upgrade.
- Batteries included. Starts and stops
opencode servefor you, auto-paginates lists, and streams events.
Install
pip install opencode-python-sdk
The package imports as opencode. Requires Python 3.10 or newer. To start a server from Python, install the OpenCode CLI too.
Quick start
from opencode import OpencodeServer
with OpencodeServer(cwd="/path/to/repo") as server, server.client() as client:
session = client.session.create(title="Review")
client.session.prompt(session.id, text="Summarize the open TODOs in this repository")
client.session.wait(session.id)
for message in client.message.list(session.id):
print(message.type)
To connect to a server that is already running:
from opencode import Opencode
client = Opencode("http://127.0.0.1:4096", password="...")
print(client.server.info().version)
base_url and password default to the OPENCODE_BASE_URL and OPENCODE_PASSWORD environment variables. The
password variable is the same one the OpenCode CLI reads.
Async
AsyncOpencode has the same methods. It works with asyncio and trio.
import asyncio
from opencode import AsyncOpencode
async def main() -> None:
async with AsyncOpencode("http://127.0.0.1:4096", password="...") as client:
session = await client.session.create(title="Async")
await client.session.prompt(session.id, text="Hello")
async for message in client.message.list(session.id):
print(message.type)
asyncio.run(main())
Calling the API
Methods mirror the official TypeScript client: client.<group>.<operation>(), with names in snake_case. Path
parameters are positional; everything else is keyword-only.
client.session.get("ses_...")
client.session.prompt("ses_...", text="Fix the failing test", resume=True)
client.session.revert.stage("ses_...", message_id="msg_...")
client.mcp.list()
The full list is in api.md.
Optional and null arguments
The server distinguishes a field that is absent from a field set to null. Optional arguments default to
NOT_GIVEN and are left out of the request; passing None sends null:
client.session.update("ses_...", title="New title") # sends {"title": "New title"}
client.session.list(parent_id=None) # only root sessions: parentID=null
Models
Responses are pydantic models. Attributes are snake_case; the wire names stay
available as aliases, so a model validates server JSON and serializes back to it unchanged.
from opencode.types import ModelRef
session = client.session.create(model=ModelRef(id="claude-sonnet-5-5", provider_id="anthropic"))
session.project_id # attribute access
session.to_dict() # {"id": "...", "projectID": "...", ...}
Unions are discriminated, so isinstance and match narrow them:
from opencode.types import SessionMessageAssistant, SessionMessageUser
for message in client.message.list(session.id):
match message:
case SessionMessageUser(text=text):
print("user:", text)
case SessionMessageAssistant():
print("assistant replied")
case _:
pass # includes variants added by newer servers
Project directory
Most endpoints take a location. Set a default for the client once:
client = Opencode("http://127.0.0.1:4096", directory="/path/to/repo")
client.agent.list() # uses /path/to/repo
client.agent.list(location={"directory": "/other"}) # per call
Pagination
List endpoints with cursors return a page. Iterating it fetches the following pages as needed.
for session in client.session.list(limit=50):
print(session.title)
page = client.session.list(limit=50)
page.data # this page only
page.has_next_page()
next_page = page.next_page()
Async list methods return a paginator: await it for the first page, or async for over every item.
Events
with client.event.subscribe() as events:
for event in events:
print(event.type)
Events are typed (SessionCreatedEvent, SessionTextDeltaEvent, and so on). Event types this client version does
not know arrive as UnknownEvent with all their fields kept. Streams apply only the connect timeout by default, since
they stay open between events.
Errors
Every error derives from opencode.OpencodeError. Errors the server declares are raised as their own class, with a
typed body:
import opencode
try:
client.session.get("ses_missing")
except opencode.SessionNotFoundError as error:
print(error.error.session_id, error.status_code)
except opencode.NotFoundError:
... # any other 404
except opencode.APIConnectionError:
... # server unreachable
| Status | Class |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 413 | ContentTooLargeError |
| 429 | RateLimitError |
| 5xx | InternalServerError |
| No response | APIConnectionError, APITimeoutError |
| Unexpected body | APIResponseValidationError |
Retries and timeouts
The client retries twice by default, with exponential backoff and jitter, and honors Retry-After up to 60 seconds.
GET,PUTandDELETEare retried on connection errors and on 408, 429, 500, 502, 503 and 504.POSTandPATCHare retried only when the request cannot have run: when the connection could not be opened, or on 429 and 503. A prompt is never sent twice because a response was slow.
client = Opencode(max_retries=5, timeout=30.0)
client.with_options(timeout=None).session.wait(session.id) # wait as long as it takes
client.session.get("ses_...", timeout=5.0) # per call
The default timeout is 10 minutes with a 10 second connect timeout.
Running a server
OpencodeServer runs opencode serve as a child process on a free local port with a random password:
from opencode import OpencodeServer
server = OpencodeServer(
cwd="/path/to/repo",
isolated=True, # ignore project opencode.json, AGENTS.md and Claude Code files
config={"model": "anthropic/claude-sonnet-5-5"}, # passed as OPENCODE_CONFIG_CONTENT
)
with server:
client = server.client()
...
The server runs in --stdio mode, so it also exits when the Python process dies. AsyncOpencodeServer is the
async equivalent.
Advanced
import httpx
from opencode import Opencode
client = Opencode(
"https://opencode.internal.example.com",
http_client=httpx.Client(proxy="http://proxy:8080", verify="/etc/ssl/ca.pem"),
default_headers={"X-Team": "platform"},
)
client.session.create(title="t", extra_headers={"X-Trace": "1"}, extra_body={"future": True})
Logs go to the opencode logger. logging.getLogger("opencode").setLevel(logging.DEBUG) shows every request and
retry; credentials are never logged.
Versioning
opencode-python-sdk (this package) |
OpenCode server |
|---|---|
| 0.1.x | 2.1.x |
opencode.API_VERSION is the server version the client was generated from. The package follows semantic
versioning and releases automatically on every merge to main; until 1.0, minor versions may contain breaking
changes. See the releases page.
How it is built
@opencode/protocol (npm) ──bun codegen/extract──▶ spec/contract.json ──python -m codegen──▶ src/opencode
codegen/extract/extract.tsloadsClientApifrom the published@opencode/protocolpackage and exports every endpoint, its inputs, outputs and declared errors, and every schema as JSON Schema.python -m codegenturns the contract into pydantic models (opencode/types), resources (opencode/resources), error classes (opencode/_errors.py) and api.md.- The transport, retries, streaming, pagination and server launcher are written by hand.
Generated code is committed. CI regenerates it and fails if anything changed. See CONTRIBUTING.md.
License
MIT
Metadata
Release files for opencode-python-sdk 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| opencode_python_sdk-0.1.1.tar.gz | 103.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| opencode_python_sdk-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 224.3 kB
Release files / opencode_python_sdk-0.1.1.tar.gz
| Download URL | opencode_python_sdk-0.1.1.tar.gz |
|---|---|
| Size | 103.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
428ff0493a9bfd976f30f5e6dd5833b617237d7c8a62f63290a9fc7c1cadbfd7
|
|
BLAKE2b-256 checksum How to use checksums |
73d9985c66741b69577aa21ea316d9a5fd4c261b43e391998aa2ae954d5852c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / opencode_python_sdk-0.1.1-py3-none-any.whl
| Download URL | opencode_python_sdk-0.1.1-py3-none-any.whl |
|---|---|
| Size | 120.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8305257986c9965c7b9f9dd8292f05fe2c8656c8e03becba50d5d84c9ca5f84b
|
|
BLAKE2b-256 checksum How to use checksums |
dd59db785523a8e9f392c51d2ec111bd1ec249c569a36fb7ae8fbba32cf0389a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for opencode-python-sdk 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for opencode_python_sdk-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.