Loading...
Loading...
Rust SDK reference for the OpenCode HTTP API and SSE streaming. Use when implementing, debugging, or reviewing code that integrates with opencode-sdk, including client setup, session/message APIs, event streaming, managed server/runtime workflows, feature-flag behavior, and error handling patterns.
npx skill4agent add alpha-innovation-labs/opencode-rs-sdk opencode-rs-sdkRust SDK for OpenCode HTTP API with SSE streaming support. Provides ergonomic async client, 15 REST API modules, 40+ event types, and managed server lifecycle.
httpsseserverclicompile_error!Client::run_simple_text()wait_for_idle_text()OpencodeErroris_not_found()is_validation_error()SseSubscriptionRawSseSubscription.close()ManagedServer.stop()x-opencode-directoryClientBuilder::directory()ManagedRuntimeBuilder::directory()urlencoding::encode()| Constraint | Value | Impact |
|---|---|---|
| Platform | Unix only (Linux/macOS) | Windows compilation fails |
| Rust Edition | 2024 | Requires Rust 1.85+ |
| Default Features | | Always available unless disabled |
| Optional Features | | Requires explicit |
| Full Feature Set | | Use for complete functionality |
| Default Server URL | | ClientBuilder default |
| Timeout Default | 300 seconds | Suitable for long AI requests |
let client = Client::builder().build()?;
let session = client.run_simple_text("Hello, AI!").await?;
let response = client.wait_for_idle_text(&session.id, Duration::from_secs(60)).await?;let session = client.create_session_with_title("My Coding Task").await?;let mut subscription = client.subscribe_session(&session.id).await?;
while let Some(event) = subscription.recv().await {
match event {
Event::MessagePartUpdated { properties } => println!("{}", properties.delta.unwrap_or_default()),
Event::SessionIdle { .. } => break,
_ => {}
}
}let server = ManagedServer::start(ServerOptions::new().port(8080)).await?;
let client = Client::builder().base_url(server.url()).build()?;
// Server auto-stops when `server` is droppedCargo.toml[dependencies]
opencode-sdk = "0.1"
# Or with all features:
opencode-sdk = { version = "0.1", features = ["full"] }use opencode_sdk::{Client, ClientBuilder};
let client = Client::builder()
.base_url("http://127.0.0.1:4096")
.directory("/path/to/project")
.timeout_secs(300)
.build()?;src/
├── lib.rs # Crate root, re-exports, feature gates
├── client.rs # Client, ClientBuilder - ergonomic API entrypoint
├── error.rs # OpencodeError, Result<T> - error handling
├── sse.rs # SseSubscriber, SseSubscription, SessionEventRouter
├── server.rs # ManagedServer, ServerOptions - server feature
├── cli.rs # CliRunner, RunOptions, CliEvent - cli feature
├── runtime.rs # ManagedRuntime - server+http features
├── http/ # HTTP API modules (requires http feature)
│ ├── mod.rs # HttpClient, HttpConfig
│ ├── sessions.rs # SessionsApi - 18 endpoints
│ ├── messages.rs # MessagesApi - 6 endpoints
│ ├── files.rs # FilesApi
│ ├── tools.rs # ToolsApi
│ └── ... # 11 more API modules
└── types/ # Data models
├── mod.rs # Type re-exports
├── session.rs # Session, SessionCreateOptions
├── message.rs # Message, Part, PromptRequest
├── event.rs # Event enum (40 variants)
└── ... # 11 more type modulesClientsrc/client.rsbase_url(url)http://127.0.0.1:4096directory(dir)x-opencode-directorytimeout_secs(secs)build()httplet sessions = client.sessions(); // SessionsApi
let messages = client.messages(); // MessagesApi
let parts = client.parts(); // PartsApi
let permissions = client.permissions(); // PermissionsApi
let questions = client.questions(); // QuestionsApi
let files = client.files(); // FilesApi
let find = client.find(); // FindApi
let providers = client.providers(); // ProvidersApi
let mcp = client.mcp(); // McpApi
let pty = client.pty(); // PtyApi
let config = client.config(); // ConfigApi
let tools = client.tools(); // ToolsApi
let project = client.project(); // ProjectApi
let worktree = client.worktree(); // WorktreeApi
let misc = client.misc(); // MiscApilet sub = client.subscribe().await?; // All events for directory
let sub = client.subscribe_session(id).await?; // Filtered to session
let sub = client.subscribe_global().await?; // Global events (all dirs)
let raw = client.subscribe_raw().await?; // Raw JSON frames
let router = client.session_event_router().await?; // Get cached router// Create session and send text (returns immediately, use SSE for response)
let session = client.run_simple_text("Hello").await?;
// Create session with title
let session = client.create_session_with_title("Task").await?;
// Send text asynchronously (empty response, use SSE)
client.send_text_async(&session.id, "Hello", None).await?;
// Subscribe, send, and wait for idle with text collection
let text = client.send_text_async_and_wait_for_idle(&session.id, "Hello", None, Duration::from_secs(60)).await?;
// Wait for idle on existing subscription
let text = client.wait_for_idle_text(&session.id, Duration::from_secs(60)).await?;httpsrc/http/sessions.rs| Method | Endpoint | Description |
|---|---|---|
| POST /session | Create new session |
| POST /session | Create with convenience options |
| GET /session/{id} | Get session by ID |
| GET /session | List all sessions |
| DELETE /session/{id} | Delete session |
| POST /session/{id}/fork | Fork session |
| POST /session/{id}/abort | Abort active session |
| PATCH /session/{id} | Update session |
| POST /session/{id}/init | Initialize session |
| POST /session/{id}/share | Share session |
| DELETE /session/{id}/share | Unshare session |
| POST /session/{id}/revert | Revert to message |
| POST /session/{id}/unrevert | Undo revert |
| POST /session/{id}/summarize | Summarize session |
| GET /session/{id}/diff | Get session diff |
| GET /session/{id}/diff?messageId={msg_id} | Get diff since message |
| GET /session/status | Get server status |
| GET /session/{id}/children | Get forked sessions |
| GET /session/{id}/todo | Get todo items |
directorylet path = if let Some(directory) = &req.directory {
format!("/session?directory={}", urlencoding::encode(directory))
} else {
"/session".to_string()
};src/http/messages.rs| Method | Endpoint | Description |
|---|---|---|
| POST /session/{id}/message | Send prompt |
| POST /session/{id}/prompt_async | Async prompt (empty response) |
| POST /session/{id}/prompt_async | Convenience text sender |
| GET /session/{id}/message | List messages |
| GET /session/{id}/message/{mid} | Get message |
| DELETE /session/{id}/message/{mid} | Remove message |
prompt()prompt_async()request_empty()request_json()src/http/parts.rssrc/http/permissions.rssrc/http/questions.rssrc/http/files.rssrc/http/find.rssrc/http/providers.rssrc/http/mcp.rssrc/http/pty.rssrc/http/config.rssrc/http/tools.rssrc/http/project.rssrc/http/worktree.rssrc/http/misc.rssrc/types/session.rspub struct Session {
pub id: String,
pub project_id: Option<String>,
pub directory: Option<String>,
pub parent_id: Option<String>,
pub summary: Option<SessionSummary>,
pub share: Option<ShareInfo>,
pub title: String,
pub version: String,
pub time: Option<SessionTime>,
pub permission: Option<Ruleset>,
pub revert: Option<RevertInfo>,
}
pub struct SessionCreateOptions { /* builder pattern */ }
pub struct CreateSessionRequest { parent_id, title, permission, directory } // directory is query param!
pub struct UpdateSessionRequest { title }
pub struct SummarizeRequest { provider_id, model_id, auto }
pub struct RevertRequest { message_id, part_id }
pub struct SessionStatus { active_session_id, busy }
pub struct SessionDiff { diff, files }
pub struct TodoItem { id, content, completed, priority }src/types/message.rspub struct Message {
pub info: MessageInfo,
pub parts: Vec<Part>,
}
pub struct MessageInfo {
pub id: String,
pub session_id: Option<String>,
pub role: String, // "user", "assistant", "system"
pub time: MessageTime,
pub agent: Option<String>,
pub variant: Option<String>,
}
pub enum Part { // 12 variants
Text { id, text, synthetic, ignored, metadata },
File { id, mime, url, filename, source },
Tool { id, call_id, tool, input, state, metadata },
Reasoning { id, text, metadata },
StepStart { id, snapshot },
StepFinish { id, reason, snapshot, cost, tokens },
Snapshot { id, snapshot },
Patch { id, hash, files },
Agent { id, name, source },
Retry { id, attempt, error },
Compaction { id, auto },
Subtask { id, prompt, description, agent, command },
Unknown, // For forward compatibility
}
pub enum PromptPart {
Text { text, synthetic, ignored, metadata },
File { mime, url, filename },
Agent { name },
Subtask { prompt, description, agent, command },
}
pub struct PromptRequest {
pub parts: Vec<PromptPart>,
pub message_id: Option<String>,
pub model: Option<ModelRef>,
pub agent: Option<String>,
pub no_reply: Option<bool>,
pub system: Option<String>,
pub variant: Option<String>,
}
pub enum ToolState { // 5 variants - ORDER MATTERS for untagged deserialization
Completed(ToolStateCompleted), // Must come before more specific variants
Error(ToolStateError),
Running(ToolStateRunning),
Pending(ToolStatePending),
Unknown(serde_json::Value),
}ToolState#[serde(untagged)]CompletedErrorPendingRunningsrc/types/event.rsServerConnectedServerHeartbeatServerInstanceDisposedGlobalDisposedSessionCreatedSessionUpdatedSessionDeletedSessionDiffSessionErrorSessionCompactedSessionStatusSessionIdleMessageUpdatedMessageRemovedMessagePartUpdatedMessagePartRemovedPtyCreatedPtyUpdatedPtyExitedPtyDeletedPermissionUpdatedPermissionRepliedPermissionAskedPermissionRepliedNextProjectUpdatedFileEditedFileWatcherUpdatedVcsBranchUpdatedLspUpdatedLspClientDiagnosticsCommandExecutedMcpToolsChangedInstallationUpdatedInstallationUpdateAvailableIdeInstalledTuiPromptAppendTuiCommandExecuteTuiToastShowTuiSessionSelectTodoUpdated#[serde(tag = "type")]"type"#[serde(alias = "sessionID")]Event::Unknownsrc/sse.rspub struct SseSubscriber { /* creates subscriptions */ }
pub struct SseSubscription { /* typed event receiver */ }
pub struct RawSseSubscription { /* raw JSON receiver */ }
pub struct SessionEventRouter { /* multiplexes to per-session channels */ }
pub struct SseOptions {
pub capacity: usize, // default: 256
pub initial_interval: Duration, // default: 250ms
pub max_interval: Duration, // default: 30s
}
pub struct SessionEventRouterOptions {
pub upstream: SseOptions,
pub session_capacity: usize, // default: 256
pub subscriber_capacity: usize, // default: 256
}
pub struct SseStreamStats {
pub events_in: u64, // server frames received
pub events_out: u64, // delivered to caller
pub dropped: u64, // filtered or channel full
pub parse_errors: u64, // bad JSON
pub reconnects: u64, // retry count
pub last_event_id: Option<String>, // resumption token
}pub async fn subscribe(&self, opts: SseOptions) -> Result<SseSubscription>;
pub async fn subscribe_typed(&self, opts: SseOptions) -> Result<SseSubscription>;
pub async fn subscribe_global(&self, opts: SseOptions) -> Result<SseSubscription>;
pub async fn subscribe_typed_global(&self, opts: SseOptions) -> Result<SseSubscription>;
pub async fn subscribe_raw(&self, opts: SseOptions) -> Result<RawSseSubscription>;
pub async fn subscribe_session(&self, session_id: &str, opts: SseOptions) -> Result<SseSubscription>;
pub async fn session_event_router(&self, opts: SessionEventRouterOptions) -> Result<SessionEventRouter>;impl SseSubscription {
pub async fn recv(&mut self) -> Option<Event>; // None = stream closed
pub fn stats(&self) -> SseStreamStats;
pub fn close(&self);
}
impl RawSseSubscription {
pub async fn recv(&mut self) -> Option<RawSseEvent>;
pub fn stats(&self) -> SseStreamStats;
pub fn close(&self);
}
impl SessionEventRouter {
pub async fn subscribe(&self, session_id: &str) -> SseSubscription;
pub fn stats(&self) -> SseStreamStats;
pub fn close(&self);
}EsEvent::Opensubscribe_session()message.part.updatedproperties.part.sessionID|sessionIdsession.idle/errorproperties.sessionID|sessionIddroppedopencode servepub struct ServerOptions {
pub port: Option<u16>, // None = random port
pub hostname: String, // default: "127.0.0.1"
pub directory: Option<PathBuf>,
pub config_json: Option<String>, // via OPENCODE_CONFIG_CONTENT
pub startup_timeout_ms: u64, // default: 5000
pub binary: String, // default: "opencode"
}
pub struct ManagedServer {
pub async fn start(opts: ServerOptions) -> Result<Self>;
pub fn url(&self) -> &Url;
pub fn port(&self) -> u16;
pub async fn stop(mut self) -> Result<()>;
pub fn is_running(&mut self) -> bool;
}/docopencode run --format jsonpub struct RunOptions {
pub format: Option<String>, // default: "json"
pub attach: Option<String>,
pub continue_session: bool,
pub session: Option<String>,
pub file: Vec<String>,
pub share: bool,
pub model: Option<String>,
pub agent: Option<String>,
pub title: Option<String>,
pub port: Option<u16>,
pub command: Option<String>,
pub directory: Option<PathBuf>,
pub binary: String, // default: "opencode"
}
pub struct CliEvent {
pub r#type: String,
pub timestamp: Option<i64>,
pub session_id: Option<String>,
pub data: serde_json::Value,
}
pub struct CliRunner {
pub async fn start(prompt: &str, opts: RunOptions) -> Result<Self>;
pub async fn recv(&mut self) -> Option<CliEvent>;
pub async fn collect_text(&mut self) -> String;
}is_text()is_step_start()is_step_finish()is_error()is_tool_use()text()Stdio::inherit()let runtime = ManagedRuntime::builder()
.hostname("127.0.0.1")
.port(4096)
.directory("/test/project")
.startup_timeout_ms(10_000)
.start()
.await?;
let client = runtime.client();
// Use client...
runtime.stop().await?;
// Or just drop runtime to stop serverlet runtime = ManagedRuntime::start_for_cwd().await?;
let session = runtime.client().run_simple_text("test").await?;httpsseuse opencode_sdk::{Client, ClientBuilder};
let client = Client::builder().build()?;Client::builder().base_url(url).directory(dir).build()client.sessions().create(&req).awaitResult<T>OpencodeErrorClient::builder()ClientBuilder::build()client.sessions()client.messages()client.subscribe_session(id).awaitclient.run_simple_text(text).awaitprompt_asyncOpencodeError::Httphttpbuild()OpencodeError::InvalidConfigsrc/client.rshttplet sessions = client.sessions();sessions.create(&CreateSessionRequest::default()).awaitsessions.create_with(SessionCreateOptions::new().with_title("Task")).awaitsessions.list().awaitsessions.delete(&id).awaitcreate(req)create_with(opts)get(id)list()delete(id)fork(id)share(id)unshare(id)revert(id, req)create_withCreateSessionRequestdirectorysrc/http/sessions.rshttplet messages = client.messages();messages.prompt(&session_id, &PromptRequest::text("Hello")).awaitmessages.prompt_async(&session_id, &req).awaitmessages.list(&session_id).awaitmessages.get(&session_id, &message_id).awaitprompt(session_id, req)prompt_async(session_id, req)send_text_async(session_id, text, model)list(session_id)get(session_id, message_id)remove(session_id, message_id)prompt_asyncPromptRequest::text("...").with_model("provider", "model")src/http/messages.rssseuse opencode_sdk::sse::{SseSubscriber, SseOptions};
let subscriber = SseSubscriber::new(
"http://127.0.0.1:4096".into(),
Some("/my/project".into()),
None, // optional ReqClient
);
let mut sub = subscriber.subscribe(SseOptions::default()).await?;
while let Some(event) = sub.recv().await {
// handle event
}SseSubscriber::new(base_url, directory, client)subscriber.subscribe(SseOptions::default()).await?while let Some(event) = sub.recv().await { /* process */ }sub.close()subscribe(opts)subscribe_session(session_id, opts)subscribe_global(opts)subscribe_raw(opts)SseSubscriptionRawSseSubscriptionrecv()Nonestats().droppedsrc/sse.rsopencode_sdk::types::eventuse opencode_sdk::types::event::Event;let event: Event = serde_json::from_str(&json)?;
match event {
Event::MessagePartUpdated { properties } => {
// Streaming text delta
if let Some(delta) = &properties.delta {
print!("{}", delta);
}
}
Event::SessionIdle { properties } => println!("Session idle: {}", properties.info.id),
Event::PermissionAsked { properties } => {
let request = &properties.request;
println!("Permission: {} for {:?}", request.permission, request.patterns);
}
Event::Unknown => println!("Unknown event type"),
_ => {}
}event.session_id()Option<&str>event.is_heartbeat()event.is_connected()session_id()Event::Unknownsrc/types/event.rsserveruse opencode_sdk::server::{ManagedServer, ServerOptions};
let server = ManagedServer::start(ServerOptions::new().port(8080)).await?;ServerOptions::new().port(8080).directory("/project")let server = ManagedServer::start(opts).await?let url = server.url()let client = Client::builder().base_url(url).build()?server.stop().await?ServerOptions::new()ServerOptions::port()::hostname()::directory()::config_json()ManagedServer::start(opts).awaitManagedServer::url()::port()ManagedServer::stop().awaitManagedServerconfig_jsonOPENCODE_CONFIG_CONTENTsrc/server.rscliuse opencode_sdk::cli::{CliRunner, RunOptions};
let mut runner = CliRunner::start("Hello", RunOptions::new()).await?;RunOptions::new().model("provider/model").agent("code")let mut runner = CliRunner::start("prompt", opts).await?while let Some(event) = runner.recv().await { /* process */ }let text = runner.collect_text().awaitRunOptions::new()RunOptions::model()::agent()::title()::attach()CliRunner::start(prompt, opts).awaitCliRunner::recv().awaitCliRunner::collect_text().awaitCliEvent::is_text()::text()formatshare: truesrc/cli.rsopencode_sdklib.rsuse opencode_sdk::{OpencodeError, Result};
fn handle_error(err: OpencodeError) {
match err {
OpencodeError::Http { status, name, message, .. } => {
eprintln!("HTTP {}: {}", status, message);
}
OpencodeError::Network(msg) => {
eprintln!("Network error: {}", msg);
}
OpencodeError::StreamClosed => {
eprintln!("SSE stream closed unexpectedly");
}
_ => eprintln!("Other error: {}", err),
}
}let result = client.run_simple_text("test").await;
if let Err(e) = result {
if e.is_not_found() {
// Handle 404
} else if e.is_server_error() {
// Handle 5xx
}
}OpencodeError::http(status, body)is_not_found()is_validation_error()is_server_error()error_name()dataStreamClosedsrc/error.rs| Type | Location | Description |
|---|---|---|
| | Main ergonomic API client |
| | Builder for Client |
| | Error enum (13 variants) |
| | Type alias for Result |
| Type | Location | Description |
|---|---|---|
| | Creates SSE subscriptions |
| | Typed event subscription |
| | Raw JSON subscription |
| | Multiplexes to sessions |
| | Subscription options |
| | Router options |
| | Diagnostics snapshot |
| | Raw SSE frame |
| Type | Location | Description |
|---|---|---|
| | Managed server process |
| | Server configuration |
| | Server + Client combo |
| | CLI wrapper |
| | CLI options |
| | CLI output event |
| Type | Location | Description |
|---|---|---|
| | Low-level HTTP client |
| | HTTP configuration |
| | Sessions API client |
| | Messages API client |
| Type | Location | Description |
|---|---|---|
| | Session model |
| | Builder for create |
| | Message with parts |
| | Message metadata |
| | Content part enum (12 variants) |
| | Prompt request |
| | Prompt part enum |
| | SSE event enum (40 variants) |
| | Global event wrapper |
| | Tool execution state |
// ❌ Won't compile without http feature
let client = Client::builder().build()?; // Returns OpencodeError::InvalidConfig
// ✅ Enable http feature in Cargo.toml
opencode-sdk = { version = "0.1", features = ["http"] }// ❌ Response events lost
client.send_text_async(&session_id, "Hello", None).await?;
let sub = client.subscribe_session(&session_id).await?; // Too late!
// ✅ Subscribe first
let sub = client.subscribe_session(&session_id).await?;
client.send_text_async(&session_id, "Hello", None).await?;
// Now events are captured// ❌ Compiling on Windows will fail with:
// error: opencode_sdk only supports Unix-like platforms (Linux/macOS). Windows is not supported.
// ✅ Use WSL, Docker, or macOS/Linux// ❌ Server dropped too early
{
let server = ManagedServer::start(ServerOptions::new()).await?;
let client = Client::builder().base_url(server.url()).build()?;
} // Server killed here!
// ❌ Client requests will fail
// ✅ Keep server alive
let server = ManagedServer::start(ServerOptions::new()).await?;
let client = Client::builder().base_url(server.url()).build()?;
// Use client while server in scope
server.stop().await?; // Or let it drop// ❌ Generic error handling misses context
if let Err(e) = result {
eprintln!("Error: {}", e);
}
// ✅ Use helper methods for specific handling
match result {
Err(e) if e.is_not_found() => println!("Not found"),
Err(e) if e.is_validation_error() => println!("Validation: {:?}", e.error_name()),
Err(e) => eprintln!("Other: {}", e),
Ok(v) => v,
}// ❌ Not checking for dropped events
let sub = client.subscribe_session(&id).await?;
// Slow processing...
while let Some(event) = sub.recv().await {
tokio::time::sleep(Duration::from_secs(1)).await; // Too slow!
}
// ✅ Monitor stats
if sub.stats().dropped > 0 {
tracing::warn!("Dropped {} events", sub.stats().dropped);
}// ❌ Treating directory as body field
let request = CreateSessionRequest {
directory: Some("/my/project".to_string()),
..Default::default()
};
// Session won't have directory context!
// ✅ Directory is a query parameter - use builder
let request = SessionCreateOptions::new()
.with_directory("/my/project")
.into();// ❌ Path with special characters causes 404
let content = files.read("path/with spaces/file.txt").await;
// ✅ URL encode path parameters
let content = files.read(&urlencoding::encode("path/with spaces/file.txt")).await;// ❌ Can hang indefinitely if stream closes
let event = subscription.recv().await;
// ✅ Use timeout
let event = tokio::time::timeout(Duration::from_secs(30), subscription.recv()).await?;| Version | Notes |
|---|---|
| 0.1.x | Initial release with HTTP API, SSE streaming, managed server |
| Feature | Dependencies | APIs Enabled |
|---|---|---|
| reqwest, serde_json | All HTTP API modules |
| reqwest-eventsource, backon | SSE streaming, subscriptions |
| tokio/process, portpicker | ManagedServer |
| tokio/process | CliRunner |
| All above | Everything |
tokioserdethiserrorurlhttptokio-utilfuturesurlencodinguuidchronotracing