Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion RUST_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ cargo-dist · cargo-deny. **Out of scope:** tree-sitter (TUI-only).
- ✅ **HTTP transport** (PR #27): `opencode-llm::transport::complete` — `build_body` → reqwest `POST` (JSON) → SSE response → `decode_sse` → `Vec<LlmEvent>`; non-2xx → `LlmError::Status { code, retryable, message }` (429/5xx retryable), network → `LlmError::Http`. Tested end-to-end against an in-test **axum** server (happy-path event parity + 429/401 classification) over plain HTTP — no new external crates, no TLS yet. Collect-then-decode (incremental streaming is a refinement).
- ✅ **TLS re-add** (PR #28): `reqwest` now uses **`rustls-tls`** (rustls + ring — owner-approved; no system OpenSSL, best musl/windows-arm cross-compile). `transport::https_client` builds an HTTPS-only client with a timeout for real provider calls. `deny.toml` gained one license (`CDLA-Permissive-2.0`, for webpki-roots' bundled CA set); `ring 0.17.14` reports a clean `Apache-2.0 AND ISC` and needed nothing. The HTTP transport tests are unchanged (TLS only affects `https://`).
- ✅ **Executor** (PR #29): `executor::RetryPolicy` + `execute()` — exponential backoff (`base·2^(n-1)`, capped) + `Retry-After` override + +50%/no-`rand` jitter, retrying retryable `Status` (429/5xx, via the `retry_after` header the transport now parses) and network errors, stopping fast on non-retryable / `max_attempts`. `executor::redact_secrets` masks `sk-ant-…` keys (the transport redacts error bodies). All unit-tested (policy classification/backoff/cap/Retry-After, the retry loop, redaction) + an E2E (axum 429→retry→200). **anthropic-messages is now production-ready: decode + lowering + transport + TLS + executor.**
- ⬜ Next: the remaining protocols (openai-chat → openai-responses → gemini → bedrock-converse) over the same `Protocol`/transport/executor; provider fixtures for parity. No public cutover (the runner consumes it; Phase 4).
- ✅ **Second protocol: openai-chat** (PR #30): same `Protocol`/transport/executor scaffolding, validating the abstraction generalizes. Handles the wire differences — system as a `role:"system"` message, string content, `{type:"function",function:{…,parameters}}` tools, `tool_choice` modes, and `tool_calls[].function.arguments` streamed by `index`. OpenAI splits `finish_reason`/`usage` across the last chunks with no terminal event, so the trait gained a defaulted **`on_halt`** flush (Anthropic unaffected). Parity proven against the openai-chat cassettes (request bodies + response streams: text → `"Hello!"` 22/2, tool-call → `get_weather({"city":"Paris"})` 67/5).
- ⬜ Next: openai-responses → gemini → bedrock-converse over the same scaffolding. No public cutover (the runner consumes it; Phase 4).

### Phase 4 — Session runner 🟡
- ✅ **Control-flow spike** (PR #24, pure logic — no IO/cutover): `opencode-core::runner` proves the `session/runner/llm.ts` control flow maps to panic-free Rust. `die(TurnTransitionError)`/`catchDefect` → `TurnTransition` (`RebuildPreparedTurn{promotion}` / `ContinueAfterOverflowCompaction`) returned as `Err` + the `run_turn` restart driver (flips `OverflowRecovery` `Enabled→Disabled`; a second overflow is `DoubleOverflow`); `needsContinuation` → `TurnOutcome::{Continue,Done}`; the outer continuation loop → `run_session` (step-limited); `FiberSet` + `raceFirst(join,awaitEmpty)` → `ToolExecutor` (over `tokio::task::JoinSet`) with `drain` (fail-fast vs all-settled) + `cancel_all`. 14 unit tests exercise every transition + tool race/timeout/cancel.
Expand Down
9 changes: 9 additions & 0 deletions crates/opencode-llm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

pub mod anthropic;
pub mod executor;
pub mod openai_chat;
pub mod transport;

use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -168,6 +169,13 @@ pub trait Protocol {

/// Whether `event` terminates the stream.
fn terminal(&self, event: &Self::Event) -> bool;

/// Flush any pending state at stream end (`onHalt` in TS) — e.g. close an open text block or emit
/// the terminal `Finish` for protocols (like openai-chat) that split finish/usage across the last
/// chunks. Default: nothing (protocols that emit everything inline need no flush).
fn on_halt(&self, _state: &Self::State) -> Vec<LlmEvent> {
Vec::new()
}
}

/// A normalized LLM request (`packages/llm/src/schema/messages.ts` `LLMRequest`) — the core subset:
Expand Down Expand Up @@ -339,6 +347,7 @@ pub fn decode_sse<P: Protocol>(protocol: &P, body: &str) -> Result<Vec<LlmEvent>
break;
}
}
out.extend(protocol.on_halt(&state));
Ok(out)
}

Expand Down
Loading
Loading