Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
aaf4c6e
Switch SDK generation to Ferrule
shrijayan Oct 26, 2026
fcaa342
Add the dependencies of Ferrule's runtime and generated tests
shrijayan Oct 26, 2026
f075ef2
Keep the SDK's feature tests in tests/custom, drop tests of the repla…
shrijayan Oct 26, 2026
b3e0e3c
Move the custom modules onto Ferrule's runtime: typed errors and shar…
shrijayan Oct 26, 2026
dc87c4f
Give the config its ten-minute default timeout
shrijayan Oct 26, 2026
368b72f
chore: regenerate SDK
shrijayan Oct 26, 2026
be0720b
Unwrap the envelopes the former generator unwrapped
shrijayan Oct 26, 2026
16dcce0
chore: regenerate SDK
shrijayan Oct 26, 2026
b92478d
Keep the client's own features in a subclass of the generated client
shrijayan Oct 26, 2026
aa5803d
Port the event, RPC and PTY modules to the generated client
shrijayan Oct 26, 2026
3246f8c
chore: regenerate SDK
shrijayan Oct 26, 2026
bf2fc4a
Adapt the feature tests and modules to the generated API
shrijayan Oct 26, 2026
4fabaf9
chore: regenerate SDK
shrijayan Oct 26, 2026
06e5133
Take the agent recipes of the latest generation
shrijayan Oct 26, 2026
3430d82
chore: regenerate SDK
shrijayan Oct 26, 2026
04f94a3
chore: regenerate SDK
shrijayan Oct 26, 2026
29fa03d
Skip the tests the mock server cannot serve
shrijayan Oct 26, 2026
3368901
chore: regenerate SDK
shrijayan Oct 26, 2026
62b701e
Keep the SDK's own CI out of generation
shrijayan Oct 26, 2026
9776f7f
Remove the former generator's modules; keep codegen/extract for the c…
shrijayan Oct 26, 2026
c6ab338
chore: regenerate SDK
shrijayan Oct 26, 2026
ef8d9e5
chore: regenerate SDK
shrijayan Oct 26, 2026
b837107
Lint and type-check with the generated code's settings; drop unwrappe…
shrijayan Oct 26, 2026
8f26349
chore: regenerate SDK
shrijayan Oct 26, 2026
1aa3a80
Format and lint the custom code with the generated code's settings
shrijayan Oct 26, 2026
049b757
Pin the type checkers and pydantic the generated code is checked with
shrijayan Oct 26, 2026
eecc739
Type the custom modules against the generated API
shrijayan Oct 26, 2026
404bd6d
Run CI and the spec sync on the Ferrule toolchain
shrijayan Oct 26, 2026
d89acdd
Credit the Apache-2.0 generated code and runtime
shrijayan Oct 26, 2026
b28d267
Document how the SDK is generated with Ferrule
shrijayan Oct 26, 2026
bcd3c47
Note the Ferrule regeneration in the changelog
shrijayan Oct 26, 2026
37ca655
Fix the file.write example in the changelog
shrijayan Oct 26, 2026
73cedf3
Pin the Ferrule action
shrijayan Oct 26, 2026
7c84117
Take the config scripts' biome-formatted casings
shrijayan Oct 26, 2026
13458f3
chore: regenerate SDK
shrijayan Oct 26, 2026
855f6b8
Make scripts/mock executable again
shrijayan Oct 26, 2026
16df5c9
Pin the Ferrule action to the commit with the regenerate fixes
shrijayan Oct 26, 2026
b2655f3
Fix CI: bound the aiohttp extra, run the generated suite on Linux, fu…
shrijayan Oct 26, 2026
8e89ef4
Run the spec scripts without typing_extensions
shrijayan Oct 26, 2026
6f9eb48
Type-check with mypy on Python 3.10
shrijayan Oct 26, 2026
a1432fa
Make retry delays zero in the custom tests instead of patching anyio.…
shrijayan Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 31 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:

jobs:
lint:
name: Lint, types, generated code
name: Lint and types
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -26,8 +26,11 @@ jobs:
- run: uv run ruff format --check .
- run: uv run pyright
- run: uv run mypy
- name: Generated code matches spec/contract.json
run: uv run python -m codegen --check
- name: openapi.json and stainless.yml match spec/contract.json
run: |
uv run --no-project --python 3.12 python scripts/opencode/contract_to_openapi.py spec/contract.json openapi.json
uv run --no-project --python 3.12 python scripts/opencode/config_models.py openapi.json stainless.yml
git diff --exit-code -- openapi.json stainless.yml

test:
name: Test (Python ${{ matrix.python }}, ${{ matrix.os }})
Expand All @@ -48,8 +51,22 @@ jobs:
with:
enable-cache: true
python-version: ${{ matrix.python }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
- run: uv sync --locked
- run: uv run pytest -m "not live" --cov --cov-report=term-missing
# The generated tests run against the mock (pytest-asyncio), on Linux as the generated
# SDKs' own CI does; the hand-written ones in tests/custom use anyio and trio, so they run
# apart, on every OS.
- name: Start the mock server
if: runner.os == 'Linux'
shell: bash
run: ./scripts/mock --daemon
- if: runner.os == 'Linux'
run: uv run pytest --cov --cov-report=term-missing
- run: >-
uv run pytest --confcutdir=tests/custom -o asyncio_mode=strict -o addopts="-ra --strict-markers"
-m "not live" --cov --cov-append --cov-report=term-missing tests/custom

lowest:
name: Test with lowest supported dependencies
Expand All @@ -61,8 +78,13 @@ jobs:
python-version: "3.10"
- run: >-
uv run --isolated --resolution lowest-direct --with-editable '.[pty,aiohttp]'
--with pytest --with respx --with trio
pytest -m "not live" -p no:cacheprovider
--with pytest --with pytest-asyncio --with respx --with trio --with dirty-equals --with time-machine
pytest -p no:cacheprovider --ignore=tests/api_resources
- run: >-
uv run --isolated --resolution lowest-direct --with-editable '.[pty,aiohttp]'
--with pytest --with pytest-asyncio --with respx --with trio
pytest -p no:cacheprovider --confcutdir=tests/custom -o asyncio_mode=strict
-o addopts="-ra --strict-markers" -m "not live" tests/custom

live:
name: Live server
Expand All @@ -80,7 +102,9 @@ jobs:
- run: echo "$GITHUB_WORKSPACE/.github/live/node_modules/.bin" >> "$GITHUB_PATH"
- run: opencode --version
- run: uv sync --locked
- run: uv run pytest -m live -v
- run: >-
uv run pytest --confcutdir=tests/custom -o asyncio_mode=strict -o addopts="-ra --strict-markers"
-m live -v tests/custom

build:
name: Build distributions
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: sdk

on:
push:
branches: [main]
paths: [openapi.json, stainless.yml]
schedule:
- cron: "0 6 * * *"
workflow_dispatch:

permissions:
contents: read

jobs:
regenerate:
runs-on: ubuntu-latest
permissions:
contents: write # push ferrule/regenerate and the generated branch
pull-requests: write # open the PR and label it
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # the previous release and the generated branch
persist-credentials: false
# Pinned to a Ferrule commit; bump it to take generator fixes. Ferrule is not on PyPI yet, so
# the action installs it from its own checkout.
- uses: shrijayan/ferrule/action@9bc0666bdd8fc7661fab11fe2d07703e28c0bdc1
with:
spec: openapi.json
config: stainless.yml
verify: true # fails on an unlabeled breaking change
open-pr: true # labels major / minor / patch
ferrule-version: source
github-token: ${{ secrets.GITHUB_TOKEN }}
15 changes: 8 additions & 7 deletions .github/workflows/sync-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ jobs:
run: |
bun add --exact --ignore-scripts "@opencode/protocol@${{ steps.version.outputs.version }}" "effect@${{ steps.version.outputs.effect }}"
bun run extract.ts
- name: Regenerate and verify
- name: Convert the contract and update the config
# Stdlib-only scripts from Ferrule (scripts/opencode/); the SDK itself is regenerated by
# sdk.yml once this PR is merged.
run: |
uv sync --locked
uv run python -m codegen
uv run ruff check .
uv run pyright
uv run pytest -m "not live"
uv run --no-project --python 3.12 python scripts/opencode/contract_to_openapi.py spec/contract.json openapi.json
uv run --no-project --python 3.12 python scripts/opencode/config_models.py openapi.json stainless.yml
- run: git diff > sync.patch
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
Expand Down Expand Up @@ -76,5 +75,7 @@ jobs:
body: |
Regenerated from `@opencode/protocol@${{ needs.generate.outputs.version }}`.

Review `api.md` for added, removed or changed operations before merging.
Updates `spec/contract.json`, `openapi.json` and the models in `stainless.yml`. Once merged,
the `sdk` workflow regenerates the SDK with Ferrule and opens a `ferrule/regenerate` PR
with the public API changes.
delete-branch: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ codegen/extract/.cache/
# MkDocs output
site/
.cache/
.stdy.log
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Using opencode 0.0.1

Instructions for coding agents writing code that uses the `opencode` package.

- Read `SKILL.md` next to this file first: install, authentication (`OPENCODE_PASSWORD`), one example per resource and error handling, for this exact version.
- Look up method signatures and types in `api.md` (https://github.com//blob/main/api.md); every request and response type is importable from `opencode.types`.
- Construct one `Opencode` (or `AsyncOpencode`) and reuse it; do not pass secrets in code.
- Catch `opencode.APIError` subclasses rather than `httpx` errors; retries and timeouts are already built in.
- Do not edit generated files of the SDK; they are regenerated from the API spec.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ Releases are published automatically on every merge to `main`. Release notes for

## Unreleased

### Breaking: generated with Ferrule

The client is now generated by [Ferrule](https://github.com/shrijayan/ferrule) from `openapi.json` and
`stainless.yml` (both made from `spec/contract.json`), in the layout Stainless-generated SDKs use. The hand-written
features (events, RPC, PTY, the server launcher, `session.run`, typed errors) are unchanged in use.

- Inline types are nested in the module of the model that holds them instead of exported from `opencode.types`
(`opencode.types.ConfigInfoCompaction` is `opencode.types.shared.config_info.Compaction`); named schemas keep
their names.
- Requests take `TypedDict` params (`opencode.types.shared_params.ModelRef`, by Python field name:
`{"id": ..., "provider_id": ...}`) and return pydantic models.
- Optional arguments default to `omit` (`opencode.omit`) instead of `NOT_GIVEN`.
- `file.write(path=..., body=...)` takes the bytes (or a local file path) as `body` instead of `payload`; `shell.create(timeout=)` is
`api_timeout=`; `rpc.call(method, rpc_id=...)` takes `rpc_id` by keyword.
- Methods that returned an envelope's `data` still do; the envelope and wrapper types (`GenerateTextResponseData`,
`Vcs*Response`, ...) are gone, and union types have no `Unknown*` fallback classes.
- Error bodies of non-2xx responses are not typed models (`*ErrorBody`); the typed exceptions remain, with the body
fields as attributes.
- `client.page()` and `client.stream()` are gone; `client.get_api_list()` and `stream=True` cover them.
- Licence: the generated code and the runtime are Apache-2.0 (`LICENSE-APACHE`, `NOTICE`); the hand-written code
stays MIT.

- `client.events`: a shared event stream with one connection for all subscribers, bounded per-subscriber queues,
`server.connected` replay, `of_type` / `for_session` / `where` filters, `on_activity` keepalive hook, cancellation,
and opt-in reconnection with backoff (`EventHub(client, reconnect=True)`).
Expand Down
61 changes: 31 additions & 30 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# Contributing

Thanks for helping. This project is a generated client with a small hand-written runtime, so most changes fall
into one of two places.
Thanks for helping. Most of this client is generated by [Ferrule](https://github.com/shrijayan/ferrule) from
`openapi.json` and `stainless.yml`; the SDK's own features are written by hand next to it.

## Setup

```bash
uv sync
uv run pytest -m "not live"
./scripts/mock --daemon # the mock server the generated tests call (needs Node)
uv run pytest
uv run pytest --confcutdir=tests/custom -o asyncio_mode=strict -o addopts="-ra --strict-markers" -m "not live" tests/custom
```

The generated tests use pytest-asyncio; the hand-written ones in `tests/custom` use anyio and trio, so they run
apart.

Live tests start a real `opencode serve` and need the [OpenCode CLI](https://opencode.ai) on `PATH`. They make no
model calls and use a temporary data directory:

```bash
uv run pytest -m live
uv run pytest --confcutdir=tests/custom -o asyncio_mode=strict -o addopts="-ra --strict-markers" -m live tests/custom
```

## Checks
Expand All @@ -26,8 +31,7 @@ uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run mypy
uv run python -m codegen --check
uv run pytest
uv run pytest # and tests/custom, as above
```

## Documentation
Expand All @@ -43,51 +47,48 @@ uv run mkdocs build --strict

### Docs must keep up with the code

A change to the public API is not done until the docs cover it. CI enforces this:

| Check | Where | Fails when |
| --- | --- | --- |
| `tests/test_docs_coverage.py` | `pytest` job | A resource, method, `opencode.__all__` name or `opencode.types` model is not in the reference; an exported class or one of its public methods has no docstring; a resource group is missing from `docs/guides/index.md`; a guide mentions a method or type that does not exist |
| ruff `D1` rules | Lint job | A public function in a public module has no docstring |
| `mkdocs build --strict` | Docs workflow | A link or cross-reference is broken |

Each failure names what is missing and the file to change. In practice:

- **New endpoint or resource group** (from a spec sync): resource and type pages are generated from the code, so
they appear on their own. Add a row for a new group to `docs/guides/index.md`.
- **New hand-written class or function exported from `opencode`**: give it a docstring and add a `::: opencode.Name`
directive to the right page in `docs/reference/`.
- **Renamed or removed API**: update every guide that mentions it.
A change to the public API is not done until the docs cover it. `mkdocs build --strict` (docs workflow) fails on a
broken link or cross-reference. Resource and type pages are generated from the code, so a new endpoint appears on
its own; add a row for a new resource group to `docs/guides/index.md`, give a new hand-written export a docstring
and a `::: opencode.Name` directive in `docs/reference/`, and update every guide that mentions renamed or removed
API.

## Layout

| Path | What it is | Edit by hand? |
| --- | --- | --- |
| `codegen/extract/` | Exports the OpenCode contract from `@opencode/protocol` | Yes |
| `spec/contract.json` | The exported contract | No, run the extractor |
| `codegen/*.py` | The Python code generator | Yes |
| `src/opencode/types/`, `src/opencode/resources/`, `src/opencode/_errors.py`, `api.md` | Generated code | No |
| `src/opencode/_*.py` (other) | Transport, retries, streaming, pagination, launcher | Yes |
| `tests/` | Tests | Yes |
| `scripts/opencode/` | Contract to `openapi.json`, and the model names in `stainless.yml` (from Ferrule) | Rarely |
| `openapi.json`, `stainless.yml` | What Ferrule generates from; `stainless.yml` also lists the files Ferrule keeps | `stainless.yml` yes |
| Files listed in `ferrule.lock` | Generated code, tests and runtime | Prefer not; edits are kept by a three-way merge |
| `src/opencode/lib/`, `_events.py`, `_rpc.py`, `_pty.py`, `_server.py`, `_service.py`, `service.py`, `testing.py`, `_errors.py` | This SDK's own features | Yes |
| `tests/custom/` | Tests of the hand-written code | Yes |

## Updating to a new OpenCode version

The `Sync OpenCode spec` workflow does this nightly and opens a pull request. To do it locally:
The `Sync OpenCode spec` workflow does this nightly and opens a pull request that updates `spec/contract.json`,
`openapi.json` and `stainless.yml`. When it is merged, the `sdk` workflow regenerates the SDK with Ferrule and opens
a `ferrule/regenerate` pull request labelled with the semver bump of the public API change. To do it locally:

```bash
cd codegen/extract
bun add --exact --ignore-scripts @opencode/protocol@<version> effect@<matching version>
bun run extract.ts
cd ../..
uv run python -m codegen
uv run --no-project --python 3.12 python scripts/opencode/contract_to_openapi.py spec/contract.json openapi.json
uv run --no-project --python 3.12 python scripts/opencode/config_models.py openapi.json stainless.yml
ferrule regenerate --repo . --spec openapi.json --config stainless.yml
```

Then review the diff of `api.md` and update the README version table if the OpenCode minor version changed.
`ferrule regenerate` commits the new generation on the `generated` branch and merges it into the current branch,
so hand-written code, edits to generated files and the `keep_files` of `stainless.yml` survive. Review the diff of
`api.md`, and update the README version table if the OpenCode minor version changed.

## Changing generated code

Change the generator in `codegen/`, run `uv run python -m codegen`, and commit both the generator change and the
regenerated files. Never edit generated files directly; CI will reject the change.
Change `stainless.yml` (or the spec conversion) and regenerate, or fix the generator in Ferrule. A small edit to a
generated file is kept by the next regeneration's three-way merge, but a change in `src/opencode/lib/` is clearer.

## Releases

Expand Down
Loading
Loading