Skip to content

Plugin RPC

Declare a plugin RPC once as a class, then call it with typed inputs and outputs:

from pydantic import BaseModel
from opencode import RpcCallError, RpcDefinition, rpc_event, rpc_method


class AddInput(BaseModel):
    amount: int


class Total(BaseModel):
    total: int


class Counter(RpcDefinition, id="counter"):
    add = rpc_method(AddInput, Total)
    reset = rpc_method(None, Total)
    changed = rpc_event(Total)


counter = client.rpc(Counter)
try:
    print(counter.call(Counter.add, AddInput(amount=2)).total)
except RpcCallError as error:
    print(error.type, error.message, error.data)

with counter.subscribe(Counter.changed) as changes:  # only rpc.counter.changed, with validated data
    for change in changes:
        print(change.data.total)

Any type pydantic validates works for inputs, outputs and event data. rpc_method(..., errors={"not_found": Model}) validates the data of declared errors. Failures raise RpcCallError; crashes and outputs that do not match the declared type raise RpcCallInternalError. counter.events() yields every raw event of the RPC. The untyped client.rpc.call(rpc_id, method, input=...) is still available.

Async

The async client returns AsyncRpcClient, with the same methods:

counter = client.rpc(Counter)
total = await counter.call(Counter.add, AddInput(amount=2))
async with counter.subscribe(Counter.changed) as changes:
    async for change in changes:
        print(change.data.total)

RPC events travel on the shared event stream, so they share its delivery rules.