opencode-python-sdk 0.0.1.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencode_python_sdk-0.0.1.dev1/.gitignore +20 -0
- opencode_python_sdk-0.0.1.dev1/CHANGELOG.md +18 -0
- opencode_python_sdk-0.0.1.dev1/LICENSE +21 -0
- opencode_python_sdk-0.0.1.dev1/PKG-INFO +468 -0
- opencode_python_sdk-0.0.1.dev1/README.md +431 -0
- opencode_python_sdk-0.0.1.dev1/api.md +372 -0
- opencode_python_sdk-0.0.1.dev1/pyproject.toml +166 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/.generated.json +39 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/__init__.py +221 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_base_client.py +539 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_client.py +255 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_constants.py +27 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_errors.py +547 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_events.py +732 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_exceptions.py +271 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_models.py +54 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_pagination.py +192 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_pty.py +586 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_qs.py +95 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_resource.py +42 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_rpc.py +489 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_server.py +335 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_streaming.py +248 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_types.py +77 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_unions.py +46 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/_version.py +8 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/lib/__init__.py +5 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/lib/_ready.py +91 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/lib/_session.py +231 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/py.typed +0 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/__init__.py +364 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/agent.py +179 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/command.py +100 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/config.py +240 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/credential.py +384 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/debug.py +178 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/event.py +88 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/experimental.py +765 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/file.py +355 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/form.py +100 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/generate.py +110 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/integration.py +1033 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/location.py +162 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/mcp.py +492 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/message.py +152 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/migration.py +118 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/model.py +177 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/permission.py +704 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/plugin.py +250 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/project.py +187 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/provider.py +183 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/pty.py +522 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/reference.py +100 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/rpc.py +113 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/server.py +230 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/session.py +3567 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/shell.py +412 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/skill.py +100 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/vcs.py +500 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/voice.py +113 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/websearch.py +184 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/resources/worktree.py +336 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/testing.py +508 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/types/__init__.py +1462 -0
- opencode_python_sdk-0.0.1.dev1/src/opencode/types/_generated.py +8540 -0
- opencode_python_sdk-0.0.1.dev1/tests/__init__.py +0 -0
- opencode_python_sdk-0.0.1.dev1/tests/conftest.py +45 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_async_client.py +51 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_client.py +251 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_docs_coverage.py +256 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_encoding.py +56 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_events.py +396 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_helpers.py +252 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_live.py +174 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_models.py +77 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_pagination.py +57 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_pty.py +236 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_rpc.py +154 -0
- opencode_python_sdk-0.0.1.dev1/tests/test_streaming.py +78 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.coverage
|
|
8
|
+
coverage.xml
|
|
9
|
+
htmlcov/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.pyright/
|
|
14
|
+
codegen/extract/node_modules/
|
|
15
|
+
codegen/extract/.cache/
|
|
16
|
+
.DS_Store
|
|
17
|
+
|
|
18
|
+
# MkDocs output
|
|
19
|
+
site/
|
|
20
|
+
.cache/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Releases are published automatically on every merge to `main`. Release notes for each version are on the
|
|
4
|
+
[GitHub releases page](https://github.com/shrijayan/opencode-python-sdk/releases).
|
|
5
|
+
|
|
6
|
+
## Unreleased
|
|
7
|
+
|
|
8
|
+
- `client.events`: a shared event stream with one connection for all subscribers, bounded per-subscriber queues,
|
|
9
|
+
`server.connected` replay, `of_type` / `for_session` / `where` filters, `on_activity` keepalive hook, cancellation,
|
|
10
|
+
and opt-in reconnection with backoff (`EventHub(client, reconnect=True)`).
|
|
11
|
+
- `client.rpc(Definition)`: typed plugin RPC calls and event subscriptions declared with `RpcDefinition`,
|
|
12
|
+
`rpc_method` and `rpc_event`; RPC failures raise `RpcCallError` / `RpcCallInternalError`.
|
|
13
|
+
- `Stream.on_activity` and `Stream.interrupt()`.
|
|
14
|
+
|
|
15
|
+
## 0.1.0
|
|
16
|
+
|
|
17
|
+
First release, generated from OpenCode 2.1.2: sync and async clients for all 142 operations, typed models and
|
|
18
|
+
events, typed errors, pagination, event streams, retries, and the `OpencodeServer` launcher.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shrijayan Rajendran and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: opencode-python-sdk
|
|
3
|
+
Version: 0.0.1.dev1
|
|
4
|
+
Summary: Typed Python client for the OpenCode 2.x server API
|
|
5
|
+
Project-URL: Homepage, https://github.com/shrijayan/opencode-python-sdk
|
|
6
|
+
Project-URL: Documentation, https://shrijayan.github.io/opencode-python-sdk/
|
|
7
|
+
Project-URL: Repository, https://github.com/shrijayan/opencode-python-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/shrijayan/opencode-python-sdk/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/shrijayan/opencode-python-sdk/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Shrijayan Rajendran
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: agents,ai,client,coding-agent,opencode,sdk
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Framework :: Pydantic :: 2
|
|
17
|
+
Classifier: Framework :: Trio
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: anyio<5,>=4.4
|
|
31
|
+
Requires-Dist: httpx<1,>=0.27
|
|
32
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
33
|
+
Requires-Dist: typing-extensions>=4.12
|
|
34
|
+
Provides-Extra: pty
|
|
35
|
+
Requires-Dist: websockets<17,>=14; extra == 'pty'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# opencode-python-sdk
|
|
39
|
+
|
|
40
|
+
[](https://github.com/shrijayan/opencode-python-sdk/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/opencode-python-sdk/)
|
|
42
|
+
[](https://shrijayan.github.io/opencode-python-sdk/)
|
|
43
|
+
[](https://pypi.org/project/opencode-python-sdk/)
|
|
44
|
+
|
|
45
|
+
Typed Python client for the [OpenCode](https://github.com/anomalyco/opencode) 2.x server API.
|
|
46
|
+
|
|
47
|
+
**Documentation: <https://shrijayan.github.io/opencode-python-sdk/>**
|
|
48
|
+
|
|
49
|
+
> **Unofficial, community-maintained.** This package is not affiliated with or endorsed by the OpenCode team.
|
|
50
|
+
> If the OpenCode maintainers want to take it over, we are happy to transfer this repository and the `opencode-python-sdk`
|
|
51
|
+
> name on PyPI. Open an issue and we will hand it over.
|
|
52
|
+
|
|
53
|
+
- **Complete.** All 142 operations of the OpenCode 2.1 API, sync and async, with typed models for every request,
|
|
54
|
+
response and event (96 event types).
|
|
55
|
+
- **Generated from the source of truth.** The contract is exported from the same Effect `HttpApi` the official
|
|
56
|
+
TypeScript client is built from, so method names and request shapes match it exactly.
|
|
57
|
+
- **Safe to run in production.** Retries only when a request cannot have changed server state, bounded backoff that
|
|
58
|
+
honors `Retry-After`, typed errors for every declared server error, and timeouts on everything.
|
|
59
|
+
- **Forward compatible.** New fields, enum values, union variants and event types from newer servers are kept, not
|
|
60
|
+
rejected, so an older client keeps working after a server upgrade.
|
|
61
|
+
- **Batteries included.** Starts and stops `opencode serve` for you, auto-paginates lists, and streams events.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install opencode-python-sdk
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The package imports as `opencode`. Requires Python 3.10 or newer. To start a server from Python, install the [OpenCode CLI](https://opencode.ai) too.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from opencode import OpencodeServer
|
|
75
|
+
|
|
76
|
+
with OpencodeServer(cwd="/path/to/repo") as server, server.client() as client:
|
|
77
|
+
session = client.session.create(title="Review")
|
|
78
|
+
client.session.prompt(session.id, text="Summarize the open TODOs in this repository")
|
|
79
|
+
client.session.wait(session.id)
|
|
80
|
+
|
|
81
|
+
for message in client.message.list(session.id):
|
|
82
|
+
print(message.type)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
To connect to a server that is already running:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from opencode import Opencode
|
|
89
|
+
|
|
90
|
+
client = Opencode("http://127.0.0.1:4096", password="...")
|
|
91
|
+
print(client.server.info().version)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`base_url` and `password` default to the `OPENCODE_BASE_URL` and `OPENCODE_PASSWORD` environment variables. The
|
|
95
|
+
password variable is the same one the OpenCode CLI reads.
|
|
96
|
+
|
|
97
|
+
## Async
|
|
98
|
+
|
|
99
|
+
`AsyncOpencode` has the same methods. It works with asyncio and trio.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import asyncio
|
|
103
|
+
|
|
104
|
+
from opencode import AsyncOpencode
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def main() -> None:
|
|
108
|
+
async with AsyncOpencode("http://127.0.0.1:4096", password="...") as client:
|
|
109
|
+
session = await client.session.create(title="Async")
|
|
110
|
+
await client.session.prompt(session.id, text="Hello")
|
|
111
|
+
|
|
112
|
+
async for message in client.message.list(session.id):
|
|
113
|
+
print(message.type)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Calling the API
|
|
120
|
+
|
|
121
|
+
Methods mirror the official TypeScript client: `client.<group>.<operation>()`, with names in `snake_case`. Path
|
|
122
|
+
parameters are positional; everything else is keyword-only.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
client.session.get("ses_...")
|
|
126
|
+
client.session.prompt("ses_...", text="Fix the failing test", resume=True)
|
|
127
|
+
client.session.revert.stage("ses_...", message_id="msg_...")
|
|
128
|
+
client.mcp.list()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The full list is in [api.md](api.md).
|
|
132
|
+
|
|
133
|
+
### Optional and null arguments
|
|
134
|
+
|
|
135
|
+
The server distinguishes a field that is absent from a field set to `null`. Optional arguments default to
|
|
136
|
+
`NOT_GIVEN` and are left out of the request; passing `None` sends `null`:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
client.session.update("ses_...", title="New title") # sends {"title": "New title"}
|
|
140
|
+
client.session.list(parent_id=None) # only root sessions: parentID=null
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Models
|
|
144
|
+
|
|
145
|
+
Responses are [pydantic](https://docs.pydantic.dev) models. Attributes are `snake_case`; the wire names stay
|
|
146
|
+
available as aliases, so a model validates server JSON and serializes back to it unchanged.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from opencode.types import ModelRef
|
|
150
|
+
|
|
151
|
+
session = client.session.create(model=ModelRef(id="claude-sonnet-5-5", provider_id="anthropic"))
|
|
152
|
+
session.project_id # attribute access
|
|
153
|
+
session.to_dict() # {"id": "...", "projectID": "...", ...}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Unions are discriminated, so `isinstance` and `match` narrow them:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from opencode.types import SessionMessageAssistant, SessionMessageUser
|
|
160
|
+
|
|
161
|
+
for message in client.message.list(session.id):
|
|
162
|
+
match message:
|
|
163
|
+
case SessionMessageUser(text=text):
|
|
164
|
+
print("user:", text)
|
|
165
|
+
case SessionMessageAssistant():
|
|
166
|
+
print("assistant replied")
|
|
167
|
+
case _:
|
|
168
|
+
pass # includes variants added by newer servers
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Project directory
|
|
172
|
+
|
|
173
|
+
Most endpoints take a `location`. Set a default for the client once:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
client = Opencode("http://127.0.0.1:4096", directory="/path/to/repo")
|
|
177
|
+
client.agent.list() # uses /path/to/repo
|
|
178
|
+
client.agent.list(location={"directory": "/other"}) # per call
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Pagination
|
|
182
|
+
|
|
183
|
+
List endpoints with cursors return a page. Iterating it fetches the following pages as needed.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
for session in client.session.list(limit=50):
|
|
187
|
+
print(session.title)
|
|
188
|
+
|
|
189
|
+
page = client.session.list(limit=50)
|
|
190
|
+
page.data # this page only
|
|
191
|
+
page.has_next_page()
|
|
192
|
+
next_page = page.next_page()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Async list methods return a paginator: `await` it for the first page, or `async for` over every item.
|
|
196
|
+
|
|
197
|
+
## Events
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
with client.event.subscribe() as events:
|
|
201
|
+
for event in events:
|
|
202
|
+
print(event.type)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Events are typed (`SessionCreatedEvent`, `SessionTextDeltaEvent`, and so on). Event types this client version does
|
|
206
|
+
not know arrive as `UnknownEvent` with all their fields kept. Streams apply only the connect timeout by default, since
|
|
207
|
+
they stay open between events.
|
|
208
|
+
|
|
209
|
+
### Shared event stream
|
|
210
|
+
|
|
211
|
+
`client.events` shares one connection among every subscriber, like `client.event` in the TypeScript client. The
|
|
212
|
+
connection opens with the first subscriber and closes when the last one leaves. Each subscriber has its own queue of
|
|
213
|
+
up to 4096 events; one that falls further behind fails with `EventOverflowError` without affecting the others. The
|
|
214
|
+
latest `server.connected` event is replayed to subscribers that join later.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from opencode.types import SessionIdleEvent
|
|
218
|
+
|
|
219
|
+
with client.events.of_type(SessionIdleEvent).for_session(session.id) as idle:
|
|
220
|
+
client.session.prompt(session.id, text="Hello")
|
|
221
|
+
event = idle.get(timeout=300) # typed as SessionIdleEvent
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- Filters chain: `of_type(*classes)` narrows the yielded type, `for_session(id)` keeps one session's events, and
|
|
225
|
+
`where(predicate)` takes any test. Entering the `with` block joins the stream, so nothing published after that is
|
|
226
|
+
missed.
|
|
227
|
+
- `subscribe(on_activity=callback)` calls `callback` for every chunk received, keepalive comments included, as a
|
|
228
|
+
liveness signal.
|
|
229
|
+
- `close()` (or leaving the `with` block) ends the subscription, also from another thread. In async code, cancel a wait
|
|
230
|
+
with `anyio.fail_after`, `asyncio.timeout` or a trio cancel scope; the subscription stays usable.
|
|
231
|
+
- Reconnection is opt-in: `EventHub(client, reconnect=True)` (or `AsyncEventHub`, or a `Reconnect(...)` policy) reopens
|
|
232
|
+
the connection with exponential backoff after connection errors, 5xx and 429. Subscribers see `server.connected`
|
|
233
|
+
once. **The stream is volatile**: events published while disconnected are lost, so re-read state through the API
|
|
234
|
+
after a reconnect when it matters.
|
|
235
|
+
|
|
236
|
+
The async hub works the same way under asyncio and trio:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
async with client.events.for_session(session.id) as events:
|
|
240
|
+
async for event in events:
|
|
241
|
+
print(event.type)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`client.event.subscribe()` still opens a dedicated connection.
|
|
245
|
+
|
|
246
|
+
### Plugin RPC
|
|
247
|
+
|
|
248
|
+
Declare a plugin RPC once as a class, then call it with typed inputs and outputs:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from pydantic import BaseModel
|
|
252
|
+
from opencode import RpcCallError, RpcDefinition, rpc_event, rpc_method
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class AddInput(BaseModel):
|
|
256
|
+
amount: int
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Total(BaseModel):
|
|
260
|
+
total: int
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class Counter(RpcDefinition, id="counter"):
|
|
264
|
+
add = rpc_method(AddInput, Total)
|
|
265
|
+
reset = rpc_method(None, Total)
|
|
266
|
+
changed = rpc_event(Total)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
counter = client.rpc(Counter)
|
|
270
|
+
try:
|
|
271
|
+
print(counter.call(Counter.add, AddInput(amount=2)).total)
|
|
272
|
+
except RpcCallError as error:
|
|
273
|
+
print(error.type, error.message, error.data)
|
|
274
|
+
|
|
275
|
+
with counter.subscribe(Counter.changed) as changes: # only rpc.counter.changed, with validated data
|
|
276
|
+
for change in changes:
|
|
277
|
+
print(change.data.total)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Any type pydantic validates works for inputs, outputs and event data. `rpc_method(..., errors={"not_found": Model})`
|
|
281
|
+
validates the `data` of declared errors. Failures raise `RpcCallError`; crashes and outputs that do not match the
|
|
282
|
+
declared type raise `RpcCallInternalError`. `counter.events()` yields every raw event of the RPC. The untyped
|
|
283
|
+
`client.rpc.call(rpc_id, method, input=...)` is still available.
|
|
284
|
+
|
|
285
|
+
## Terminals
|
|
286
|
+
|
|
287
|
+
`client.pty.attach()` opens an interactive connection to a PTY over a WebSocket. It needs the `pty` extra:
|
|
288
|
+
`pip install "opencode-python-sdk[pty]"`.
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
pty = client.pty.create(command="/bin/sh").data
|
|
292
|
+
with client.pty.attach(pty.id) as term:
|
|
293
|
+
term.resize(120, 40)
|
|
294
|
+
term.send("echo hi\n")
|
|
295
|
+
for chunk in term: # output bytes, until the shell exits
|
|
296
|
+
print(chunk.decode(errors="replace"), end="")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`recv(timeout=...)` reads one chunk and returns `None` once the terminal has ended. `term.cursor` holds the output
|
|
300
|
+
position to pass as `cursor=` when reattaching; `cursor=-1` skips the replay of earlier output. Persistent terminals
|
|
301
|
+
work the same way through `client.experimental.persistent_pty.attach(pty_id, role=..., takeover=...)`. The async
|
|
302
|
+
client returns `AsyncPtyConnection` (asyncio only): `async with await client.pty.attach(...) as term`. A connection
|
|
303
|
+
the server refuses or closes abnormally raises `PtyConnectionError`. See
|
|
304
|
+
[`examples/terminal.py`](examples/terminal.py) for a full TTY bridge.
|
|
305
|
+
|
|
306
|
+
## Errors
|
|
307
|
+
|
|
308
|
+
Every error derives from `opencode.OpencodeError`. Errors the server declares are raised as their own class, with a
|
|
309
|
+
typed body:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
import opencode
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
client.session.get("ses_missing")
|
|
316
|
+
except opencode.SessionNotFoundError as error:
|
|
317
|
+
print(error.error.session_id, error.status_code)
|
|
318
|
+
except opencode.NotFoundError:
|
|
319
|
+
... # any other 404
|
|
320
|
+
except opencode.APIConnectionError:
|
|
321
|
+
... # server unreachable
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
| Status | Class |
|
|
325
|
+
| --- | --- |
|
|
326
|
+
| 400 | `BadRequestError` |
|
|
327
|
+
| 401 | `AuthenticationError` |
|
|
328
|
+
| 403 | `PermissionDeniedError` |
|
|
329
|
+
| 404 | `NotFoundError` |
|
|
330
|
+
| 409 | `ConflictError` |
|
|
331
|
+
| 413 | `ContentTooLargeError` |
|
|
332
|
+
| 429 | `RateLimitError` |
|
|
333
|
+
| 5xx | `InternalServerError` |
|
|
334
|
+
| No response | `APIConnectionError`, `APITimeoutError` |
|
|
335
|
+
| Unexpected body | `APIResponseValidationError` |
|
|
336
|
+
|
|
337
|
+
## Retries and timeouts
|
|
338
|
+
|
|
339
|
+
The client retries twice by default, with exponential backoff and jitter, and honors `Retry-After` up to 60 seconds.
|
|
340
|
+
|
|
341
|
+
- `GET`, `PUT` and `DELETE` are retried on connection errors and on 408, 429, 500, 502, 503 and 504.
|
|
342
|
+
- `POST` and `PATCH` are retried only when the request cannot have run: when the connection could not be opened, or
|
|
343
|
+
on 429 and 503. A prompt is never sent twice because a response was slow.
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
client = Opencode(max_retries=5, timeout=30.0)
|
|
347
|
+
client.with_options(timeout=None).session.wait(session.id) # wait as long as it takes
|
|
348
|
+
client.session.get("ses_...", timeout=5.0) # per call
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The default timeout is 10 minutes with a 10 second connect timeout.
|
|
352
|
+
|
|
353
|
+
## Running an agent headless
|
|
354
|
+
|
|
355
|
+
`client.session.run` does the usual "prompt an agent and wait" round trip in one call: it creates a session (or
|
|
356
|
+
reuses `session_id`), sends the prompt, blocks until the session is idle, and returns the new messages with the
|
|
357
|
+
final text and usage. `client.wait_until_ready` waits until agents and MCP servers have loaded, which happens
|
|
358
|
+
asynchronously after the server boots. Both have async versions on `AsyncOpencode`.
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
from opencode import Opencode
|
|
362
|
+
from opencode.types import ModelRef, PromptInputFileAttachment
|
|
363
|
+
|
|
364
|
+
with Opencode("http://127.0.0.1:4096", password="...") as client:
|
|
365
|
+
client.wait_until_ready(agents=["reviewer"], mcp_servers=["github"], timeout=60)
|
|
366
|
+
result = client.session.run(
|
|
367
|
+
"Review the attached diff",
|
|
368
|
+
agent="reviewer",
|
|
369
|
+
model=ModelRef(provider_id="anthropic", id="claude-sonnet-5-5"), # set it with the agent
|
|
370
|
+
files=[PromptInputFileAttachment(uri="data:text/plain;base64,...", name="diff.patch")],
|
|
371
|
+
timeout=600,
|
|
372
|
+
)
|
|
373
|
+
print(result.outcome, result.text)
|
|
374
|
+
print(result.cost, result.tokens.input, result.tokens.output) # this run; result.session has the totals
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
If the wait ends early (Ctrl-C, task cancellation, `timeout`, a connection error), `run` interrupts the session
|
|
378
|
+
on the server before re-raising, so the agent does not keep running unattended.
|
|
379
|
+
|
|
380
|
+
## Testing your code
|
|
381
|
+
|
|
382
|
+
`opencode.testing.FakeServer` is an in-process fake of the server: an `httpx` transport that keeps sessions,
|
|
383
|
+
messages and events in memory and answers with the same payloads as the real server. No process, no model.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from opencode import Opencode
|
|
387
|
+
from opencode.testing import FakeServer
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_summarize() -> None:
|
|
391
|
+
fake = FakeServer(reply=lambda prompt: f"summary of {prompt!r}")
|
|
392
|
+
client = Opencode.fake(fake) # same as Opencode(transport=fake)
|
|
393
|
+
|
|
394
|
+
assert client.session.run("README").text == "summary of 'README'"
|
|
395
|
+
assert fake.prompts == ["README"]
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`fake.fail(method, path_regex, status=..., exception=...)` injects errors, `fake.add_agent(..., after_polls=n)`
|
|
399
|
+
and `fake.set_mcp_status(...)` simulate slow boots, and `fake.requests` records every request. Routes the fake
|
|
400
|
+
does not implement answer `404`.
|
|
401
|
+
|
|
402
|
+
## Running a server
|
|
403
|
+
|
|
404
|
+
`OpencodeServer` runs `opencode serve` as a child process on a free local port with a random password:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from opencode import OpencodeServer
|
|
408
|
+
|
|
409
|
+
server = OpencodeServer(
|
|
410
|
+
cwd="/path/to/repo",
|
|
411
|
+
isolated=True, # ignore project opencode.json, AGENTS.md and Claude Code files
|
|
412
|
+
config={"model": "anthropic/claude-sonnet-5-5"}, # passed as OPENCODE_CONFIG_CONTENT
|
|
413
|
+
)
|
|
414
|
+
with server:
|
|
415
|
+
client = server.client()
|
|
416
|
+
...
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The server runs in `--stdio` mode, so it also exits when the Python process dies. `AsyncOpencodeServer` is the
|
|
420
|
+
async equivalent.
|
|
421
|
+
|
|
422
|
+
## Advanced
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
import httpx
|
|
426
|
+
|
|
427
|
+
from opencode import Opencode
|
|
428
|
+
|
|
429
|
+
client = Opencode(
|
|
430
|
+
"https://opencode.internal.example.com",
|
|
431
|
+
http_client=httpx.Client(proxy="http://proxy:8080", verify="/etc/ssl/ca.pem"),
|
|
432
|
+
default_headers={"X-Team": "platform"},
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
client.session.create(title="t", extra_headers={"X-Trace": "1"}, extra_body={"future": True})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Logs go to the `opencode` logger. `logging.getLogger("opencode").setLevel(logging.DEBUG)` shows every request and
|
|
439
|
+
retry; credentials are never logged.
|
|
440
|
+
|
|
441
|
+
## Versioning
|
|
442
|
+
|
|
443
|
+
| `opencode-python-sdk` (this package) | OpenCode server |
|
|
444
|
+
| --- | --- |
|
|
445
|
+
| 0.1.x | 2.1.x |
|
|
446
|
+
|
|
447
|
+
`opencode.API_VERSION` is the server version the client was generated from. The package follows semantic
|
|
448
|
+
versioning and releases automatically on every merge to `main`; until 1.0, minor versions may contain breaking
|
|
449
|
+
changes. See the [releases page](https://github.com/shrijayan/opencode-python-sdk/releases).
|
|
450
|
+
|
|
451
|
+
## How it is built
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
@opencode/protocol (npm) ──bun codegen/extract──▶ spec/contract.json ──python -m codegen──▶ src/opencode
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
1. `codegen/extract/extract.ts` loads `ClientApi` from the published `@opencode/protocol` package and exports
|
|
458
|
+
every endpoint, its inputs, outputs and declared errors, and every schema as JSON Schema.
|
|
459
|
+
2. `python -m codegen` turns the contract into pydantic models (`opencode/types`), resources
|
|
460
|
+
(`opencode/resources`), error classes (`opencode/_errors.py`) and [api.md](api.md).
|
|
461
|
+
3. The transport, retries, streaming, pagination and server launcher are written by hand.
|
|
462
|
+
|
|
463
|
+
Generated code is committed. CI regenerates it and fails if anything changed. See
|
|
464
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
465
|
+
|
|
466
|
+
## License
|
|
467
|
+
|
|
468
|
+
MIT
|