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.
Files changed (79) hide show
  1. opencode_python_sdk-0.0.1.dev1/.gitignore +20 -0
  2. opencode_python_sdk-0.0.1.dev1/CHANGELOG.md +18 -0
  3. opencode_python_sdk-0.0.1.dev1/LICENSE +21 -0
  4. opencode_python_sdk-0.0.1.dev1/PKG-INFO +468 -0
  5. opencode_python_sdk-0.0.1.dev1/README.md +431 -0
  6. opencode_python_sdk-0.0.1.dev1/api.md +372 -0
  7. opencode_python_sdk-0.0.1.dev1/pyproject.toml +166 -0
  8. opencode_python_sdk-0.0.1.dev1/src/opencode/.generated.json +39 -0
  9. opencode_python_sdk-0.0.1.dev1/src/opencode/__init__.py +221 -0
  10. opencode_python_sdk-0.0.1.dev1/src/opencode/_base_client.py +539 -0
  11. opencode_python_sdk-0.0.1.dev1/src/opencode/_client.py +255 -0
  12. opencode_python_sdk-0.0.1.dev1/src/opencode/_constants.py +27 -0
  13. opencode_python_sdk-0.0.1.dev1/src/opencode/_errors.py +547 -0
  14. opencode_python_sdk-0.0.1.dev1/src/opencode/_events.py +732 -0
  15. opencode_python_sdk-0.0.1.dev1/src/opencode/_exceptions.py +271 -0
  16. opencode_python_sdk-0.0.1.dev1/src/opencode/_models.py +54 -0
  17. opencode_python_sdk-0.0.1.dev1/src/opencode/_pagination.py +192 -0
  18. opencode_python_sdk-0.0.1.dev1/src/opencode/_pty.py +586 -0
  19. opencode_python_sdk-0.0.1.dev1/src/opencode/_qs.py +95 -0
  20. opencode_python_sdk-0.0.1.dev1/src/opencode/_resource.py +42 -0
  21. opencode_python_sdk-0.0.1.dev1/src/opencode/_rpc.py +489 -0
  22. opencode_python_sdk-0.0.1.dev1/src/opencode/_server.py +335 -0
  23. opencode_python_sdk-0.0.1.dev1/src/opencode/_streaming.py +248 -0
  24. opencode_python_sdk-0.0.1.dev1/src/opencode/_types.py +77 -0
  25. opencode_python_sdk-0.0.1.dev1/src/opencode/_unions.py +46 -0
  26. opencode_python_sdk-0.0.1.dev1/src/opencode/_version.py +8 -0
  27. opencode_python_sdk-0.0.1.dev1/src/opencode/lib/__init__.py +5 -0
  28. opencode_python_sdk-0.0.1.dev1/src/opencode/lib/_ready.py +91 -0
  29. opencode_python_sdk-0.0.1.dev1/src/opencode/lib/_session.py +231 -0
  30. opencode_python_sdk-0.0.1.dev1/src/opencode/py.typed +0 -0
  31. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/__init__.py +364 -0
  32. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/agent.py +179 -0
  33. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/command.py +100 -0
  34. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/config.py +240 -0
  35. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/credential.py +384 -0
  36. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/debug.py +178 -0
  37. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/event.py +88 -0
  38. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/experimental.py +765 -0
  39. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/file.py +355 -0
  40. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/form.py +100 -0
  41. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/generate.py +110 -0
  42. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/integration.py +1033 -0
  43. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/location.py +162 -0
  44. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/mcp.py +492 -0
  45. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/message.py +152 -0
  46. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/migration.py +118 -0
  47. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/model.py +177 -0
  48. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/permission.py +704 -0
  49. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/plugin.py +250 -0
  50. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/project.py +187 -0
  51. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/provider.py +183 -0
  52. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/pty.py +522 -0
  53. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/reference.py +100 -0
  54. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/rpc.py +113 -0
  55. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/server.py +230 -0
  56. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/session.py +3567 -0
  57. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/shell.py +412 -0
  58. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/skill.py +100 -0
  59. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/vcs.py +500 -0
  60. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/voice.py +113 -0
  61. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/websearch.py +184 -0
  62. opencode_python_sdk-0.0.1.dev1/src/opencode/resources/worktree.py +336 -0
  63. opencode_python_sdk-0.0.1.dev1/src/opencode/testing.py +508 -0
  64. opencode_python_sdk-0.0.1.dev1/src/opencode/types/__init__.py +1462 -0
  65. opencode_python_sdk-0.0.1.dev1/src/opencode/types/_generated.py +8540 -0
  66. opencode_python_sdk-0.0.1.dev1/tests/__init__.py +0 -0
  67. opencode_python_sdk-0.0.1.dev1/tests/conftest.py +45 -0
  68. opencode_python_sdk-0.0.1.dev1/tests/test_async_client.py +51 -0
  69. opencode_python_sdk-0.0.1.dev1/tests/test_client.py +251 -0
  70. opencode_python_sdk-0.0.1.dev1/tests/test_docs_coverage.py +256 -0
  71. opencode_python_sdk-0.0.1.dev1/tests/test_encoding.py +56 -0
  72. opencode_python_sdk-0.0.1.dev1/tests/test_events.py +396 -0
  73. opencode_python_sdk-0.0.1.dev1/tests/test_helpers.py +252 -0
  74. opencode_python_sdk-0.0.1.dev1/tests/test_live.py +174 -0
  75. opencode_python_sdk-0.0.1.dev1/tests/test_models.py +77 -0
  76. opencode_python_sdk-0.0.1.dev1/tests/test_pagination.py +57 -0
  77. opencode_python_sdk-0.0.1.dev1/tests/test_pty.py +236 -0
  78. opencode_python_sdk-0.0.1.dev1/tests/test_rpc.py +154 -0
  79. 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
+ [![CI](https://github.com/shrijayan/opencode-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/shrijayan/opencode-python-sdk/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/opencode-python-sdk.svg)](https://pypi.org/project/opencode-python-sdk/)
42
+ [![Docs](https://img.shields.io/badge/docs-shrijayan.github.io-d9730d)](https://shrijayan.github.io/opencode-python-sdk/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/opencode-python-sdk.svg)](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